[VVP] Flag duplicate parameters in .env files
[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         if self.vnf.base_module:
173             self.generate_environments(self.vnf.base_module)
174         if self.supports_output_passing():
175             self.vnf.filter_base_outputs()
176         for mod in self.vnf.incremental_modules:
177             self.generate_environments(mod)
178
179     def replace(self, param_name, alt_message=None, single=False):
180         value = self.get_param(param_name, single)
181         value = None if value == "CHANGEME" else value
182         if value:
183             return value
184         else:
185             self.module_incomplete = True
186             return alt_message or replace(param_name)
187
188     def start_module(self, module, env):
189         """Initialize/reset the environment for the module"""
190         self.current_module = module
191         self.current_module_env = env
192         self.module_incomplete = False
193         self.env_cache = {}
194
195     def generate_environments(self, module):
196         """
197         Generate a preload for the given module in all available environments
198         in the ``self.preload_env``.  This will invoke the abstract
199         generate_module once for each available environment **and** an
200         empty environment to create a blank template.
201
202         :param module:  module to generate for
203         """
204         print("\nGenerating Preloads for {}".format(module))
205         print("-" * 50)
206         print("... generating blank template")
207         self.start_module(module, {})
208         blank_preload_dir = self.make_preload_dir(self.base_output_dir)
209         self.generate_module(module, blank_preload_dir)
210         self.generate_preload_env(module, blank_preload_dir)
211         if self.preload_env:
212             for env in self.preload_env.environments:
213                 output_dir = self.make_preload_dir(env.base_dir / "preloads")
214                 print(
215                     "... generating preload for env ({}) to {}".format(
216                         env.name, output_dir
217                     )
218                 )
219                 self.start_module(module, env.get_module(module.label))
220                 self.generate_module(module, output_dir)
221
222     def make_preload_dir(self, base_dir):
223         path = os.path.join(base_dir, self.output_sub_dir())
224         if not os.path.exists(path):
225             os.makedirs(path, exist_ok=True)
226         return path
227
228     @staticmethod
229     def generate_preload_env(module, blank_preload_dir):
230         """
231         Create a .env template suitable for completing and using for
232         preload generation from env files.
233         """
234         yaml.add_representer(OrderedDict, represent_ordered_dict)
235         output_dir = os.path.join(blank_preload_dir, "preload_env")
236         env_file = os.path.join(output_dir, "{}.env".format(module.vnf_name))
237         defaults_file = os.path.join(output_dir, "defaults.yaml")
238         if not os.path.exists(output_dir):
239             os.makedirs(output_dir, exist_ok=True)
240         with open(env_file, "w") as f:
241             yaml.dump(module.env_template, f)
242         if not os.path.exists(defaults_file):
243             with open(defaults_file, "w") as f:
244                 yaml.dump({"vnf_name": "CHANGEME"}, f)
245
246     def get_param(self, param_name, single):
247         """
248         Retrieves the value for the given param if it exists. If requesting a
249         single item, and the parameter is tied to a list then only one item from
250         the list will be returned.  For each subsequent call with the same parameter
251         it will iterate/rotate through the values in that list.  If single is False
252         then the full list will be returned.
253
254         :param param_name:  name of the parameter
255         :param single:      If True returns single value from lists otherwises the full
256                             list.  This has no effect on non-list values
257         """
258         value = self.env_cache.get(param_name)
259         if not value:
260             value = self.current_module_env.get(param_name)
261             if isinstance(value, list):
262                 value = value.copy()
263                 value.reverse()
264             self.env_cache[param_name] = value
265         if value and single and isinstance(value, list):
266             return value.pop()
267         else:
268             return value