[VVP] Support pluggable data sources for preload data
[vvp/validation-scripts.git] / ice_validator / preload / generator.py
1 # -*- coding: utf8 -*-
2 # ============LICENSE_START====================================================
3 # org.onap.vvp/validation-scripts
4 # ===================================================================
5 # Copyright © 2019 AT&T Intellectual Property. All rights reserved.
6 # ===================================================================
7 #
8 # Unless otherwise specified, all software contained herein is licensed
9 # under the Apache License, Version 2.0 (the "License");
10 # you may not use this software except in compliance with the License.
11 # You may obtain a copy of the License at
12 #
13 #             http://www.apache.org/licenses/LICENSE-2.0
14 #
15 # Unless required by applicable law or agreed to in writing, software
16 # distributed under the License is distributed on an "AS IS" BASIS,
17 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 # See the License for the specific language governing permissions and
19 # limitations under the License.
20 #
21 #
22 #
23 # Unless otherwise specified, all documentation contained herein is licensed
24 # under the Creative Commons License, Attribution 4.0 Intl. (the "License");
25 # you may not use this documentation except in compliance with the License.
26 # You may obtain a copy of the License at
27 #
28 #             https://creativecommons.org/licenses/by/4.0/
29 #
30 # Unless required by applicable law or agreed to in writing, documentation
31 # distributed under the License is distributed on an "AS IS" BASIS,
32 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33 # See the License for the specific language governing permissions and
34 # limitations under the License.
35 #
36 # ============LICENSE_END============================================
37
38 import json
39 import os
40 from abc import ABC, abstractmethod
41 from collections import OrderedDict
42 from pathlib import Path
43
44 import yaml
45
46 from preload.data import (
47     AbstractPreloadDataSource,
48     AbstractPreloadInstance,
49     BlankPreloadInstance,
50 )
51 from preload.model import VnfModule, Vnf
52
53
54 def represent_ordered_dict(dumper, data):
55     value = []
56
57     for item_key, item_value in data.items():
58         node_key = dumper.represent_data(item_key)
59         node_value = dumper.represent_data(item_value)
60
61         value.append((node_key, node_value))
62
63     return yaml.nodes.MappingNode(u"tag:yaml.org,2002:map", value)
64
65
66 def get_json_template(template_dir, template_name):
67     template_name = template_name + ".json"
68     with open(os.path.join(template_dir, template_name)) as f:
69         return json.loads(f.read())
70
71
72 def get_or_create_template(template_dir, key, value, sequence, template_name):
73     """
74     Search a sequence of dicts where a given key matches value.  If
75     found, then it returns that item.  If not, then it loads the
76     template identified by template_name, adds it ot the sequence, and
77     returns the template
78     """
79     for item in sequence:
80         if item[key] == value:
81             return item
82     new_template = get_json_template(template_dir, template_name)
83     sequence.append(new_template)
84     return new_template
85
86
87 def replace(param, index=None):
88     """
89     Optionally used by the preload generator to wrap items in the preload
90     that need to be replaced by end users
91     :param param: parameter name
92     :param index: optional index (int or str) of the parameter
93     """
94     if (param.endswith("_names") or param.endswith("_ips")) and index is not None:
95         param = "{}[{}]".format(param, index)
96     return "VALUE FOR: {}".format(param) if param else ""
97
98
99 class AbstractPreloadGenerator(ABC):
100     """
101     All preload generators must inherit from this class and implement the
102     abstract methods.
103
104     Preload generators are automatically discovered at runtime via a plugin
105     architecture.  The system path is scanned looking for modules with the name
106     preload_*, then all non-abstract classes that inherit from AbstractPreloadGenerator
107     are registered as preload plugins
108
109     Attributes:
110         :param vnf:             Instance of Vnf that contains the preload data
111         :param base_output_dir: Base directory to house the preloads.  All preloads
112                                 must be written to a subdirectory under this directory
113         :param data_source:     Source data for preload population
114     """
115
116     def __init__(
117         self, vnf: Vnf, base_output_dir: Path, data_source: AbstractPreloadDataSource
118     ):
119         self.data_source = data_source
120         self.vnf = vnf
121         self.base_output_dir = base_output_dir
122         self.module_incomplete = False
123
124     @classmethod
125     @abstractmethod
126     def format_name(cls):
127         """
128         String name to identify the format (ex: VN-API, GR-API)
129         """
130         raise NotImplementedError()
131
132     @classmethod
133     @abstractmethod
134     def output_sub_dir(cls):
135         """
136         String sub-directory name that will appear under ``base_output_dir``
137         """
138         raise NotImplementedError()
139
140     @classmethod
141     @abstractmethod
142     def supports_output_passing(cls):
143         """
144         Some preload methods allow automatically mapping output parameters in the
145         base module to the input parameter of other modules.  This means these
146         that the incremental modules do not need these base module outputs in their
147         preloads.
148
149         At this time, VNF-API does not support output parameter passing, but
150         GR-API does.
151
152         If this is true, then the generator will call Vnf#filter_output_params
153         after the preload module for the base module has been created
154         """
155         raise NotImplementedError()
156
157     @abstractmethod
158     def generate_module(self, module: VnfModule, preload: AbstractPreloadInstance, output_dir: Path):
159         """
160         Create the preloads.  This method is responsible for generating the
161         content of the preload and writing the file to disk.
162         """
163         raise NotImplementedError()
164
165     def generate(self):
166         # handle the base module first
167         print("\nGenerating {} preloads".format(self.format_name()))
168         if self.vnf.base_module:
169             self.generate_preloads(self.vnf.base_module)
170         if self.supports_output_passing():
171             self.vnf.filter_base_outputs()
172         for mod in self.vnf.incremental_modules:
173             self.generate_preloads(mod)
174
175     def start_module(self):
176         """Initialize/reset the environment for the module"""
177         self.module_incomplete = False
178
179     def generate_preloads(self, module):
180         """
181         Generate a preload for the given module in all available environments
182         in the ``self.preload_env``.  This will invoke the abstract
183         generate_module once for each available environment **and** an
184         empty environment to create a blank template.
185
186         :param module:  module to generate for
187         """
188         print("\nGenerating Preloads for {}".format(module))
189         print("-" * 50)
190         print("... generating blank template")
191         self.start_module()
192         preload = BlankPreloadInstance(Path(self.base_output_dir), module.label)
193         blank_preload_dir = self.make_preload_dir(preload)
194         self.generate_module(module, preload, blank_preload_dir)
195         self.generate_preload_env(module, preload)
196
197         if self.data_source:
198             preloads = self.data_source.get_module_preloads(module)
199             for preload in preloads:
200                 output_dir = self.make_preload_dir(preload)
201                 print(
202                     "... generating preload for {} to {}".format(
203                         preload.module_label, output_dir
204                     )
205                 )
206                 self.start_module()
207                 self.generate_module(module, preload, output_dir)
208
209     def make_preload_dir(self, preload: AbstractPreloadInstance):
210         preload_dir = preload.output_dir.joinpath(self.output_sub_dir())
211         preload_dir.mkdir(parents=True, exist_ok=True)
212         return preload_dir
213
214     @staticmethod
215     def generate_preload_env(module: VnfModule, preload: AbstractPreloadInstance):
216         """
217         Create a .env template suitable for completing and using for
218         preload generation from env files.
219         """
220         yaml.add_representer(OrderedDict, represent_ordered_dict)
221         output_dir = preload.output_dir.joinpath("preload_env")
222         env_file = output_dir.joinpath("{}.env".format(module.label))
223         defaults_file = output_dir.joinpath("defaults.yaml")
224         output_dir.mkdir(parents=True, exist_ok=True)
225         with env_file.open("w") as f:
226             yaml.dump(module.env_template, f)
227         if not defaults_file.exists():
228             with defaults_file.open("w") as f:
229                 yaml.dump({"vnf_name": "CHANGEME"}, f)
230
231     def normalize(self, preload_value, param_name, alt_message=None, index=None):
232         preload_value = None if preload_value == "CHANGEME" else preload_value
233         if preload_value:
234             return preload_value
235         else:
236             self.module_incomplete = True
237             return alt_message or replace(param_name, index)