/*
- * 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;
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;
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;
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;
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"));
.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
// 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
// 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
}
}
}
+
+ // 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));
+ }
}