aboutsummaryrefslogtreecommitdiffstats
path: root/ice_validator/preload
diff options
context:
space:
mode:
Diffstat (limited to 'ice_validator/preload')
-rw-r--r--ice_validator/preload/__init__.py4
-rw-r--r--ice_validator/preload/data.py372
-rw-r--r--ice_validator/preload/engine.py114
-rw-r--r--ice_validator/preload/environment.py135
-rw-r--r--ice_validator/preload/generator.py145
-rw-r--r--ice_validator/preload/model.py136
6 files changed, 765 insertions, 141 deletions
diff --git a/ice_validator/preload/__init__.py b/ice_validator/preload/__init__.py
index 70f9ecb..ec6ad7b 100644
--- a/ice_validator/preload/__init__.py
+++ b/ice_validator/preload/__init__.py
@@ -34,3 +34,7 @@
# limitations under the License.
#
# ============LICENSE_END============================================
+
+from preload.environment import EnvironmentFileDataSource
+
+__all__ = ["EnvironmentFileDataSource"]
diff --git a/ice_validator/preload/data.py b/ice_validator/preload/data.py
new file mode 100644
index 0000000..721608f
--- /dev/null
+++ b/ice_validator/preload/data.py
@@ -0,0 +1,372 @@
+from abc import ABC, abstractmethod
+from pathlib import Path
+from typing import Iterable, Any, Optional, Mapping
+
+from preload.model import VnfModule
+
+
+class AbstractPreloadInstance(ABC):
+ """
+ Represents the data source for a single instance of a preload for
+ any format. The implementation of AbstractPreloadGenerator will
+ call the methods of this class to retrieve the necessary data
+ to populate the preload. If a data element is not available,
+ then simply return ``None`` and a suitable placeholder will be
+ placed in the preload.
+ """
+
+ @property
+ @abstractmethod
+ def output_dir(self) -> Path:
+ """
+ Base output directory where the preload will be generated. Please
+ note, that the generator may create nested directories under this
+ directory for the preload.
+
+ :return: Path to the desired output directory. This directory
+ and its parents will be created by the generator if
+ it is not already present.
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def module_label(self) -> str:
+ """
+ Identifier of the module. This must match the base name of the
+ heat module (ex: if the Heat file name is base.yaml, then the label
+ is 'base'.
+
+ :return: string name of the module
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def vf_module_name(self) -> Optional[str]:
+ """
+ :return: module name to populate in the preload if available
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def flag_incompletes(self) -> bool:
+ """
+ If True, then the generator will modify the file name of any
+ generated preload to end with _incomplete.<ext> if any preload
+ value was not satisfied by the data source. If False, then
+ the file name will be the same regardless of the completeness
+ of the preload.
+
+ :return: True if file names should denote preload incompleteness
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def preload_basename(self) -> str:
+ """
+ Base name of the preload that will be used by the generator to create
+ the file name.
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def vnf_name(self) -> Optional[str]:
+ """
+ :return: the VNF name to populate in the prelad if available
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def vnf_type(self) -> Optional[str]:
+ """
+ The VNF Type must be match the values in SDC. It is a concatenation
+ of <Service Instance Name>/<Resource Instance Name>.
+
+ :return: VNF Type to populate in the preload if available
+ """
+ raise NotImplementedError()
+
+ @property
+ @abstractmethod
+ def vf_module_model_name(self) -> Optional[str]:
+ """
+ :return: Module model name if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_availability_zone(self, index: int, param_name: str) -> Optional[str]:
+ """
+ Retrieve the value for the availability zone at requested zero-based
+ index (i.e. 0, 1, 2, etc.)
+
+ :param index: index of availability zone (0, 1, etc.)
+ :param param_name: Name of the parameter from Heat
+ :return: value for the AZ if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_network_name(self, network_role: str, name_param: str) -> Optional[str]:
+ """
+ Retrieve the OpenStack name of the network for the given network role.
+
+ :param network_role: Network role from Heat template
+ :param name_param: Network name parameter from Heat
+ :return: Name of the network if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_subnet_id(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ """
+ Retrieve the subnet's UUID for the given network and IP version (4 or 6).
+
+ :param network_role: Network role from Heat template
+ :param ip_version: IP Version (4 or 6)
+ :param param_name: Parameter name from Heat
+ :return: UUID of the subnet if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_subnet_name(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ """
+ Retrieve the OpenStack Subnet name for the given network role and IP version
+
+ :param network_role: Network role from Heat template
+ :param ip_version: IP Version (4 or 6)
+ :param param_name: Parameter name from Heat
+ :return: Name of the subnet if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_vm_name(self, vm_type: str, index: int, param_name: str) -> Optional[str]:
+ """
+ Retrieve the vm name for the given VM type and index.
+
+ :param vm_type: VM Type from Heat template
+ :param index: Zero-based index of the VM for the vm-type
+ :param param_name: Parameter name from Heat
+ :return: VM Name if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_floating_ip(
+ self, vm_type: str, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ """
+ Retreive the floating IP for the VM and Port identified by VM Type,
+ Network Role, and IP Version.
+
+ :param vm_type: VM Type from Heat template
+ :param network_role: Network Role from Heat template
+ :param ip_version: IP Version (4 or 6)
+ :param param_name: Parameter name from Heat
+ :return: floating IP address if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_fixed_ip(
+ self, vm_type: str, network_role: str, ip_version: int, index: int, param: str
+ ) -> Optional[str]:
+ """
+ Retreive the fixed IP for the VM and Port identified by VM Type,
+ Network Role, IP Version, and index.
+
+ :param vm_type: VM Type from Heat template
+ :param network_role: Network Role from Heat template
+ :param ip_version: IP Version (4 or 6)
+ :param index: zero-based index for the IP for the given
+ VM Type, Network Role, IP Version combo
+ :param param_name: Parameter name from Heat
+ :return: floating IP address if available
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_vnf_parameter(self, key: str, value: Any) -> Optional[Any]:
+ """
+ Retrieve the value for the given key. These will be placed in the
+ tag-values/vnf parameters in the preload. If a value was specified in
+ the environment packaged in the Heat for for the VNF module, then
+ that value will be passed in ``value``. This class can return
+ the value or ``None`` if it does not have a value for the given key.
+
+ :param key: parameter name from Heat
+ :param value: Value from Heat env file if it was assigned there;
+ None otherwise
+ :return: Returns the value for the object. This should
+ be a str, dict, or list. The generator will
+ format it properly based on the selected output format
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_additional_parameters(self) -> Mapping[str, Any]:
+ """
+ Return any additional parameters that should be added to the VNF parameters.
+
+ This can be useful if you want to duplicate paramters in tag values that are
+ also in the other sections (ex: VM names).
+
+ :return: dict of str to object mappings that the generator must add to
+ the vnf_parameters/tag values
+ """
+ raise NotImplementedError()
+
+
+class AbstractPreloadDataSource(ABC):
+ """
+ Represents a data source for a VNF preload data. Implementations of this
+ class can be dynamically discovered if they are in a preload plugin module.
+ A module is considered a preload plugin module if it starts with
+ prelaod_ and is available as a top level module on Python's sys.path.
+
+ The ``get_module_preloads`` will be invoked for each module in
+ the VNF. An instance of AbstractPreloadInstance must be returned for
+ each instance of the preload module that is to be created.
+
+ Parameters:
+ :param path: The path to the configuration source selected
+ in either the VVP GUI or command-line. This
+ may be a file or directory depending upon
+ the source_type defined by this data source
+ """
+
+ def __init__(self, path: Path):
+ self.path = path
+
+ @classmethod
+ @abstractmethod
+ def get_source_type(cls) -> str:
+ """
+ If 'FILE' returned, then the config source will be a specific
+ file; If 'DIR', then the config source will be a directory
+ :return:
+ """
+ raise NotImplementedError()
+
+ @classmethod
+ @abstractmethod
+ def get_identifier(cls) -> str:
+ """
+ Identifier for the given data source. This is the value that
+ can be passed via --preload-source-type.
+
+ :return: short identifier for this data source type
+ """
+
+ @classmethod
+ @abstractmethod
+ def get_name(self) -> str:
+ """
+ Human readable name to describe the preload data source. It is
+ recommended not to exceed 50 characters.
+
+ :return: human readable name of the preload data source (ex: Environment Files)
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def get_module_preloads(
+ self, module: VnfModule
+ ) -> Iterable[AbstractPreloadInstance]:
+ """
+ For the requested module, return an instance of AbstractPreloadInstance
+ for every preload module you wish to be created.
+
+ :param module: Module of the VNF
+ :return: iterable of preloads to create for the given module
+ """
+ raise NotImplementedError()
+
+
+class BlankPreloadInstance(AbstractPreloadInstance):
+ """
+ Used to create blank preload templates. VVP will always create
+ a template of a preload in the requested format with no data provided.
+ """
+
+ def __init__(self, output_dir: Path, module_name: str):
+ self._output_dir = output_dir
+ self._module_name = module_name
+
+ @property
+ def flag_incompletes(self) -> bool:
+ return False
+
+ @property
+ def preload_basename(self) -> str:
+ return self._module_name
+
+ @property
+ def vf_module_name(self) -> Optional[str]:
+ return None
+
+ def get_vm_name(self, vm_type: str, index: int, param_name: str) -> Optional[str]:
+ return None
+
+ def get_availability_zone(self, index: int, param_name: str) -> Optional[str]:
+ return None
+
+ @property
+ def output_dir(self) -> Path:
+ return self._output_dir
+
+ @property
+ def module_label(self) -> str:
+ return self._module_name
+
+ @property
+ def vnf_name(self) -> Optional[str]:
+ return None
+
+ @property
+ def vnf_type(self) -> Optional[str]:
+ return None
+
+ @property
+ def vf_module_model_name(self) -> Optional[str]:
+ return None
+
+ def get_network_name(self, network_role: str, name_param: str) -> Optional[str]:
+ return None
+
+ def get_subnet_id(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ return None
+
+ def get_subnet_name(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ return None
+
+ def get_floating_ip(
+ self, vm_type: str, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ return None
+
+ def get_fixed_ip(
+ self, vm_type: str, network_role: str, ip_version: int, index: int, param: str
+ ) -> Optional[str]:
+ return None
+
+ def get_vnf_parameter(self, key: str, value: Any) -> Optional[Any]:
+ return None
+
+ def get_additional_parameters(self) -> Mapping[str, Any]:
+ return {}
diff --git a/ice_validator/preload/engine.py b/ice_validator/preload/engine.py
new file mode 100644
index 0000000..488766d
--- /dev/null
+++ b/ice_validator/preload/engine.py
@@ -0,0 +1,114 @@
+import importlib
+import inspect
+import os
+import pkgutil
+import shutil
+from itertools import chain
+from pathlib import Path
+from typing import List, Type
+
+from preload.data import AbstractPreloadDataSource
+from preload.generator import AbstractPreloadGenerator
+from preload.model import get_heat_templates, Vnf
+from tests.helpers import get_output_dir
+
+
+def create_preloads(config, exitstatus):
+ """
+ Create preloads in every format that can be discovered by get_generator_plugins
+ """
+ if config.getoption("self_test"):
+ return
+ print("+===================================================================+")
+ print("| Preload Template Generation |")
+ print("+===================================================================+")
+
+ preload_dir = os.path.join(get_output_dir(config), "preloads")
+ if os.path.exists(preload_dir):
+ shutil.rmtree(preload_dir)
+ plugins = PluginManager()
+ available_formats = [p.format_name() for p in plugins.preload_generators]
+ selected_formats = config.getoption("preload_formats") or available_formats
+ preload_source = None
+ if config.getoption("preload_source"):
+ preload_source_path = Path(config.getoption("preload_source"))
+ source_class = plugins.get_source_for_id(
+ config.getoption("preload_source_type")
+ )
+ preload_source = source_class(preload_source_path)
+
+ heat_templates = get_heat_templates(config)
+ vnf = None
+ for plugin_class in plugins.preload_generators:
+ if plugin_class.format_name() not in selected_formats:
+ continue
+ vnf = Vnf(heat_templates)
+ generator = plugin_class(vnf, preload_dir, preload_source)
+ generator.generate()
+ if vnf and vnf.uses_contrail:
+ print(
+ "\nWARNING: Preload template generation does not support Contrail\n"
+ "at this time, but Contrail resources were detected. The preload \n"
+ "template may be incomplete."
+ )
+ if exitstatus != 0:
+ print(
+ "\nWARNING: Heat violations detected. Preload templates may be\n"
+ "incomplete or have errors."
+ )
+
+
+def is_implementation_of(class_, base_class):
+ """
+ Returns True if the class is an implementation of AbstractPreloadGenerator
+ """
+ return (
+ inspect.isclass(class_)
+ and not inspect.isabstract(class_)
+ and issubclass(class_, base_class)
+ )
+
+
+def get_implementations_of(class_, modules):
+ """
+ Returns all classes that implement ``class_`` from modules
+ """
+ members = list(
+ chain.from_iterable(
+ inspect.getmembers(mod, lambda c: is_implementation_of(c, class_))
+ for mod in modules
+ )
+ )
+ return [m[1] for m in members]
+
+
+class PluginManager:
+ def __init__(self):
+ self.preload_plugins = [
+ importlib.import_module(name)
+ for finder, name, ispkg in pkgutil.iter_modules()
+ if name.startswith("preload_") or name == "preload"
+ ]
+ self.preload_generators: List[
+ Type[AbstractPreloadGenerator]
+ ] = get_implementations_of(AbstractPreloadGenerator, self.preload_plugins)
+ self.preload_sources: List[
+ Type[AbstractPreloadDataSource]
+ ] = get_implementations_of(AbstractPreloadDataSource, self.preload_plugins)
+
+ def get_source_for_id(self, identifier: str) -> Type[AbstractPreloadDataSource]:
+ for source in self.preload_sources:
+ if identifier == source.get_identifier():
+ return source
+ raise RuntimeError(
+ "Unable to find preload source for identifier {}".format(identifier)
+ )
+
+ def get_source_for_name(self, name: str) -> Type[AbstractPreloadDataSource]:
+ for source in self.preload_sources:
+ if name == source.get_name():
+ return source
+ raise RuntimeError("Unable to find preload source for name {}".format(name))
+
+
+PLUGIN_MGR = PluginManager()
diff --git a/ice_validator/preload/environment.py b/ice_validator/preload/environment.py
index 083be9b..0477e66 100644
--- a/ice_validator/preload/environment.py
+++ b/ice_validator/preload/environment.py
@@ -1,14 +1,19 @@
import re
import tempfile
from pathlib import Path
+from typing import Any, Optional, Mapping
from cached_property import cached_property
+from preload.data import AbstractPreloadInstance, AbstractPreloadDataSource
+from preload.model import VnfModule
from tests.helpers import check, first, unzip, load_yaml
SERVICE_TEMPLATE_PATTERN = re.compile(r".*service-.*?-template.yml")
RESOURCE_TEMPLATE_PATTERN = re.compile(r".*resource-(.*?)-template.yml")
+ZONE_PARAMS = ("availability_zone_0", "availability_zone_1", "availability_zone_2")
+
def yaml_files(path):
"""
@@ -278,3 +283,133 @@ class PreloadEnvironment:
def __repr__(self):
return "PreloadEnvironment(name={})".format(self.name)
+
+
+class EnvironmentFilePreloadInstance(AbstractPreloadInstance):
+
+ def __init__(self, env: PreloadEnvironment, module_label: str, module_params: dict):
+ self.module_params = module_params
+ self._module_label = module_label
+ self.env = env
+ self.env_cache = {}
+
+ @property
+ def flag_incompletes(self) -> bool:
+ return True
+
+ @property
+ def preload_basename(self) -> str:
+ return self.module_label
+
+ @property
+ def output_dir(self) -> Path:
+ return self.env.base_dir.joinpath("preloads")
+
+ @property
+ def module_label(self) -> str:
+ return self._module_label
+
+ @property
+ def vf_module_name(self) -> str:
+ return self.get_param("vf_module_name")
+
+ @property
+ def vnf_name(self) -> Optional[str]:
+ return self.get_param("vnf_name")
+
+ @property
+ def vnf_type(self) -> Optional[str]:
+ return self.get_param("vnf-type")
+
+ @property
+ def vf_module_model_name(self) -> Optional[str]:
+ return self.get_param("vf-module-model-name")
+
+ def get_availability_zone(self, index: int, param_name: str) -> Optional[str]:
+ return self.get_param(param_name)
+
+ def get_network_name(self, network_role: str, name_param: str) -> Optional[str]:
+ return self.get_param(name_param)
+
+ def get_subnet_id(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ return self.get_param(param_name)
+
+ def get_subnet_name(
+ self, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ # Not supported with env files
+ return None
+
+ def get_vm_name(self, vm_type: str, index: int, param_name: str) -> Optional[str]:
+ return self.get_param(param_name, single=True)
+
+ def get_floating_ip(
+ self, vm_type: str, network_role: str, ip_version: int, param_name: str
+ ) -> Optional[str]:
+ return self.get_param(param_name)
+
+ def get_fixed_ip(
+ self, vm_type: str, network_role: str, ip_version: int, index: int, param: str
+ ) -> Optional[str]:
+ return self.get_param(param, single=True)
+
+ def get_vnf_parameter(self, key: str, value: Any) -> Optional[str]:
+ module_value = self.get_param(key)
+ return module_value or value
+
+ def get_additional_parameters(self) -> Mapping[str, Any]:
+ return {}
+
+ def get_param(self, param_name, single=False):
+ """
+ Retrieves the value for the given param if it exists. If requesting a
+ single item, and the parameter is tied to a list then only one item from
+ the list will be returned. For each subsequent call with the same parameter
+ it will iterate/rotate through the values in that list. If single is False
+ then the full list will be returned.
+
+ :param param_name: name of the parameter
+ :param single: If True returns single value from lists otherwises the full
+ list. This has no effect on non-list values
+ """
+ value = self.env_cache.get(param_name)
+ if not value:
+ value = self.module_params.get(param_name)
+ if isinstance(value, list):
+ value = value.copy()
+ value.reverse()
+ self.env_cache[param_name] = value
+
+ if value and single and isinstance(value, list):
+ result = value.pop()
+ else:
+ result = value
+ return result if result != "CHANGEME" else None
+
+
+class EnvironmentFileDataSource(AbstractPreloadDataSource):
+
+ def __init__(self, path: Path):
+ super().__init__(path)
+ check(path.is_dir(), f"{path} must be an existing directory")
+ self.path = path
+ self.env = PreloadEnvironment(path)
+
+ @classmethod
+ def get_source_type(cls) -> str:
+ return "DIR"
+
+ @classmethod
+ def get_identifier(self) -> str:
+ return "envfiles"
+
+ @classmethod
+ def get_name(self) -> str:
+ return "Environment Files"
+
+ def get_module_preloads(self, module: VnfModule):
+ for env in self.env.environments:
+ module_params = env.get_module(module.label)
+ yield EnvironmentFilePreloadInstance(env, module.label, module_params)
diff --git a/ice_validator/preload/generator.py b/ice_validator/preload/generator.py
index bdd81fa..ffdc420 100644
--- a/ice_validator/preload/generator.py
+++ b/ice_validator/preload/generator.py
@@ -39,9 +39,17 @@ import json
import os
from abc import ABC, abstractmethod
from collections import OrderedDict
+from pathlib import Path
import yaml
+from preload.data import (
+ AbstractPreloadDataSource,
+ AbstractPreloadInstance,
+ BlankPreloadInstance,
+)
+from preload.model import VnfModule, Vnf
+
def represent_ordered_dict(dumper, data):
value = []
@@ -76,26 +84,15 @@ def get_or_create_template(template_dir, key, value, sequence, template_name):
return new_template
-def yield_by_count(sequence):
- """
- Iterates through sequence and yields each item according to its __count__
- attribute. If an item has a __count__ of it will be returned 3 times
- before advancing to the next item in the sequence.
-
- :param sequence: sequence of dicts (must contain __count__)
- :returns: generator of tuple key, value pairs
- """
- for key, value in sequence.items():
- for i in range(value["__count__"]):
- yield (key, value)
-
-
-def replace(param):
+def replace(param, index=None):
"""
Optionally used by the preload generator to wrap items in the preload
that need to be replaced by end users
- :param param: p
+ :param param: parameter name
+ :param index: optional index (int or str) of the parameter
"""
+ if (param.endswith("_names") or param.endswith("_ips")) and index is not None:
+ param = "{}[{}]".format(param, index)
return "VALUE FOR: {}".format(param) if param else ""
@@ -113,15 +110,15 @@ class AbstractPreloadGenerator(ABC):
:param vnf: Instance of Vnf that contains the preload data
:param base_output_dir: Base directory to house the preloads. All preloads
must be written to a subdirectory under this directory
+ :param data_source: Source data for preload population
"""
- def __init__(self, vnf, base_output_dir, preload_env):
- self.preload_env = preload_env
+ def __init__(
+ self, vnf: Vnf, base_output_dir: Path, data_source: AbstractPreloadDataSource
+ ):
+ self.data_source = data_source
self.vnf = vnf
- self.current_module = None
- self.current_module_env = {}
self.base_output_dir = base_output_dir
- self.env_cache = {}
self.module_incomplete = False
@classmethod
@@ -158,11 +155,10 @@ class AbstractPreloadGenerator(ABC):
raise NotImplementedError()
@abstractmethod
- def generate_module(self, module, output_dir):
+ def generate_module(self, module: VnfModule, preload: AbstractPreloadInstance, output_dir: Path):
"""
- Create the preloads and write them to ``output_dir``. This
- method is responsible for generating the content of the preload and
- writing the file to disk.
+ Create the preloads. This method is responsible for generating the
+ content of the preload and writing the file to disk.
"""
raise NotImplementedError()
@@ -170,29 +166,17 @@ class AbstractPreloadGenerator(ABC):
# handle the base module first
print("\nGenerating {} preloads".format(self.format_name()))
if self.vnf.base_module:
- self.generate_environments(self.vnf.base_module)
+ self.generate_preloads(self.vnf.base_module)
if self.supports_output_passing():
self.vnf.filter_base_outputs()
for mod in self.vnf.incremental_modules:
- self.generate_environments(mod)
+ self.generate_preloads(mod)
- def replace(self, param_name, alt_message=None, single=False):
- value = self.get_param(param_name, single)
- value = None if value == "CHANGEME" else value
- if value:
- return value
- else:
- self.module_incomplete = True
- return alt_message or replace(param_name)
-
- def start_module(self, module, env):
+ def start_module(self):
"""Initialize/reset the environment for the module"""
- self.current_module = module
- self.current_module_env = env
self.module_incomplete = False
- self.env_cache = {}
- def generate_environments(self, module):
+ def generate_preloads(self, module):
"""
Generate a preload for the given module in all available environments
in the ``self.preload_env``. This will invoke the abstract
@@ -204,65 +188,50 @@ class AbstractPreloadGenerator(ABC):
print("\nGenerating Preloads for {}".format(module))
print("-" * 50)
print("... generating blank template")
- self.start_module(module, {})
- blank_preload_dir = self.make_preload_dir(self.base_output_dir)
- self.generate_module(module, blank_preload_dir)
- self.generate_preload_env(module, blank_preload_dir)
- if self.preload_env:
- for env in self.preload_env.environments:
- output_dir = self.make_preload_dir(env.base_dir / "preloads")
+ self.start_module()
+ preload = BlankPreloadInstance(Path(self.base_output_dir), module.label)
+ blank_preload_dir = self.make_preload_dir(preload)
+ self.generate_module(module, preload, blank_preload_dir)
+ self.generate_preload_env(module, preload)
+
+ if self.data_source:
+ preloads = self.data_source.get_module_preloads(module)
+ for preload in preloads:
+ output_dir = self.make_preload_dir(preload)
print(
- "... generating preload for env ({}) to {}".format(
- env.name, output_dir
+ "... generating preload for {} to {}".format(
+ preload.module_label, output_dir
)
)
- self.start_module(module, env.get_module(module.label))
- self.generate_module(module, output_dir)
+ self.start_module()
+ self.generate_module(module, preload, output_dir)
- def make_preload_dir(self, base_dir):
- path = os.path.join(base_dir, self.output_sub_dir())
- if not os.path.exists(path):
- os.makedirs(path, exist_ok=True)
- return path
+ def make_preload_dir(self, preload: AbstractPreloadInstance):
+ preload_dir = preload.output_dir.joinpath(self.output_sub_dir())
+ preload_dir.mkdir(parents=True, exist_ok=True)
+ return preload_dir
@staticmethod
- def generate_preload_env(module, blank_preload_dir):
+ def generate_preload_env(module: VnfModule, preload: AbstractPreloadInstance):
"""
Create a .env template suitable for completing and using for
preload generation from env files.
"""
yaml.add_representer(OrderedDict, represent_ordered_dict)
- output_dir = os.path.join(blank_preload_dir, "preload_env")
- env_file = os.path.join(output_dir, "{}.env".format(module.vnf_name))
- defaults_file = os.path.join(output_dir, "defaults.yaml")
- if not os.path.exists(output_dir):
- os.makedirs(output_dir, exist_ok=True)
- with open(env_file, "w") as f:
+ output_dir = preload.output_dir.joinpath("preload_env")
+ env_file = output_dir.joinpath("{}.env".format(module.label))
+ defaults_file = output_dir.joinpath("defaults.yaml")
+ output_dir.mkdir(parents=True, exist_ok=True)
+ with env_file.open("w") as f:
yaml.dump(module.env_template, f)
- if not os.path.exists(defaults_file):
- with open(defaults_file, "w") as f:
+ if not defaults_file.exists():
+ with defaults_file.open("w") as f:
yaml.dump({"vnf_name": "CHANGEME"}, f)
- def get_param(self, param_name, single):
- """
- Retrieves the value for the given param if it exists. If requesting a
- single item, and the parameter is tied to a list then only one item from
- the list will be returned. For each subsequent call with the same parameter
- it will iterate/rotate through the values in that list. If single is False
- then the full list will be returned.
-
- :param param_name: name of the parameter
- :param single: If True returns single value from lists otherwises the full
- list. This has no effect on non-list values
- """
- value = self.env_cache.get(param_name)
- if not value:
- value = self.current_module_env.get(param_name)
- if isinstance(value, list):
- value = value.copy()
- value.reverse()
- self.env_cache[param_name] = value
- if value and single and isinstance(value, list):
- return value.pop()
+ def normalize(self, preload_value, param_name, alt_message=None, index=None):
+ preload_value = None if preload_value == "CHANGEME" else preload_value
+ if preload_value:
+ return preload_value
else:
- return value
+ self.module_incomplete = True
+ return alt_message or replace(param_name, index)
diff --git a/ice_validator/preload/model.py b/ice_validator/preload/model.py
index 3ca7bda..21d849e 100644
--- a/ice_validator/preload/model.py
+++ b/ice_validator/preload/model.py
@@ -35,17 +35,15 @@
#
# ============LICENSE_END============================================
import os
-import shutil
from abc import ABC, abstractmethod
from collections import OrderedDict
+from itertools import chain
+from typing import Tuple, List
-from preload.generator import yield_by_count
-from preload.environment import PreloadEnvironment
from tests.helpers import (
get_param,
get_environment_pair,
prop_iterator,
- get_output_dir,
is_base_module,
remove,
)
@@ -54,7 +52,6 @@ from tests.structures import NeutronPortProcessor, Heat
from tests.test_environment_file_parameters import get_preload_excluded_parameters
from tests.utils import nested_dict
from tests.utils.vm_types import get_vm_type_for_nova_server
-from config import Config, get_generator_plugins
from tests.test_environment_file_parameters import ENV_PARAMETER_SPEC
@@ -133,7 +130,9 @@ class Network(FilterBaseOutputs):
self.subnet_params = set()
def filter_output_params(self, base_outputs):
- self.subnet_params = remove(self.subnet_params, base_outputs)
+ self.subnet_params = remove(
+ self.subnet_params, base_outputs, key=lambda s: s.param_name
+ )
def __hash__(self):
return hash(self.network_role)
@@ -142,12 +141,27 @@ class Network(FilterBaseOutputs):
return hash(self) == hash(other)
+class Subnet:
+ def __init__(self, param_name: str):
+ self.param_name = param_name
+
+ @property
+ def ip_version(self):
+ return 6 if "_v6_" in self.param_name else 4
+
+ def __hash__(self):
+ return hash(self.param_name)
+
+ def __eq__(self, other):
+ return hash(self) == hash(other)
+
+
class Port(FilterBaseOutputs):
def __init__(self, vm, network):
self.vm = vm
self.network = network
self.fixed_ips = []
- self.floating_ips = []
+ self.floating_ips = set()
self.uses_dhcp = True
def add_ips(self, props):
@@ -161,12 +175,35 @@ class Port(FilterBaseOutputs):
self.uses_dhcp = False
self.fixed_ips.append(IpParam(ip_address, self))
if subnet:
- self.network.subnet_params.add(subnet)
+ self.network.subnet_params.add(Subnet(subnet))
for ip in prop_iterator(props, "allowed_address_pairs", "ip_address"):
- self.uses_dhcp = False
param = get_param(ip) if ip else ""
if param:
- self.floating_ips.append(IpParam(param, self))
+ self.floating_ips.add(IpParam(param, self))
+
+ @property
+ def ipv6_fixed_ips(self):
+ return list(
+ sorted(
+ (ip for ip in self.fixed_ips if ip.ip_version == 6),
+ key=lambda ip: ip.param,
+ )
+ )
+
+ @property
+ def ipv4_fixed_ips(self):
+ return list(
+ sorted(
+ (ip for ip in self.fixed_ips if ip.ip_version == 4),
+ key=lambda ip: ip.param,
+ )
+ )
+
+ @property
+ def fixed_ips_with_index(self) -> List[Tuple[int, IpParam]]:
+ ipv4s = enumerate(self.ipv4_fixed_ips)
+ ipv6s = enumerate(self.ipv6_fixed_ips)
+ return list(chain(ipv4s, ipv6s))
def filter_output_params(self, base_outputs):
self.fixed_ips = remove(self.fixed_ips, base_outputs, key=lambda ip: ip.param)
@@ -218,9 +255,10 @@ class VirtualMachineType(FilterBaseOutputs):
class Vnf:
- def __init__(self, templates):
- self.modules = [VnfModule(t, self) for t in templates]
+ def __init__(self, templates, config=None):
+ self.modules = [VnfModule(t, self, config) for t in templates]
self.uses_contrail = self._uses_contrail()
+ self.config = config
self.base_module = next(
(mod for mod in self.modules if mod.is_base_module), None
)
@@ -256,8 +294,9 @@ def env_path(heat_path):
class VnfModule(FilterBaseOutputs):
- def __init__(self, template_file, vnf):
+ def __init__(self, template_file, vnf, config):
self.vnf = vnf
+ self.config = config
self.vnf_name = os.path.splitext(os.path.basename(template_file))[0]
self.template_file = template_file
self.heat = Heat(filepath=template_file, envpath=env_path(template_file))
@@ -265,11 +304,30 @@ class VnfModule(FilterBaseOutputs):
env_yaml = env_pair.get("eyml") if env_pair else {}
self.parameters = {key: "" for key in self.heat.parameters}
self.parameters.update(env_yaml.get("parameters") or {})
+ # Filter out any parameters passed from the volume module's outputs
+ self.parameters = {
+ key: value
+ for key, value in self.parameters.items()
+ if key not in self.volume_module_outputs
+ }
self.networks = []
self.virtual_machine_types = self._create_vm_types()
self._add_networks()
self.outputs_filtered = False
+ @property
+ def volume_module_outputs(self):
+ heat_dir = os.path.dirname(self.template_file)
+ heat_filename = os.path.basename(self.template_file)
+ basename, ext = os.path.splitext(heat_filename)
+ volume_template_name = "{}_volume{}".format(basename, ext)
+ volume_path = os.path.join(heat_dir, volume_template_name)
+ if os.path.exists(volume_path):
+ volume_mod = Heat(filepath=volume_path)
+ return volume_mod.outputs
+ else:
+ return {}
+
def filter_output_params(self, base_outputs):
for vm in self.virtual_machine_types:
vm.filter_output_params(base_outputs)
@@ -329,10 +387,7 @@ class VnfModule(FilterBaseOutputs):
@property
def env_specs(self):
"""Return available Environment Spec definitions"""
- try:
- return Config().env_specs
- except FileNotFoundError:
- return [ENV_PARAMETER_SPEC]
+ return [ENV_PARAMETER_SPEC] if not self.config else self.config.env_specs
@property
def platform_provided_params(self):
@@ -356,7 +411,7 @@ class VnfModule(FilterBaseOutputs):
params[az] = CHANGE
for network in self.networks:
params[network.name_param] = CHANGE
- for param in set(network.subnet_params):
+ for param in set(s.param_name for s in network.subnet_params):
params[param] = CHANGE
for vm in self.virtual_machine_types:
for name in set(vm.names):
@@ -417,40 +472,15 @@ class VnfModule(FilterBaseOutputs):
return hash(self) == hash(other)
-def create_preloads(config, exitstatus):
+def yield_by_count(sequence):
"""
- Create preloads in every format that can be discovered by get_generator_plugins
+ Iterates through sequence and yields each item according to its __count__
+ attribute. If an item has a __count__ of it will be returned 3 times
+ before advancing to the next item in the sequence.
+
+ :param sequence: sequence of dicts (must contain __count__)
+ :returns: generator of tuple key, value pairs
"""
- if config.getoption("self_test"):
- return
- print("+===================================================================+")
- print("| Preload Template Generation |")
- print("+===================================================================+")
-
- preload_dir = os.path.join(get_output_dir(config), "preloads")
- if os.path.exists(preload_dir):
- shutil.rmtree(preload_dir)
- env_directory = config.getoption("env_dir")
- preload_env = PreloadEnvironment(env_directory) if env_directory else None
- plugins = get_generator_plugins()
- available_formats = [p.format_name() for p in plugins]
- selected_formats = config.getoption("preload_formats") or available_formats
- heat_templates = get_heat_templates(config)
- vnf = None
- for plugin_class in plugins:
- if plugin_class.format_name() not in selected_formats:
- continue
- vnf = Vnf(heat_templates)
- generator = plugin_class(vnf, preload_dir, preload_env)
- generator.generate()
- if vnf and vnf.uses_contrail:
- print(
- "\nWARNING: Preload template generation does not support Contrail\n"
- "at this time, but Contrail resources were detected. The preload \n"
- "template may be incomplete."
- )
- if exitstatus != 0:
- print(
- "\nWARNING: Heat violations detected. Preload templates may be\n"
- "incomplete."
- )
+ for key, value in sequence.items():
+ for i in range(value["__count__"]):
+ yield (key, value)