2 * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.netconf.client.mdsal;
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;
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;
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;
71 * Holds QNames for all YANG modules reported by ietf-netconf-monitoring/state/schemas.
73 public final class NetconfStateSchemas implements NetconfDeviceSchemas {
74 public static final NetconfStateSchemas EMPTY = new NetconfStateSchemas(ImmutableSet.of());
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;
88 final var document = XmlUtil.newDocument();
89 final var filterElem = document.createElementNS(NamespaceURN.BASE, "filter");
90 filterElem.setAttribute("type", "subtree");
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);
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))
107 private final ImmutableSet<QName> availableYangSchemasQNames;
109 public NetconfStateSchemas(final Set<QName> availableYangSchemasQNames) {
110 this.availableYangSchemasQNames = ImmutableSet.copyOf(availableYangSchemasQNames);
114 public Set<QName> getAvailableYangSchemasQNames() {
115 return availableYangSchemasQNames;
119 * Issue get request to remote device and parse response to find all schemas under netconf-state/schemas.
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);
131 final var future = SettableFuture.<NetconfStateSchemas>create();
132 Futures.addCallback(deviceRpc.invokeNetconf(Get.QNAME, GET_SCHEMAS_RPC),
133 new FutureCallback<DOMRpcResult>() {
135 public void onSuccess(final DOMRpcResult result) {
136 onGetSchemasResult(future, id, modelContext, result);
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);
145 }, MoreExecutors.directExecutor());
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));
158 for (var error : errors) {
159 LOG.info("{}: schema retrieval warning: {}", id, error);
162 final var value = result.value();
164 LOG.warn("{}: missing RPC output", id);
168 final var data = value.childByArg(NETCONF_DATA_NODEID);
170 LOG.warn("{}: missing RPC data", id);
174 if (!(data instanceof AnyxmlNode<?> anyxmlData)) {
175 future.setException(new VerifyException("Unexpected data " + data.prettyTree()));
178 final var dataBody = anyxmlData.body();
179 if (!(dataBody instanceof DOMSource domDataBody)) {
180 future.setException(new VerifyException("Unexpected body " + dataBody));
184 // Server may include additional data which we do not understand. Make sure we trim the input before we try
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);
193 // Now normalize the anyxml content to the selected model context
194 final NormalizedNode normalizedData;
196 normalizedData = NormalizedDataUtil.transformDOMSourceToNormalizedNode(modelContext, filteredBody)
198 } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
199 LOG.debug("{}: failed to transform {}", id, filteredBody, e);
200 future.setException(e);
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()));
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);
217 if (!(netconfState instanceof ContainerNode netconfStateCont)) {
218 future.setException(new VerifyException("Unexpected netconf-state " + netconfState.prettyTree()));
223 final var schemas = netconfStateCont.childByArg(new NodeIdentifier(Schemas.QNAME));
224 if (schemas == null) {
225 LOG.warn("{}: missing schemas", id);
229 if (!(schemas instanceof ContainerNode schemasNode)) {
230 future.setException(new VerifyException("Unexpected schemas " + schemas.prettyTree()));
234 create(future, id, schemasNode);
238 * Parse response of get(netconf-state/schemas) to find all schemas under netconf-state/schemas.
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));
245 LOG.warn("{}: missing schema", id);
249 if (!(child instanceof SystemMapNode schemaMap)) {
250 future.setException(new VerifyException("Unexpected schemas " + child.prettyTree()));
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
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.
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
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);
282 future.set(new NetconfStateSchemas(builder.build()));
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);
293 // Note: module name or submodule name
294 final var identifier = (String) schemaEntry.getChildByArg(SCHEMA_IDENTIFIER_NODEID).body();
296 final var version = (String) schemaEntry.getChildByArg(SCHEMA_VERSION_NODEID).body();
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);
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);
313 boolean foundNetconf = false;
314 for (var locEntry : location.body()) {
315 final var loc = locEntry.body();
316 if (NETCONF_LOCATION.equals(loc)) {
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);
326 LOG.debug("{}: Ignoring schema due to no NETCONF location", id);
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);
341 static DOMSource ietfMonitoringCopy(final DOMSource domSource) {
342 final var sourceDoc = XmlUtil.newDocument();
343 sourceDoc.appendChild(sourceDoc.importNode(domSource.getNode(), true));
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;
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);
357 return new DOMSource(filteredElement);
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);