2 * Copyright (c) 2024 PANTHEON.tech, s.r.o. 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.impl;
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.base.Predicates;
14 import com.google.common.collect.Collections2;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.common.collect.ImmutableSet;
17 import com.google.common.collect.Sets;
18 import com.google.common.util.concurrent.FutureCallback;
19 import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture;
21 import com.google.common.util.concurrent.MoreExecutors;
22 import com.google.common.util.concurrent.SettableFuture;
23 import java.util.ArrayList;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
29 import java.util.Objects;
31 import java.util.concurrent.ExecutionException;
32 import java.util.stream.Collectors;
33 import org.opendaylight.netconf.api.CapabilityURN;
34 import org.opendaylight.netconf.client.mdsal.NetconfDevice.EmptySchemaContextException;
35 import org.opendaylight.netconf.client.mdsal.NetconfDeviceCapabilities;
36 import org.opendaylight.netconf.client.mdsal.api.DeviceNetconfSchema;
37 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
38 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.available.capabilities.AvailableCapability;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.available.capabilities.AvailableCapabilityBuilder;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.unavailable.capabilities.UnavailableCapability;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.unavailable.capabilities.UnavailableCapability.FailureReason;
43 import org.opendaylight.yangtools.yang.common.QName;
44 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
45 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
46 import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
47 import org.opendaylight.yangtools.yang.model.repo.api.EffectiveModelContextFactory;
48 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
49 import org.opendaylight.yangtools.yang.model.repo.api.SchemaRepository;
50 import org.opendaylight.yangtools.yang.model.repo.api.SchemaResolutionException;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * Schema builder that tries to build schema context from provided sources or biggest subset of it.
57 final class SchemaSetup implements FutureCallback<EffectiveModelContext> {
58 private static final Logger LOG = LoggerFactory.getLogger(SchemaSetup.class);
60 private final SettableFuture<DeviceNetconfSchema> resultFuture = SettableFuture.create();
61 private final Set<AvailableCapability> nonModuleBasedCapabilities = new HashSet<>();
62 private final Map<QName, FailureReason> unresolvedCapabilites = new HashMap<>();
63 private final Set<AvailableCapability> resolvedCapabilities = new HashSet<>();
65 private final RemoteDeviceId deviceId;
66 private final DeviceSources deviceSources;
67 private final NetconfSessionPreferences remoteSessionCapabilities;
68 private final SchemaRepository repository;
69 private final EffectiveModelContextFactory contextFactory;
71 private Collection<SourceIdentifier> requiredSources;
73 SchemaSetup(final SchemaRepository repository, final EffectiveModelContextFactory contextFactory,
74 final RemoteDeviceId deviceId, final DeviceSources deviceSources,
75 final NetconfSessionPreferences remoteSessionCapabilities) {
76 this.repository = requireNonNull(repository);
77 this.contextFactory = requireNonNull(contextFactory);
78 this.deviceId = requireNonNull(deviceId);
79 this.deviceSources = requireNonNull(deviceSources);
80 this.remoteSessionCapabilities = requireNonNull(remoteSessionCapabilities);
82 // If device supports notifications and does not contain necessary modules, add them automatically
83 if (remoteSessionCapabilities.containsNonModuleCapability(CapabilityURN.NOTIFICATION)) {
84 // FIXME: mutable collection modification!
85 deviceSources.getRequiredSourcesQName().addAll(List.of(
86 org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.netconf.notification._1._0.rev080714
87 .YangModuleInfoImpl.getInstance().getName(),
88 org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715
89 .YangModuleInfoImpl.getInstance().getName())
93 requiredSources = deviceSources.getRequiredSources();
94 final var missingSources = filterMissingSources(requiredSources);
96 addUnresolvedCapabilities(getQNameFromSourceIdentifiers(missingSources),
97 UnavailableCapability.FailureReason.MissingSource);
98 requiredSources.removeAll(missingSources);
101 ListenableFuture<DeviceNetconfSchema> startResolution() {
107 public void onSuccess(final EffectiveModelContext result) {
108 LOG.debug("{}: Schema context built successfully from {}", deviceId, requiredSources);
110 final Collection<QName> filteredQNames = Sets.difference(deviceSources.getRequiredSourcesQName(),
111 unresolvedCapabilites.keySet());
112 resolvedCapabilities.addAll(filteredQNames.stream()
113 .map(capability -> new AvailableCapabilityBuilder()
114 .setCapability(capability.toString())
115 .setCapabilityOrigin(remoteSessionCapabilities.capabilityOrigin(capability))
117 .collect(Collectors.toList()));
119 nonModuleBasedCapabilities.addAll(remoteSessionCapabilities.nonModuleCaps().keySet().stream()
120 .map(capability -> new AvailableCapabilityBuilder()
121 .setCapability(capability)
122 .setCapabilityOrigin(remoteSessionCapabilities.capabilityOrigin(capability))
124 .collect(Collectors.toList()));
127 resultFuture.set(new DeviceNetconfSchema(new NetconfDeviceCapabilities(
128 ImmutableMap.copyOf(unresolvedCapabilites), ImmutableSet.copyOf(resolvedCapabilities),
129 ImmutableSet.copyOf(nonModuleBasedCapabilities)), result));
133 public void onFailure(final Throwable cause) {
134 // schemaBuilderFuture.checkedGet() throws only SchemaResolutionException
135 // that might be wrapping a MissingSchemaSourceException so we need to look
136 // at the cause of the exception to make sure we don't misinterpret it.
137 if (cause instanceof MissingSchemaSourceException) {
138 requiredSources = handleMissingSchemaSourceException((MissingSchemaSourceException) cause);
139 } else if (cause instanceof SchemaResolutionException) {
140 requiredSources = handleSchemaResolutionException((SchemaResolutionException) cause);
142 LOG.debug("Unhandled failure", cause);
143 resultFuture.setException(cause);
151 private void trySetupSchema() {
152 if (!requiredSources.isEmpty()) {
153 // Initiate async resolution, drive it back based on the result
154 LOG.trace("{}: Trying to build schema context from {}", deviceId, requiredSources);
155 Futures.addCallback(contextFactory.createEffectiveModelContext(requiredSources), this,
156 MoreExecutors.directExecutor());
158 LOG.debug("{}: no more sources for schema context", deviceId);
159 resultFuture.setException(
160 new EmptySchemaContextException(deviceId + ": No more sources for schema context"));
164 private List<SourceIdentifier> filterMissingSources(final Collection<SourceIdentifier> origSources) {
165 return origSources.parallelStream()
166 .filter(sourceId -> {
168 repository.getSchemaSource(sourceId, YangTextSource.class).get();
170 } catch (InterruptedException | ExecutionException e) {
171 LOG.debug("Failed to acquire source {}", sourceId, e);
175 .collect(Collectors.toList());
178 private void addUnresolvedCapabilities(final Collection<QName> capabilities, final FailureReason reason) {
179 for (QName s : capabilities) {
180 unresolvedCapabilites.put(s, reason);
184 private List<SourceIdentifier> handleMissingSchemaSourceException(
185 final MissingSchemaSourceException exception) {
186 // In case source missing, try without it
187 final SourceIdentifier missingSource = exception.sourceId();
188 LOG.warn("{}: Unable to build schema context, missing source {}, will reattempt without it",
189 deviceId, missingSource);
190 LOG.debug("{}: Unable to build schema context, missing source {}, will reattempt without it",
191 deviceId, missingSource, exception);
192 final var qNameOfMissingSource = getQNameFromSourceIdentifiers(Sets.newHashSet(missingSource));
193 if (!qNameOfMissingSource.isEmpty()) {
194 addUnresolvedCapabilities(qNameOfMissingSource, UnavailableCapability.FailureReason.MissingSource);
196 return stripUnavailableSource(missingSource);
199 private Collection<SourceIdentifier> handleSchemaResolutionException(
200 final SchemaResolutionException resolutionException) {
201 // In case resolution error, try only with resolved sources
202 // There are two options why schema resolution exception occurred : unsatisfied imports or flawed model
203 // FIXME Do we really have assurance that these two cases cannot happen at once?
204 final var failedSourceId = resolutionException.sourceId();
205 if (failedSourceId != null) {
206 // flawed model - exclude it
207 LOG.warn("{}: Unable to build schema context, failed to resolve source {}, will reattempt without it",
208 deviceId, failedSourceId);
209 LOG.warn("{}: Unable to build schema context, failed to resolve source {}, will reattempt without it",
210 deviceId, failedSourceId, resolutionException);
211 addUnresolvedCapabilities(getQNameFromSourceIdentifiers(List.of(failedSourceId)),
212 UnavailableCapability.FailureReason.UnableToResolve);
213 return stripUnavailableSource(failedSourceId);
215 // unsatisfied imports
216 addUnresolvedCapabilities(
217 getQNameFromSourceIdentifiers(resolutionException.getUnsatisfiedImports().keySet()),
218 UnavailableCapability.FailureReason.UnableToResolve);
219 LOG.warn("{}: Unable to build schema context, unsatisfied imports {}, will reattempt with resolved only",
220 deviceId, resolutionException.getUnsatisfiedImports());
221 LOG.debug("{}: Unable to build schema context, unsatisfied imports {}, will reattempt with resolved only",
222 deviceId, resolutionException.getUnsatisfiedImports(), resolutionException);
223 return resolutionException.getResolvedSources();
226 private List<SourceIdentifier> stripUnavailableSource(final SourceIdentifier sourceIdToRemove) {
227 final var tmp = new ArrayList<>(requiredSources);
228 checkState(tmp.remove(sourceIdToRemove), "%s: Trying to remove %s from %s failed", deviceId, sourceIdToRemove,
233 private Collection<QName> getQNameFromSourceIdentifiers(final Collection<SourceIdentifier> identifiers) {
234 final Collection<QName> qNames = Collections2.transform(identifiers, this::getQNameFromSourceIdentifier);
236 if (qNames.isEmpty()) {
237 LOG.debug("{}: Unable to map any source identifiers to a capability reported by device : {}", deviceId,
240 return Collections2.filter(qNames, Predicates.notNull());
243 private QName getQNameFromSourceIdentifier(final SourceIdentifier identifier) {
244 // Required sources are all required and provided merged in DeviceSourcesResolver
245 for (final QName qname : deviceSources.getRequiredSourcesQName()) {
246 if (!qname.getLocalName().equals(identifier.name().getLocalName())) {
250 if (Objects.equals(identifier.revision(), qname.getRevision().orElse(null))) {
254 LOG.warn("Unable to map identifier to a devices reported capability: {} Available: {}",identifier,
255 deviceSources.getRequiredSourcesQName());
256 // return null since we cannot find the QName,
257 // this capability will be removed from required sources and not reported as unresolved-capability