Refactor NetconfDeviceSchemas
[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.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;
54
55 /**
56  * Schema builder that tries to build schema context from provided sources or biggest subset of it.
57  */
58 final class SchemaSetup implements FutureCallback<EffectiveModelContext> {
59     private static final Logger LOG = LoggerFactory.getLogger(SchemaSetup.class);
60
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;
71
72     private Collection<SourceIdentifier> requiredSources;
73
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);
82
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());
92         }
93
94         requiredSources = deviceRequiredSources.stream()
95             .map(qname -> new SourceIdentifier(qname.getLocalName(), qname.getRevision().orElse(null)))
96             .collect(Collectors.toList());
97
98         final var missingSources = filterMissingSources(requiredSources);
99         addUnresolvedCapabilities(getQNameFromSourceIdentifiers(missingSources),
100             UnavailableCapability.FailureReason.MissingSource);
101         requiredSources.removeAll(missingSources);
102     }
103
104     ListenableFuture<DeviceNetconfSchema> startResolution() {
105         trySetupSchema();
106         return resultFuture;
107     }
108
109     @Override
110     public void onSuccess(final EffectiveModelContext result) {
111         LOG.debug("{}: Schema context built successfully from {}", deviceId, requiredSources);
112
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))
118                 .build())
119             .collect(Collectors.toList()));
120
121         nonModuleBasedCapabilities.addAll(sessionPreferences.nonModuleCaps().keySet().stream()
122             .map(capability -> new AvailableCapabilityBuilder()
123                 .setCapability(capability)
124                 .setCapabilityOrigin(sessionPreferences.capabilityOrigin(capability))
125                 .build())
126             .collect(Collectors.toList()));
127
128         resultFuture.set(new DeviceNetconfSchema(new NetconfDeviceCapabilities(
129             ImmutableMap.copyOf(unresolvedCapabilites), ImmutableSet.copyOf(resolvedCapabilities),
130             ImmutableSet.copyOf(nonModuleBasedCapabilities)), result));
131     }
132
133     @Override
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);
142         } else {
143             LOG.debug("Unhandled failure", cause);
144             resultFuture.setException(cause);
145             // No more trying...
146             return;
147         }
148
149         trySetupSchema();
150     }
151
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());
158         } else {
159             LOG.debug("{}: no more sources for schema context", deviceId);
160             resultFuture.setException(
161                 new EmptySchemaContextException(deviceId + ": No more sources for schema context"));
162         }
163     }
164
165     private List<SourceIdentifier> filterMissingSources(final Collection<SourceIdentifier> origSources) {
166         return origSources.parallelStream()
167             .filter(sourceId -> {
168                 try {
169                     repository.getSchemaSource(sourceId, YangTextSource.class).get();
170                     return false;
171                 } catch (InterruptedException | ExecutionException e) {
172                     LOG.debug("Failed to acquire source {}", sourceId, e);
173                     return true;
174                 }
175             })
176             .collect(Collectors.toList());
177     }
178
179     private void addUnresolvedCapabilities(final Collection<QName> capabilities, final FailureReason reason) {
180         for (QName s : capabilities) {
181             unresolvedCapabilites.put(s, reason);
182         }
183     }
184
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);
196         }
197         return stripUnavailableSource(missingSource);
198     }
199
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);
215         }
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();
225     }
226
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,
230             requiredSources);
231         return tmp;
232     }
233
234     private Collection<QName> getQNameFromSourceIdentifiers(final Collection<SourceIdentifier> identifiers) {
235         final Collection<QName> qNames = Collections2.transform(identifiers, this::getQNameFromSourceIdentifier);
236
237         if (qNames.isEmpty()) {
238             LOG.debug("{}: Unable to map any source identifiers to a capability reported by device : {}", deviceId,
239                     identifiers);
240         }
241         return Collections2.filter(qNames, Predicates.notNull());
242     }
243
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())) {
248                 continue;
249             }
250
251             if (Objects.equals(identifier.revision(), qname.getRevision().orElse(null))) {
252                 return qname;
253             }
254         }
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
259         return null;
260     }
261 }