6c0d4b88c2ed879f0b1ced90a3e1415e80a4ea21
[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
43 import yaml
44
45
46 def represent_ordered_dict(dumper, data):
47     value = []
48
49     for item_key, item_value in data.items():
50         node_key = dumper.represent_data(item_key)
51         node_value = dumper.represent_data(item_value)
52
53         value.append((node_key, node_value))
54
55     return yaml.nodes.MappingNode(u"tag:yaml.org,2002:map", value)
56
57
58 def get_json_template(template_dir, template_name):
59     template_name = template_name + ".json"
60     with open(os.path.join(template_dir, template_name)) as f:
61         return json.loads(f.read())
62
63
64 def get_or_create_template(template_dir, key, value, sequence, template_name):
65     """
66     Search a sequence of dicts where a given key matches value.  If
67     found, then it returns that item.  If not, then it loads the
68     template identified by template_name, adds it ot the sequence, and
69     returns the template
70     """
71     for item in sequence:
72         if item[key] == value:
73             return item
74     new_template = get_json_template(template_dir, template_name)
75     sequence.append(new_template)
76     return new_template
77
78
79 def yield_by_count(sequence):
80     """
81     Iterates through sequence and yields each item according to its __count__
82     attribute.  If an item has a __count__ of it will be returned 3 times
83     before advancing to the next item in the sequence.
84
85     :param sequence: sequence of dicts (must contain __count__)
86     :returns:        generator of tuple key, value pairs
87     """
88     for key, value in sequence.items():
89         for i in range(value["__count__"]):
90             yield (key, value)
91
92
93 def replace(param):
94     """
95     Optionally used by the preload generator to wrap items in the preload
96     that need to be replaced by end users
97     :param param: p
98     """
99     return "VALUE FOR: {}".format(param) if param else ""
100
101
102 class AbstractPreloadGenerator(ABC):
103     """
104     All preload generators must inherit from this class and implement the
105     abstract methods.
106
107     Preload generators are automatically discovered at runtime via a plugin
108     architecture.  The system path is scanned looking for modules with the name
109     preload_*, then all non-abstract classes that inherit from AbstractPreloadGenerator
110     are registered as preload plugins
111
112     Attributes:
113         :param vnf:             Instance of Vnf that contains the preload data
114         :param base_output_dir: Base directory to house the preloads.  All preloads
115                                 must be written to a subdirectory under this directory
116     """
117
118     def __init__(self, vnf, base_output_dir, preload_env):
119         self.preload_env = preload_env
120         self.vnf = vnf
121         self.current_module = None
122         self.current_module_env = {}
123         self.base_output_dir = base_output_dir
124         self.env_cache = {}
125         self.module_incomplete = False
126
127     @classmethod
128     @abstractmethod
129     def format_name(cls):
130         """
131         String name to identify the format (ex: VN-API, GR-API)
132         """
133         raise NotImplementedError()
134
135     @classmethod
136     @abstractmethod
137     def output_sub_dir(cls):
138         """
139         String sub-directory name that will appear under ``base_output_dir``
140         """
141         raise NotImplementedError()
142
143     @classmethod
144     @abstractmethod
145     def supports_output_passing(cls):
146         """
147         Some preload methods allow automatically mapping output parameters in the
148         base module to the input parameter of other modules.  This means these
149         that the incremental modules do not need these base module outputs in their
150         preloads.
151
152         At this time, VNF-API does not support output parameter passing, but
153         GR-API does.
154
155         If this is true, then the generator will call Vnf#filter_output_params
156         after the preload module for the base module has been created
157         """
158         raise NotImplementedError()
159
160     @abstractmethod
161     def generate_module(self, module, output_dir):
162         """
163         Create the preloads and write them to ``output_dir``.  This
164         method is responsible for generating the content of the preload and
165         writing the file to disk.
166         """
167         raise NotImplementedError()
168
169     def generate(self):
170         # handle the base module first
171         print("\nGenerating {} preloads".format(self.format_name()))
172         self.generate_environments(self.vnf.base_module)
173         if self.supports_output_passing():
174             self.vnf.filter_base_outputs()
175         for mod in self.vnf.incremental_modules:
176             self.generate_environments(mod)
177
178     def replace(self, param_name, alt_message=None, single=False):
179         value = self.get_param(param_name, single)
180         value = None if value == "CHANGEME" else value
181         if value:
182             return value
183         else:
184             self.module_incomplete = True
185             return alt_message or replace(param_name)
186
187     def start_module(self, module, env):
188         """Initialize/reset the environment for the module"""
189         self.current_module = module
190         self.current_module_env = env
191         self.module_incomplete = False
192         self.env_cache = {}
193
194     def generate_environments(self, module):
195         """
196         Generate a preload for the given module in all available environments
197         in the ``self.preload_env``.  This will invoke the abstract
198         generate_module once for each available environment **and** an
199         empty environment to create a blank template.
200
201         :param module:  module to generate for
202         """
203         print("\nGenerating Preloads for {}".format(module))
204         print("-" * 50)
205         print("... generating blank template")
206         self.start_module(module, {})
207         blank_preload_dir = self.make_preload_dir(self.base_output_dir)
208         self.generate_module(module, blank_preload_dir)
209         self.generate_preload_env(module, blank_preload_dir)
210         if self.preload_env:
211             for env in self.preload_env.environments:
212                 output_dir = self.make_preload_dir(env.base_dir / "preloads")
213                 print(
214                     "... generating preload for env ({}) to {}".format(
215                         env.name, output_dir
216                     )
217                 )
218                 self.start_module(module, env.get_module(module.label))
219                 self.generate_module(module, output_dir)
220
221     def make_preload_dir(self, base_dir):
222         path = os.path.join(base_dir, self.output_sub_dir())
223         if not os.path.exists(path):
224             os.makedirs(path, exist_ok=True)
225         return path
226
227     @staticmethod
228     def generate_preload_env(module, blank_preload_dir):
229         """
230         Create a .env template suitable for completing and using for
231         preload generation from env files.
232         """
233         yaml.add_representer(OrderedDict, represent_ordered_dict)
234         output_dir = os.path.join(blank_preload_dir, "preload_env")
235         env_file = os.path.join(output_dir, "{}.env".format(module.vnf_name))
236         defaults_file = os.path.join(output_dir, "defaults.yaml")
237         if not os.path.exists(output_dir):
238             os.makedirs(output_dir, exist_ok=True)
239         with open(env_file, "w") as f:
240             yaml.dump(module.env_template, f)
241         if not os.path.exists(defaults_file):
242             with open(defaults_file, "w") as f:
243                 yaml.dump({"vnf_name": "CHANGEME"}, f)
244
245     def get_param(self, param_name, single):
246         """
247         Retrieves the value for the given param if it exists. If requesting a
248         single item, and the parameter is tied to a list then only one item from
249         the list will be returned.  For each subsequent call with the same parameter
250         it will iterate/rotate through the values in that list.  If single is False
251         then the full list will be returned.
252
253         :param param_name:  name of the parameter
254         :param single:      If True returns single value from lists otherwises the full
255                             list.  This has no effect on non-list values
256         """
257         value = self.env_cache.get(param_name)
258         if not value:
259             value = self.current_module_env.get(param_name)
260             if isinstance(value, list):
261                 value.reverse()
262             self.env_cache[param_name] = value
263         if value and single and isinstance(value, list):
264             return value.pop()
265         else:
266             return value