55095da78c85fe1f1778a3014d767991006f2286
[smo/teiv.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2024 Ericsson
4  *  Modifications Copyright (C) 2024 OpenInfra Foundation Europe
5  *  ================================================================================
6  *  Licensed under the Apache License, Version 2.0 (the "License");
7  *  you may not use this file except in compliance with the License.
8  *  You may obtain a copy of the License at
9  *
10  *        http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  *
18  *  SPDX-License-Identifier: Apache-2.0
19  *  ============LICENSE_END=========================================================
20  */
21 package org.oran.smo.yangtools.parser.yanglibrary;
22
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Objects;
31 import java.util.Set;
32 import java.util.stream.Collectors;
33
34 import org.oran.smo.yangtools.parser.ParserExecutionContext;
35 import org.oran.smo.yangtools.parser.data.YangData;
36 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot;
37 import org.oran.smo.yangtools.parser.data.dom.YangDataDomDocumentRoot.SourceDataType;
38 import org.oran.smo.yangtools.parser.data.dom.YangDataDomNode;
39 import org.oran.smo.yangtools.parser.data.util.IdentityRefValue;
40 import org.oran.smo.yangtools.parser.findings.Finding;
41 import org.oran.smo.yangtools.parser.findings.FindingsManager;
42 import org.oran.smo.yangtools.parser.findings.ModifyableFindingSeverityCalculator;
43 import org.oran.smo.yangtools.parser.findings.ParserFindingType;
44 import org.oran.smo.yangtools.parser.input.YangInputResolver;
45 import org.oran.smo.yangtools.parser.model.schema.ModuleAndNamespaceResolver;
46 import org.oran.smo.yangtools.parser.yanglibrary.Module.IetfYangLibraryConformanceType;
47
48 /**
49  * Parser for Yang Library instance data. Can handle both the "yang-library" and "modules-state" containers.
50  *
51  * @author Mark Hollmann
52  */
53 public class IetfYangLibraryParser {
54
55     /**
56      * The module name for the yang-library module as defined in RFC 8525.
57      */
58     public static final String IETF_YANG_LIBRARY_MODULE_NAME = "ietf-yang-library";
59     /**
60      * The module namespace for the yang-library module as defined in RFC 8525.
61      */
62     public static final String IETF_YANG_LIBRARY_NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-yang-library";
63
64     public static final String YANG_LIBRARY_MODULES_STATE = "modules-state";
65     public static final String YANG_LIBRARY_YANG_LIBRARY = "yang-library";
66
67     private final List<YangLibraryPopulator> populators;
68     private final FindingsManager findingsManager;
69     private final ParserExecutionContext context;
70
71     private static final ModuleAndNamespaceResolver RESOLVER = new ModuleAndNamespaceResolver();
72
73     static {
74         RESOLVER.recordModuleMapping(IETF_YANG_LIBRARY_MODULE_NAME, IETF_YANG_LIBRARY_NAMESPACE);
75         RESOLVER.recordNamespaceMapping(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME);
76
77         RESOLVER.recordModuleMapping(Datastore.IETF_DATASTORES_MODULE_NAME, Datastore.IETF_DATASTORES_NAMESPACE);
78         RESOLVER.recordNamespaceMapping(Datastore.IETF_DATASTORES_NAMESPACE, Datastore.IETF_DATASTORES_MODULE_NAME);
79     }
80
81     /**
82      * Creates a Yang Library parser using the default populator for RFC8525.
83      */
84     public IetfYangLibraryParser() {
85         this(Collections.singletonList(new RFC8525Populator()));
86     }
87
88     /**
89      * Creates a YL parser using the supplied populators. The client should make sure to supply a
90      * populator for extraction of core Yang Library data according to RFC8525.
91      */
92     public IetfYangLibraryParser(final List<YangLibraryPopulator> populators) {
93         this.populators = populators;
94
95         findingsManager = new FindingsManager(new ModifyableFindingSeverityCalculator());
96         context = new ParserExecutionContext(findingsManager);
97
98         context.setCheckModulesAgainstYangLibrary(false);
99     }
100
101     /**
102      * Returns the FindingsManager used during parsing. Issues encountered during parsing will be
103      * captured as Findings.
104      */
105     public FindingsManager getFindingsManager() {
106         return findingsManager;
107     }
108
109     /**
110      * Parses instance data into Yang Library instances. The supplied resolver must resolve to one or more
111      * data inputs (typically files containing XML or JSON data). All Yang Library instances encountered
112      * in the data will be extracted, not just the instance found at the top-level (ie., mount points are
113      * also handled).
114      * <p/>
115      * Yang library instance data using the (deprecated) "modules-state" will be converted to a corresponding
116      * representation of the "yang-library".
117      * <p/>
118      * Issues may be encountered during the parsing; after parsing, the findings manager should be inspected
119      * for any issues.
120      */
121     public List<YangLibrary> parseIntoYangLibraries(final YangInputResolver instanceDataInputResolver) {
122
123         findingsManager.clear();
124
125         final List<YangData> yangDatas = instanceDataInputResolver.getResolvedYangInput().stream().map(YangData::new)
126                 .collect(Collectors.toList());
127
128         /*
129          * Parse in all of the data. The worst that can happen is that the XML or JSON is malformed, or that
130          * prefixes cannot be resolved.
131          */
132         for (final YangData yangData : yangDatas) {
133             yangData.parse(context);
134         }
135
136         if (!findingsManager.getAllFindings().isEmpty()) {
137             findingsManager.addFinding(new Finding(ParserFindingType.P000_UNSPECIFIED_ERROR,
138                     "There were findings during parsing of the YANG instance data that may have resulted in the YANG Library data not being populated correctly."));
139         }
140
141         /*
142          * We look at each input and try to find the relevant containers.
143          */
144         final List<YangLibrary> result = new ArrayList<>();
145
146         for (final YangData yangData : yangDatas) {
147             final YangDataDomDocumentRoot yangDataDomDocumentRoot = yangData.getYangDataDomDocumentRoot();
148             if (yangDataDomDocumentRoot != null) {
149                 extractYangLibraryUnderDomNode(yangDataDomDocumentRoot, result);
150             }
151         }
152
153         return result;
154     }
155
156     /**
157      * Works like {@link parseIntoYangLibraries}, but uses the supplied data DOm as input of Yang Library data, and will
158      * only extract the top-level Yang Library instance (if any).
159      */
160     public static YangLibrary getTopLevelYangLibrary(final YangDataDomDocumentRoot dataDomDocumentRoot) {
161
162         final IetfYangLibraryParser yangLibraryParser = new IetfYangLibraryParser();
163         final List<YangLibrary> extractedYangLibraries = new ArrayList<>();
164         yangLibraryParser.extractYangLibraryUnderDomNode(dataDomDocumentRoot, extractedYangLibraries);
165
166         return YangLibrary.getTopLevelSchema(extractedYangLibraries);
167     }
168
169     /**
170      * Given a data DOM node (possibly being document root), tries to extract a YANG Library under the DOM node.
171      */
172     private void extractYangLibraryUnderDomNode(final YangDataDomNode domNode, List<YangLibrary> result) {
173
174         final ModulesState modulesStateInstance = createModulesStateBranch(domNode);
175         YangLibrary yangLibraryInstance = createYangLibraryBranch(domNode);
176
177         /*
178          * The modules-state is deprecated, so we translate to yang-library if needed. This
179          * allows a client to always operate on the yang-library without having to worry
180          * about the deprecated modules-state branch.
181          *
182          * Note we will never translate back from yang-library to modules-state, as this
183          * would require us to figure out which is the correct set of modules (looking
184          * at datastore and schema elements), which can be ambiguous - plus it is
185          * deprecated...
186          */
187         if (modulesStateInstance != null && yangLibraryInstance == null) {
188             yangLibraryInstance = translateModulesStateDataToYangLibraryData(modulesStateInstance);
189         }
190
191         if (yangLibraryInstance != null) {
192             result.add(yangLibraryInstance);
193         }
194
195         /*
196          * Recursively work down the DOM tree - the YANG library can conceivably be under a mount point
197          * further down the tree.
198          */
199         domNode.getChildren().forEach(child -> extractYangLibraryUnderDomNode(child, result));
200     }
201
202     private static YangLibrary translateModulesStateDataToYangLibraryData(final ModulesState modulesState) {
203
204         final YangLibrary yangLibraryInstance = new YangLibrary(modulesState.getMountPoint());
205         yangLibraryInstance.setContentId(modulesState.getModuleSetId());
206
207         final ModuleSet defaultModuleSet = new ModuleSet();
208         defaultModuleSet.setName("module-set-auto-generated-by-yang-library-parser");
209
210         for (final Module module : modulesState.getModules()) {
211             if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPLEMENT) {
212                 defaultModuleSet.addImplementingModule(module);
213             } else if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPORT) {
214                 defaultModuleSet.addImportOnlyModule(module);
215             }
216         }
217
218         yangLibraryInstance.addDatastore(new Datastore(Datastore.RUNNING_DATASTORE_IDENTITY,
219                 "schema-auto-generated-by-yang-library-parser", Collections.singletonList(defaultModuleSet)));
220
221         return yangLibraryInstance;
222     }
223
224     // ============================= Modules state branch ========================================
225
226     private ModulesState createModulesStateBranch(final YangDataDomNode parentDomNode) {
227
228         final YangDataDomNode modulesStateDomNode = getDomChild(parentDomNode, YANG_LIBRARY_MODULES_STATE);
229
230         if (modulesStateDomNode != null) {
231             final ModulesState modulesState = new ModulesState(parentDomNode);
232             populateModulesState(modulesState, modulesStateDomNode);
233
234             /*
235              * The YANG model for RFC 7895 (modules-state) allows for semantic errors - for example, the
236              * same module, of different revisions, could be marked as IMPLEMENTING. This is likely to be
237              * causing problems for the client later on, so we do some validation here to try to catch
238              * these issues as early as possible.
239              */
240             validateModulesState(modulesState);
241
242             return modulesState;
243         }
244
245         return null;
246     }
247
248     private void populateModulesState(final ModulesState modulesState, final YangDataDomNode modulesStateDomNode) {
249
250         populators.forEach(pop -> pop.populateModulesState(context, modulesState, modulesStateDomNode));
251
252         for (final YangDataDomNode moduleChildDomNode : getDomChildren(modulesStateDomNode, "module")) {
253             final Module module = new Module();
254             modulesState.addModule(module);
255             populateModuleInModulesState(module, moduleChildDomNode);
256         }
257     }
258
259     private void populateModuleInModulesState(final Module module, final YangDataDomNode moduleDomNode) {
260
261         populators.forEach(pop -> pop.populateModuleInModulesState(context, module, moduleDomNode));
262
263         for (final YangDataDomNode submoduleDomNode : getDomChildren(moduleDomNode, "submodule")) {
264             final Submodule submodule = new Submodule();
265             module.addSubmodule(submodule);
266             populators.forEach(pop -> pop.populateSubmoduleInModulesState(context, submodule, submoduleDomNode));
267         }
268     }
269
270     private void validateModulesState(final ModulesState modulesState) {
271         validateModulesStateForConformanceType(modulesState);
272     }
273
274     private void validateModulesStateForConformanceType(final ModulesState modulesState) {
275         /*
276          * Couple basic rules:
277          *
278          * A correct enum value must be used for 'conformance-type'.
279          * The same module (by name) cannot have 'conformance-type implement'.
280          * There must be at least a single 'conformance-type implement'.
281          */
282         final Set<String> implementingModuleNames = new HashSet<>();
283         for (final Module module : modulesState.getModules()) {
284             if (module.getConformanceType() == IetfYangLibraryConformanceType.UNKNOWN) {
285                 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
286                         "Unknown conformance-type for module '" + module.getName() + "'."));
287             } else if (module.getConformanceType() == IetfYangLibraryConformanceType.IMPLEMENT) {
288                 if (implementingModuleNames.contains(module.getName())) {
289                     context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
290                             "Module '" + module.getName() + "' listed more than once with conformance-type 'implement'."));
291                 } else {
292                     implementingModuleNames.add(module.getName());
293                 }
294             }
295         }
296
297         if (implementingModuleNames.isEmpty()) {
298             context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
299                     "YANG library does not have at least a single module entry with conformance-type 'implement'."));
300         }
301     }
302
303     // ============================ YANG Library branch ======================================
304
305     private YangLibrary createYangLibraryBranch(final YangDataDomNode parentDomNode) {
306
307         try {
308             final YangDataDomNode yangLibraryDomNode = getDomChild(parentDomNode, YANG_LIBRARY_YANG_LIBRARY);
309             if (yangLibraryDomNode != null) {
310                 final YangLibrary yangLibrary = new YangLibrary(parentDomNode);
311                 populateYangLibrary(yangLibrary, yangLibraryDomNode);
312                 return yangLibrary;
313             }
314         } catch (final Exception ex) {
315             context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
316                     "Incorrect YANG library."));
317         }
318
319         return null;
320     }
321
322     private void populateYangLibrary(final YangLibrary yangLibrary, final YangDataDomNode yangLibraryDomNode) {
323
324         populators.forEach(pop -> pop.populateYangLibrary(context, yangLibrary, yangLibraryDomNode));
325
326         final Map<String, ModuleSet> extractedModuleSets = new HashMap<>();
327
328         for (final YangDataDomNode moduleSetDomNode : getDomChildren(yangLibraryDomNode, "module-set")) {
329             final ModuleSet moduleSet = new ModuleSet();
330             populateModuleSet(moduleSet, moduleSetDomNode);
331             extractedModuleSets.put(moduleSet.getName(), moduleSet);
332         }
333
334         final Map<String, List<ModuleSet>> schemaToModuleSetMappings = getSchemaToModuleSetMappings(context,
335                 extractedModuleSets, yangLibraryDomNode);
336
337         handleDatastoreElements(yangLibrary, schemaToModuleSetMappings, yangLibraryDomNode);
338     }
339
340     private void populateModuleSet(final ModuleSet moduleSet, final YangDataDomNode moduleSetDomNode) {
341
342         populators.forEach(pop -> pop.populateModuleSet(context, moduleSet, moduleSetDomNode));
343
344         for (final YangDataDomNode moduleDomNode : getDomChildren(moduleSetDomNode, "module")) {
345             final Module module = new Module();
346             moduleSet.addImplementingModule(module);
347             populateModuleInYangLibrary(module, moduleDomNode, Module.MODULE_IMPLEMENT);
348         }
349         for (final YangDataDomNode moduleDomNode : getDomChildren(moduleSetDomNode, "import-only-module")) {
350             final Module module = new Module();
351             moduleSet.addImportOnlyModule(module);
352             populateModuleInYangLibrary(module, moduleDomNode, Module.MODULE_IMPORT);
353         }
354     }
355
356     private void populateModuleInYangLibrary(final Module module, final YangDataDomNode moduleDomNode,
357             final String conformanceType) {
358
359         populators.forEach(pop -> pop.populateModuleInYangLibrary(context, module, moduleDomNode, conformanceType));
360
361         for (final YangDataDomNode submoduleDomNode : getDomChildren(moduleDomNode, "submodule")) {
362             final Submodule submodule = new Submodule();
363             module.addSubmodule(submodule);
364             populators.forEach(pop -> pop.populateSubmoduleInYangLibrary(context, submodule, submoduleDomNode));
365         }
366     }
367
368     /**
369      * Returns a mapping of schema name -to- list of module sets
370      */
371     private static Map<String, List<ModuleSet>> getSchemaToModuleSetMappings(final ParserExecutionContext context,
372             final Map<String, ModuleSet> extractedModuleSets, final YangDataDomNode yangLibraryDomNode) {
373
374         final Map<String, List<ModuleSet>> result = new HashMap<>();
375
376         for (final YangDataDomNode schemaDomNode : getDomChildren(yangLibraryDomNode, "schema")) {
377
378             final String schemaName = getValueOfChild(context, schemaDomNode, "name", "");
379             if (schemaName.isEmpty()) {
380                 context.getFindingsManager().addFinding(new Finding(
381                         ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING, "Missing schema name."));
382             }
383
384             result.put(schemaName, new ArrayList<>());
385
386             final List<String> moduleSetNames = getValueOfChildren(context, schemaDomNode, "module-set");
387
388             for (final String moduleSetName : moduleSetNames) {
389                 final ModuleSet moduleSet = extractedModuleSets.get(moduleSetName);
390                 if (moduleSet != null) {
391                     result.get(schemaName).add(moduleSet);
392                 } else {
393                     context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
394                             "schema '" + schemaName + "' refers to non-existing module-set '" + moduleSetName + "'."));
395                 }
396             }
397         }
398
399         /*
400          * Special handling - sometimes whoever creates the Yang Library data forgets to use the "schema"
401          * data node. We are nice here and simply auto-generate one, assigning all the module-sets found.
402          */
403
404         if (result.isEmpty()) {
405             result.put("schema-auto-generated-by-yang-library-parser", new ArrayList<>(extractedModuleSets.values()));
406         }
407
408         return result;
409     }
410
411     private void handleDatastoreElements(final YangLibrary yangLibrary,
412             final Map<String, List<ModuleSet>> schemaToModuleSetMappings, final YangDataDomNode yangLibraryDomNode) {
413
414         for (final YangDataDomNode datastoreDomNode : getDomChildren(yangLibraryDomNode, "datastore")) {
415
416             IdentityRefValue datastoreIdentityRef = null;
417
418             final YangDataDomNode datastoreNameDomNode = getDomChild(datastoreDomNode, "name");
419             if (datastoreNameDomNode != null) {
420
421                 final String datastoreIdentityRefAsString = Objects.toString(datastoreNameDomNode.getValue());
422
423                 /*
424                  * The value of this leaf is an identity ref, e.g.:
425                  *
426                  * <datastore xmlns:ds="urn:ietf:params:xml:ns:yang:ietf-datastores">
427                  *     <name>ds:running</name>
428                  *     <schema>schema1</schema>
429                  * </datastore>
430                  *
431                  * ...or, in JSON...
432                  *
433                  * "ietf-yang-library:datastore" : [
434                  *   {
435                  *     "name" : "ietf-datastores:running",
436                  *     "schema" : "schema1"
437                  *   }
438                  * ]
439                  */
440
441                 if (datastoreNameDomNode.getSourceDataType() == SourceDataType.XML) {
442                     datastoreIdentityRef = new IdentityRefValue(datastoreIdentityRefAsString, datastoreNameDomNode
443                             .getPrefixResolver(), datastoreNameDomNode.getNamespace());
444                 } else {
445                     datastoreIdentityRef = new IdentityRefValue(datastoreIdentityRefAsString, datastoreNameDomNode
446                             .getNamespace());
447                 }
448
449                 /*
450                  * If the namespace is unknown (maybe the designer forgot to declare the namespace),
451                  * we will assume the namespace of the "ietf-datastores" module.
452                  */
453                 if (datastoreIdentityRef.getIdentityNamespace() == null && datastoreIdentityRef
454                         .getIdentityModuleName() == null) {
455                     context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
456                             "Unresolvable/missing prefix/module for <datastore> name '" + datastoreIdentityRefAsString + "'."));
457                     datastoreIdentityRef = new IdentityRefValue(Datastore.IETF_DATASTORES_NAMESPACE,
458                             Datastore.IETF_DATASTORES_MODULE_NAME, datastoreIdentityRefAsString);
459                 }
460
461             } else {
462                 context.getFindingsManager().addFinding(new Finding(
463                         ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING, "Missing datastore name."));
464             }
465
466             final String schemaName = getValueOfChild(context, datastoreDomNode, "schema", "");
467             if (schemaName.isEmpty()) {
468                 context.getFindingsManager().addFinding(new Finding(
469                         ParserFindingType.P082_YANG_LIBRARY_MANDATORY_VALUE_MISSING,
470                         "Missing value for leaf 'schema' for datastore."));
471             }
472             if (!schemaToModuleSetMappings.containsKey(schemaName)) {
473                 context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
474                         "datastore refers to non-existing schema '" + schemaName + "'."));
475             } else {
476                 yangLibrary.addDatastore(new Datastore(datastoreIdentityRef, schemaName, schemaToModuleSetMappings.get(
477                         schemaName)));
478             }
479         }
480
481         /*
482          * Special handling: Designer did not specify a datastore in the YANG Library. If there is only a
483          * single schema we assume that this schema is the one to be used.
484          */
485         if (yangLibrary.getDatastores().isEmpty() && schemaToModuleSetMappings.size() == 1) {
486             final Entry<String, List<ModuleSet>> theOnlySchemaToModuleSetMappingEntry = schemaToModuleSetMappings.entrySet()
487                     .iterator().next();
488             yangLibrary.addDatastore(new Datastore(Datastore.RUNNING_DATASTORE_IDENTITY,
489                     theOnlySchemaToModuleSetMappingEntry.getKey(), theOnlySchemaToModuleSetMappingEntry.getValue()));
490         }
491     }
492
493     static YangDataDomNode getDomChild(final YangDataDomNode parentDomNode, final String soughtName) {
494         return parentDomNode.getChild(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME, soughtName);
495     }
496
497     static List<YangDataDomNode> getDomChildren(final YangDataDomNode parentDomNode, final String soughtName) {
498         return parentDomNode.getChildren(IETF_YANG_LIBRARY_NAMESPACE, IETF_YANG_LIBRARY_MODULE_NAME, soughtName);
499     }
500
501     static String getValueOfChild(final ParserExecutionContext context, final YangDataDomNode parentDomNode,
502             final String soughtName, final String defaultValue) {
503
504         final YangDataDomNode child = getDomChild(parentDomNode, soughtName);
505         final Object valueOfChild = child == null ? null : child.getValue();
506
507         if (valueOfChild != null && !(valueOfChild instanceof String)) {
508             context.getFindingsManager().addFinding(new Finding(ParserFindingType.P081_INCORRECT_YANG_LIBRARY_DATA,
509                     "Expected a string value for '" + soughtName + "'."));
510             return defaultValue;
511         }
512
513         return valueOfChild == null ? defaultValue : ((String) valueOfChild).trim();
514     }
515
516     static List<String> getValueOfChildren(final ParserExecutionContext context, final YangDataDomNode parentDomNode,
517             final String soughtName) {
518         return getDomChildren(parentDomNode, soughtName).stream().map(YangDataDomNode::getStringValue).collect(Collectors
519                 .toList());
520     }
521 }