Eliminate SchemaResourcesDTO
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / impl / SchemaSetup.java
1 /*
2  * Copyright (c) 2024 PANTHEON.tech, s.r.o. 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.impl;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static java.util.Objects.requireNonNull;
12
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;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Set;
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;
53
54 /**
55  * Schema builder that tries to build schema context from provided sources or biggest subset of it.
56  */
57 final class SchemaSetup implements FutureCallback<EffectiveModelContext> {
58     private static final Logger LOG = LoggerFactory.getLogger(SchemaSetup.class);
59
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<>();
64
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;
70
71     private Collection<SourceIdentifier> requiredSources;
72
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);
81
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())
90             );
91         }
92
93         requiredSources = deviceSources.getRequiredSources();
94         final var missingSources = filterMissingSources(requiredSources);
95
96         addUnresolvedCapabilities(getQNameFromSourceIdentifiers(missingSources),
97             UnavailableCapability.FailureReason.MissingSource);
98         requiredSources.removeAll(missingSources);
99     }
100
101     ListenableFuture<DeviceNetconfSchema> startResolution() {
102         trySetupSchema();
103         return resultFuture;
104     }
105
106     @Override
107     public void onSuccess(final EffectiveModelContext result) {
108         LOG.debug("{}: Schema context built successfully from {}", deviceId, requiredSources);
109
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))
116                 .build())
117             .collect(Collectors.toList()));
118
119         nonModuleBasedCapabilities.addAll(remoteSessionCapabilities.nonModuleCaps().keySet().stream()
120             .map(capability -> new AvailableCapabilityBuilder()
121                 .setCapability(capability)
122                 .setCapabilityOrigin(remoteSessionCapabilities.capabilityOrigin(capability))
123                 .build())
124             .collect(Collectors.toList()));
125
126
127         resultFuture.set(new DeviceNetconfSchema(new NetconfDeviceCapabilities(
128             ImmutableMap.copyOf(unresolvedCapabilites), ImmutableSet.copyOf(resolvedCapabilities),
129             ImmutableSet.copyOf(nonModuleBasedCapabilities)), result));
130     }
131
132     @Override
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);
141         } else {
142             LOG.debug("Unhandled failure", cause);
143             resultFuture.setException(cause);
144             // No more trying...
145             return;
146         }
147
148         trySetupSchema();
149     }
150
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());
157         } else {
158             LOG.debug("{}: no more sources for schema context", deviceId);
159             resultFuture.setException(
160                 new EmptySchemaContextException(deviceId + ": No more sources for schema context"));
161         }
162     }
163
164     private List<SourceIdentifier> filterMissingSources(final Collection<SourceIdentifier> origSources) {
165         return origSources.parallelStream()
166             .filter(sourceId -> {
167                 try {
168                     repository.getSchemaSource(sourceId, YangTextSource.class).get();
169                     return false;
170                 } catch (InterruptedException | ExecutionException e) {
171                     LOG.debug("Failed to acquire source {}", sourceId, e);
172                     return true;
173                 }
174             })
175             .collect(Collectors.toList());
176     }
177
178     private void addUnresolvedCapabilities(final Collection<QName> capabilities, final FailureReason reason) {
179         for (QName s : capabilities) {
180             unresolvedCapabilites.put(s, reason);
181         }
182     }
183
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);
195         }
196         return stripUnavailableSource(missingSource);
197     }
198
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);
214         }
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();
224     }
225
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,
229             requiredSources);
230         return tmp;
231     }
232
233     private Collection<QName> getQNameFromSourceIdentifiers(final Collection<SourceIdentifier> identifiers) {
234         final Collection<QName> qNames = Collections2.transform(identifiers, this::getQNameFromSourceIdentifier);
235
236         if (qNames.isEmpty()) {
237             LOG.debug("{}: Unable to map any source identifiers to a capability reported by device : {}", deviceId,
238                     identifiers);
239         }
240         return Collections2.filter(qNames, Predicates.notNull());
241     }
242
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())) {
247                 continue;
248             }
249
250             if (Objects.equals(identifier.revision(), qname.getRevision().orElse(null))) {
251                 return qname;
252             }
253         }
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
258         return null;
259     }
260 }