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.NetconfDeviceSchemas;
38 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
39 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.available.capabilities.AvailableCapability;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.available.capabilities.AvailableCapabilityBuilder;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.unavailable.capabilities.UnavailableCapability;
43 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.unavailable.capabilities.UnavailableCapability.FailureReason;
44 import org.opendaylight.yangtools.yang.common.QName;
45 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
46 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
47 import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
48 import org.opendaylight.yangtools.yang.model.repo.api.EffectiveModelContextFactory;
49 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
50 import org.opendaylight.yangtools.yang.model.repo.api.SchemaRepository;
51 import org.opendaylight.yangtools.yang.model.repo.api.SchemaResolutionException;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * Schema builder that tries to build schema context from provided sources or biggest subset of it.
58 final class SchemaSetup implements FutureCallback<EffectiveModelContext> {
59 private static final Logger LOG = LoggerFactory.getLogger(SchemaSetup.class);
61 private final SettableFuture<DeviceNetconfSchema> resultFuture = SettableFuture.create();
62 private final Set<AvailableCapability> nonModuleBasedCapabilities = new HashSet<>();
63 private final Map<QName, FailureReason> unresolvedCapabilites = new HashMap<>();
64 private final Set<AvailableCapability> resolvedCapabilities = new HashSet<>();
65 private final RemoteDeviceId deviceId;
66 private final NetconfDeviceSchemas deviceSchemas;
67 private final Set<QName> deviceRequiredSources;
68 private final NetconfSessionPreferences sessionPreferences;
69 private final SchemaRepository repository;
70 private final EffectiveModelContextFactory contextFactory;
72 private Collection<SourceIdentifier> requiredSources;
74 SchemaSetup(final SchemaRepository repository, final EffectiveModelContextFactory contextFactory,
75 final RemoteDeviceId deviceId, final NetconfDeviceSchemas deviceSchemas,
76 final NetconfSessionPreferences sessionPreferences) {
77 this.repository = requireNonNull(repository);
78 this.contextFactory = requireNonNull(contextFactory);
79 this.deviceId = requireNonNull(deviceId);
80 this.deviceSchemas = requireNonNull(deviceSchemas);
81 this.sessionPreferences = requireNonNull(sessionPreferences);
83 // If device supports notifications and does not contain necessary modules, add them automatically
84 deviceRequiredSources = new HashSet<>(deviceSchemas.requiredSources());
85 if (sessionPreferences.containsNonModuleCapability(CapabilityURN.NOTIFICATION)) {
86 deviceRequiredSources.add(
87 org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.netconf.notification._1._0.rev080714
88 .YangModuleInfoImpl.getInstance().getName());
89 deviceRequiredSources.add(
90 org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715
91 .YangModuleInfoImpl.getInstance().getName());
94 requiredSources = deviceRequiredSources.stream()
95 .map(qname -> new SourceIdentifier(qname.getLocalName(), qname.getRevision().orElse(null)))
96 .collect(Collectors.toList());
98 final var missingSources = filterMissingSources(requiredSources);
99 addUnresolvedCapabilities(getQNameFromSourceIdentifiers(missingSources),
100 UnavailableCapability.FailureReason.MissingSource);
101 requiredSources.removeAll(missingSources);
104 ListenableFuture<DeviceNetconfSchema> startResolution() {
110 public void onSuccess(final EffectiveModelContext result) {
111 LOG.debug("{}: Schema context built successfully from {}", deviceId, requiredSources);
113 final var filteredQNames = Sets.difference(deviceRequiredSources, unresolvedCapabilites.keySet());
114 resolvedCapabilities.addAll(filteredQNames.stream()
115 .map(capability -> new AvailableCapabilityBuilder()
116 .setCapability(capability.toString())
117 .setCapabilityOrigin(sessionPreferences.capabilityOrigin(capability))
119 .collect(Collectors.toList()));
121 nonModuleBasedCapabilities.addAll(sessionPreferences.nonModuleCaps().keySet().stream()
122 .map(capability -> new AvailableCapabilityBuilder()
123 .setCapability(capability)
124 .setCapabilityOrigin(sessionPreferences.capabilityOrigin(capability))
126 .collect(Collectors.toList()));
128 resultFuture.set(new DeviceNetconfSchema(new NetconfDeviceCapabilities(
129 ImmutableMap.copyOf(unresolvedCapabilites), ImmutableSet.copyOf(resolvedCapabilities),
130 ImmutableSet.copyOf(nonModuleBasedCapabilities)), result));
134 public void onFailure(final Throwable cause) {
135 // schemaBuilderFuture.checkedGet() throws only SchemaResolutionException
136 // that might be wrapping a MissingSchemaSourceException so we need to look
137 // at the cause of the exception to make sure we don't misinterpret it.
138 if (cause instanceof MissingSchemaSourceException) {
139 requiredSources = handleMissingSchemaSourceException((MissingSchemaSourceException) cause);
140 } else if (cause instanceof SchemaResolutionException) {
141 requiredSources = handleSchemaResolutionException((SchemaResolutionException) cause);
143 LOG.debug("Unhandled failure", cause);
144 resultFuture.setException(cause);
152 private void trySetupSchema() {
153 if (!requiredSources.isEmpty()) {
154 // Initiate async resolution, drive it back based on the result
155 LOG.trace("{}: Trying to build schema context from {}", deviceId, requiredSources);
156 Futures.addCallback(contextFactory.createEffectiveModelContext(requiredSources), this,
157 MoreExecutors.directExecutor());
159 LOG.debug("{}: no more sources for schema context", deviceId);
160 resultFuture.setException(
161 new EmptySchemaContextException(deviceId + ": No more sources for schema context"));
165 private List<SourceIdentifier> filterMissingSources(final Collection<SourceIdentifier> origSources) {
166 return origSources.parallelStream()
167 .filter(sourceId -> {
169 repository.getSchemaSource(sourceId, YangTextSource.class).get();
171 } catch (InterruptedException | ExecutionException e) {
172 LOG.debug("Failed to acquire source {}", sourceId, e);
176 .collect(Collectors.toList());
179 private void addUnresolvedCapabilities(final Collection<QName> capabilities, final FailureReason reason) {
180 for (QName s : capabilities) {
181 unresolvedCapabilites.put(s, reason);
185 private List<SourceIdentifier> handleMissingSchemaSourceException(
186 final MissingSchemaSourceException exception) {
187 // In case source missing, try without it
188 final SourceIdentifier missingSource = exception.sourceId();
189 LOG.warn("{}: Unable to build schema context, missing source {}, will reattempt without it",
190 deviceId, missingSource);
191 LOG.debug("{}: Unable to build schema context, missing source {}, will reattempt without it",
192 deviceId, missingSource, exception);
193 final var qNameOfMissingSource = getQNameFromSourceIdentifiers(Sets.newHashSet(missingSource));
194 if (!qNameOfMissingSource.isEmpty()) {
195 addUnresolvedCapabilities(qNameOfMissingSource, UnavailableCapability.FailureReason.MissingSource);
197 return stripUnavailableSource(missingSource);
200 private Collection<SourceIdentifier> handleSchemaResolutionException(
201 final SchemaResolutionException resolutionException) {
202 // In case resolution error, try only with resolved sources
203 // There are two options why schema resolution exception occurred : unsatisfied imports or flawed model
204 // FIXME Do we really have assurance that these two cases cannot happen at once?
205 final var failedSourceId = resolutionException.sourceId();
206 if (failedSourceId != null) {
207 // flawed model - exclude it
208 LOG.warn("{}: Unable to build schema context, failed to resolve source {}, will reattempt without it",
209 deviceId, failedSourceId);
210 LOG.warn("{}: Unable to build schema context, failed to resolve source {}, will reattempt without it",
211 deviceId, failedSourceId, resolutionException);
212 addUnresolvedCapabilities(getQNameFromSourceIdentifiers(List.of(failedSourceId)),
213 UnavailableCapability.FailureReason.UnableToResolve);
214 return stripUnavailableSource(failedSourceId);
216 // unsatisfied imports
217 addUnresolvedCapabilities(
218 getQNameFromSourceIdentifiers(resolutionException.getUnsatisfiedImports().keySet()),
219 UnavailableCapability.FailureReason.UnableToResolve);
220 LOG.warn("{}: Unable to build schema context, unsatisfied imports {}, will reattempt with resolved only",
221 deviceId, resolutionException.getUnsatisfiedImports());
222 LOG.debug("{}: Unable to build schema context, unsatisfied imports {}, will reattempt with resolved only",
223 deviceId, resolutionException.getUnsatisfiedImports(), resolutionException);
224 return resolutionException.getResolvedSources();
227 private List<SourceIdentifier> stripUnavailableSource(final SourceIdentifier sourceIdToRemove) {
228 final var tmp = new ArrayList<>(requiredSources);
229 checkState(tmp.remove(sourceIdToRemove), "%s: Trying to remove %s from %s failed", deviceId, sourceIdToRemove,
234 private Collection<QName> getQNameFromSourceIdentifiers(final Collection<SourceIdentifier> identifiers) {
235 final Collection<QName> qNames = Collections2.transform(identifiers, this::getQNameFromSourceIdentifier);
237 if (qNames.isEmpty()) {
238 LOG.debug("{}: Unable to map any source identifiers to a capability reported by device : {}", deviceId,
241 return Collections2.filter(qNames, Predicates.notNull());
244 private QName getQNameFromSourceIdentifier(final SourceIdentifier identifier) {
245 // Required sources are all required and provided merged in DeviceSourcesResolver
246 for (final QName qname : deviceRequiredSources) {
247 if (!qname.getLocalName().equals(identifier.name().getLocalName())) {
251 if (Objects.equals(identifier.revision(), qname.getRevision().orElse(null))) {
255 LOG.warn("Unable to map identifier to a devices reported capability: {} Available: {}", identifier,
256 deviceRequiredSources);
257 // return null since we cannot find the QName,
258 // this capability will be removed from required sources and not reported as unresolved-capability