2 # ============LICENSE_START====================================================
3 # org.onap.vvp/validation-scripts
4 # ===================================================================
5 # Copyright © 2019 AT&T Intellectual Property. All rights reserved.
6 # ===================================================================
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
13 # http://www.apache.org/licenses/LICENSE-2.0
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.
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
28 # https://creativecommons.org/licenses/by/4.0/
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.
36 # ============LICENSE_END============================================
43 from abc import ABC, abstractmethod
44 from itertools import chain
45 from typing import Set
47 from tests.helpers import (
54 from tests.parametrizers import parametrize_heat_templates
55 from tests.structures import NeutronPortProcessor, Heat
56 from tests.test_environment_file_parameters import get_preload_excluded_parameters
57 from tests.utils import nested_dict
58 from tests.utils.vm_types import get_vm_type_for_nova_server
61 # This is only used to fake out parametrizers
63 def __init__(self, config):
67 def parametrize(self, name, file_list):
68 self.inputs[name] = file_list
71 def get_heat_templates(config):
73 Returns the Heat template paths discovered by the pytest parameterizers
74 :param config: pytest config
75 :return: list of heat template paths
77 meta = DummyMetafunc(config)
78 parametrize_heat_templates(meta)
79 heat_templates = meta.inputs.get("heat_templates", [])
80 if isinstance(heat_templates, list) and len(heat_templates) > 0:
81 heat_templates = heat_templates[0]
87 def get_json_template(template_dir, template_name):
88 template_name = template_name + ".json"
89 with open(os.path.join(template_dir, template_name)) as f:
90 return json.loads(f.read())
93 def remove(sequence, exclude, key=None):
95 Remove a copy of sequence that items occur in exclude.
97 :param sequence: sequence of objects
98 :param exclude: objects to excluded (must support ``in`` check)
99 :param key: optional function to extract key from item in sequence
100 :return: list of items not in the excluded
102 key_func = key if key else lambda x: x
103 result = (s for s in sequence if key_func(s) not in exclude)
104 return set(result) if isinstance(sequence, Set) else list(result)
107 def get_or_create_template(template_dir, key, value, sequence, template_name):
109 Search a sequence of dicts where a given key matches value. If
110 found, then it returns that item. If not, then it loads the
111 template identified by template_name, adds it ot the sequence, and
114 for item in sequence:
115 if item[key] == value:
117 new_template = get_json_template(template_dir, template_name)
118 sequence.append(new_template)
124 Optionally used by the preload generator to wrap items in the preload
125 that need to be replaced by end users
128 return "VALUE FOR: {}".format(param) if param else ""
131 class AbstractPreloadGenerator(ABC):
133 All preload generators must inherit from this class and implement the
136 Preload generators are automatically discovered at runtime via a plugin
137 architecture. The system path is scanned looking for modules with the name
138 preload_*, then all non-abstract classes that inherit from AbstractPreloadGenerator
139 are registered as preload plugins
142 :param vnf: Instance of Vnf that contains the preload data
143 :param base_output_dir: Base directory to house the preloads. All preloads
144 must be written to a subdirectory under this directory
147 def __init__(self, vnf, base_output_dir):
149 self.base_output_dir = base_output_dir
150 os.makedirs(self.output_dir, exist_ok=True)
154 def format_name(cls):
156 String name to identify the format (ex: VN-API, GR-API)
158 raise NotImplementedError()
162 def output_sub_dir(cls):
164 String sub-directory name that will appear under ``base_output_dir``
166 raise NotImplementedError()
170 def supports_output_passing(cls):
172 Some preload methods allow automatically mapping output parameters in the
173 base module to the input parameter of other modules. This means these
174 that the incremental modules do not need these base module outputs in their
177 At this time, VNF-API does not support output parameter passing, but
180 If this is true, then the generator will call Vnf#filter_output_params
181 after the preload module for the base module has been created
183 raise NotImplementedError()
186 def generate_module(self, module):
188 Create the preloads and write them to ``self.output_dir``. This
189 method is responsible for generating the content of the preload and
190 writing the file to disk.
192 raise NotImplementedError()
195 def output_dir(self):
196 return os.path.join(self.base_output_dir, self.output_sub_dir())
199 # handle the base module first
200 print("\nGenerating {} preloads".format(self.format_name()))
201 self.generate_module(self.vnf.base_module)
202 print("... generated template for {}".format(self.vnf.base_module))
203 if self.supports_output_passing():
204 self.vnf.filter_base_outputs()
205 for mod in self.vnf.incremental_modules:
206 self.generate_module(mod)
207 print("... generated for {}".format(mod))
210 class FilterBaseOutputs(ABC):
212 Invoked to remove parameters in an object that appear in the base module.
213 Base output parameters can be passed to incremental modules
214 so they do not need to be defined in a preload. This method can be
215 invoked on a module to pre-filter the parameters before a preload is
218 The method should remove the parameters that exist in the base module from
219 both itself and any sub-objects.
223 def filter_output_params(self, base_outputs):
224 raise NotImplementedError()
228 def __init__(self, ip_addr_param, port):
229 self.param = ip_addr_param or ""
233 def ip_version(self):
234 return 6 if "_v6_" in self.param else 4
237 return hash(self.param)
239 def __eq__(self, other):
240 return hash(self) == hash(other)
243 return "{}(v{})".format(self.param, self.ip_version)
249 class Network(FilterBaseOutputs):
250 def __init__(self, role, name_param):
251 self.network_role = role
252 self.name_param = name_param
253 self.subnet_params = set()
255 def filter_output_params(self, base_outputs):
256 self.subnet_params = remove(self.subnet_params, base_outputs)
259 return hash(self.network_role)
261 def __eq__(self, other):
262 return hash(self) == hash(other)
265 class Port(FilterBaseOutputs):
266 def __init__(self, vm, network):
268 self.network = network
270 self.floating_ips = []
271 self.uses_dhcp = True
273 def add_ips(self, props):
274 props = props.get("properties") or props
275 for fixed_ip in props.get("fixed_ips") or []:
276 if not isinstance(fixed_ip, dict):
278 ip_address = get_param(fixed_ip.get("ip_address"))
279 subnet = get_param(fixed_ip.get("subnet") or fixed_ip.get("subnet_id"))
281 self.uses_dhcp = False
282 self.fixed_ips.append(IpParam(ip_address, self))
284 self.network.subnet_params.add(subnet)
285 for ip in prop_iterator(props, "allowed_address_pairs", "ip_address"):
286 self.uses_dhcp = False
287 param = get_param(ip) if ip else ""
289 self.floating_ips.append(IpParam(param, self))
291 def filter_output_params(self, base_outputs):
292 self.fixed_ips = remove(self.fixed_ips, base_outputs, key=lambda ip: ip.param)
293 self.floating_ips = remove(
294 self.floating_ips, base_outputs, key=lambda ip: ip.param
298 class VirtualMachineType(FilterBaseOutputs):
299 def __init__(self, vm_type, vnf_module):
300 self.vm_type = vm_type
304 self.vnf_module = vnf_module
306 def filter_output_params(self, base_outputs):
307 self.names = remove(self.names, base_outputs)
308 for port in self.ports:
309 port.filter_output_params(base_outputs)
313 return {port.network for port in self.ports}
316 def floating_ips(self):
317 for port in self.ports:
318 for ip in port.floating_ips:
323 for port in self.ports:
324 for ip in port.fixed_ips:
327 def update_ports(self, network, props):
328 port = self.get_or_create_port(network)
331 def get_or_create_port(self, network):
332 for port in self.ports:
333 if port.network == network:
335 port = Port(self, network)
336 self.ports.append(port)
341 def __init__(self, templates):
342 self.modules = [VnfModule(t, self) for t in templates]
343 self.uses_contrail = self._uses_contrail()
344 self.base_module = next(
345 (mod for mod in self.modules if mod.is_base_module), None
347 self.incremental_modules = [m for m in self.modules if not m.is_base_module]
349 def _uses_contrail(self):
350 for mod in self.modules:
351 resources = mod.heat.get_all_resources()
352 types = (r.get("type", "") for r in resources.values())
353 if any(t.startswith("OS::ContrailV2") for t in types):
358 def base_output_params(self):
359 return self.base_module.heat.outputs
361 def filter_base_outputs(self):
362 non_base_modules = (m for m in self.modules if not m.is_base_module)
363 for mod in non_base_modules:
364 mod.filter_output_params(self.base_output_params)
367 def yield_by_count(sequence):
369 Iterates through sequence and yields each item according to its __count__
370 attribute. If an item has a __count__ of it will be returned 3 times
371 before advancing to the next item in the sequence.
373 :param sequence: sequence of dicts (must contain __count__)
374 :returns: generator of tuple key, value pairs
376 for key, value in sequence.items():
377 for i in range(value["__count__"]):
381 def env_path(heat_path):
383 Create the path to the env file for the give heat path.
384 :param heat_path: path to heat file
385 :return: path to env file (assumes it is present and named correctly)
387 base_path = os.path.splitext(heat_path)[0]
388 return "{}.env".format(base_path)
391 class VnfModule(FilterBaseOutputs):
392 def __init__(self, template_file, vnf):
394 self.vnf_name = os.path.splitext(os.path.basename(template_file))[0]
395 self.template_file = template_file
396 self.heat = Heat(filepath=template_file, envpath=env_path(template_file))
397 env_pair = get_environment_pair(self.template_file)
398 env_yaml = env_pair.get("eyml") if env_pair else {}
399 self.parameters = env_yaml.get("parameters") or {}
401 self.virtual_machine_types = self._create_vm_types()
403 self.outputs_filtered = False
405 def filter_output_params(self, base_outputs):
406 for vm in self.virtual_machine_types:
407 vm.filter_output_params(base_outputs)
408 for network in self.networks:
409 network.filter_output_params(base_outputs)
411 k: v for k, v in self.parameters.items() if k not in base_outputs
415 for network in self.networks
416 if network.name_param not in base_outputs or network.subnet_params
418 self.outputs_filtered = True
420 def _create_vm_types(self):
421 servers = self.heat.get_resource_by_type("OS::Nova::Server", all_resources=True)
423 for _, props in yield_by_count(servers):
424 vm_type = get_vm_type_for_nova_server(props)
425 vm = vm_types.setdefault(vm_type, VirtualMachineType(vm_type, self))
427 name = nested_dict.get(props, "properties", "name", default={})
428 vm_name = get_param(name) if name else ""
429 vm.names.append(vm_name)
430 return list(vm_types.values())
432 def _add_networks(self):
433 ports = self.heat.get_resource_by_type("OS::Neutron::Port", all_resources=True)
434 for rid, props in yield_by_count(ports):
435 resource_type, port_match = NeutronPortProcessor.get_rid_match_tuple(rid)
436 if resource_type != "external":
438 network_role = port_match.group("network_role")
439 vm = self._get_vm_type(port_match.group("vm_type"))
440 network = self._get_network(network_role, props)
441 vm.update_ports(network, props)
444 def is_base_module(self):
445 return is_base_module(self.template_file)
448 def availability_zones(self):
449 """Returns a list of all availability zone parameters found in the template"""
451 p for p in self.heat.parameters if p.startswith("availability_zone")
455 def preload_parameters(self):
457 Subset of parameters from the env file that can be overridden in
458 tag values. Per VNF Heat Guidelines, specific parameters such as
459 flavor, image, etc. must not be overridden so they are excluded.
461 :return: dict of parameters suitable for the preload
463 excluded = get_preload_excluded_parameters(self.template_file)
464 return {k: v for k, v in self.parameters.items() if k not in excluded}
466 def _get_vm_type(self, vm_type):
467 for vm in self.virtual_machine_types:
468 if vm_type.lower() == vm.vm_type.lower():
470 raise RuntimeError("Encountered unknown VM type: {}".format(vm_type))
472 def _get_network(self, network_role, props):
473 network_prop = nested_dict.get(props, "properties", "network") or {}
474 name_param = get_param(network_prop) if network_prop else ""
475 for network in self.networks:
476 if network.network_role.lower() == network_role.lower():
478 new_network = Network(network_role, name_param)
479 self.networks.append(new_network)
483 return "VNF Module ({})".format(os.path.basename(self.template_file))
489 return hash(self.vnf_name)
491 def __eq__(self, other):
492 return hash(self) == hash(other)
495 def create_preloads(config, exitstatus):
497 Create preloads in every format that can be discovered by get_generator_plugins
499 if config.getoption("self_test"):
501 print("+===================================================================+")
502 print("| Preload Template Generation |")
503 print("+===================================================================+")
505 preload_dir = os.path.join(get_output_dir(config), "preloads")
506 if os.path.exists(preload_dir):
507 shutil.rmtree(preload_dir)
508 heat_templates = get_heat_templates(config)
510 for gen_class in get_generator_plugins():
511 vnf = Vnf(heat_templates)
512 generator = gen_class(vnf, preload_dir)
514 if vnf and vnf.uses_contrail:
516 "\nWARNING: Preload template generation does not support Contrail\n"
517 "at this time, but Contrail resources were detected. The preload \n"
518 "template may be incomplete."
522 "\nWARNING: Heat violations detected. Preload templates may be\n"
527 def is_preload_generator(class_):
529 Returns True if the class is an implementation of AbstractPreloadGenerator
532 inspect.isclass(class_)
533 and not inspect.isabstract(class_)
534 and issubclass(class_, AbstractPreloadGenerator)
538 def get_generator_plugins():
540 Scan the system path for modules that are preload plugins and discover
541 and return the classes that implement AbstractPreloadGenerator in those
545 importlib.import_module(name)
546 for finder, name, ispkg in pkgutil.iter_modules()
547 if name.startswith("preload_")
549 members = chain.from_iterable(
550 inspect.getmembers(mod, is_preload_generator) for mod in preload_plugins
552 return [m[1] for m in members]