def get_environment_pair(heat_template): """Returns a yaml/env pair given a yaml file""" base_dir, filename = os.path.split(heat_template) basename = os.path.splitext(filename)[0] env_template = os.path.join(base_dir, "{}.env".format(basename)) if os.path.exists(env_template): with open(heat_template, "r") as fh: yyml = yaml.load(fh) with open(env_template, "r") as fh: eyml = yaml.load(fh) environment_pair = {"name": basename, "yyml": yyml, "eyml": eyml} return environment_pair return None
def test_get_file_only_reference_local_files(yaml_file): """ Make sure that all references to get_file only try to access local files and only assume a flat directory structure """ is_url = re.compile(r"(?:http|https|file|ftp|ftps)://.+") base_dir, filename = path.split(yaml_file) with open(yaml_file) as fh: yml = yaml.load(fh) # skip if parameters are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") get_files = find_all_get_file_in_yml(yml["resources"]) invalid_files = [] for get_file in get_files: if is_url.match(get_file): pytest.skip("external get_file detected") continue if get_file not in listdir(base_dir): invalid_files.append(get_file) continue assert not set( invalid_files), "Non-local files detected in get_file {}".format( invalid_files)
def test_volume_templates_contains_cinder_or_resource_group(volume_template): """ Check that all templates marked as volume templates are in fact volume templates """ acceptable_resources = [] dirname = os.path.dirname(volume_template) list_of_files = get_list_of_nested_files(volume_template, dirname) list_of_files.append(volume_template) for file in list_of_files: with open(file) as fh: yml = yaml.load(fh) resources = yml.get("resources") or {} for k, v in resources.items(): if not isinstance(v, dict): continue if "type" not in v: continue if v["type"] in ["OS::Cinder::Volume", "OS::Heat::ResourceGroup"]: acceptable_resources.append(k) assert acceptable_resources, ( "No OS::Cinder::Volume or OS::Heat::ResourceGroup resources " "found in volume module")
def test_environment_context(yaml_file): """ A VNF's Heat Orchestration Template's OS::Nova::Server Resource **MUST** contain the metadata map value parameter 'environment_context'. A VNF's Heat Orchestration Template's OS::Nova::Server Resource metadata map value parameter 'environment_context' **MUST** be declared as type: 'string'. """ with open(yaml_file) as fh: yml = yaml.load(fh) if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") if "resources" not in yml: pytest.skip("No resources specified in the heat template") for resource, v in yml["resources"].items(): if (not isinstance(v, dict) or v.get("type") != "OS::Nova::Server" or "properties" not in v): continue metadata = v["properties"].get("metadata") if not isinstance(metadata, dict): continue error = validate_metadata(metadata, yml["parameters"]) if error: assert False, '%s resource "%s" %s' % (yaml_file, resource, error)
def test_unique_resources_across_all_yaml_files(yaml_files): """ Check that all instance names are unique across all yaml files. """ resources_ids = collections.defaultdict(set) for yaml_file in yaml_files: with open(yaml_file) as fh: yml = yaml.load(fh) if "resources" not in yml: continue for resource_id in yml["resources"]: resources_ids[resource_id].add(os.path.split(yaml_file)[1]) dup_ids = { r_id: files for r_id, files in resources_ids.items() if len(files) > 1 } msg = "The following resource IDs are duplicated in one or more files: " errors = [ "ID ({}) appears in {}.".format(r_id, ", ".join(files)) for r_id, files in dup_ids.items() ] msg += ", ".join(errors) assert not dup_ids, msg
def test_network_resource_id_format(yaml_file): """ Make sure all network resource ids use the allowed naming convention """ RE_INTERNAL_NETWORK_RID = re.compile( # match pattern r"int_(?P<network_role>.+)_network$") with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") invalid_networks = [] for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if "properties" not in v: continue if property_uses_get_resource(v, "network"): continue if v.get("type") not in NETWORK_RESOURCE_TYPES: continue match = RE_INTERNAL_NETWORK_RID.match(k) if not match: invalid_networks.append(k) assert not set(invalid_networks), ( "Heat templates must only create internal networks " "and follow format int_{{network-role}}_network" "{}".format(", ".join(invalid_networks)))
def test_heat_template_parameters_contain_required_fields(yaml_file): """ Check that all parameters in the environment file have the required fields """ required_keys = {"type", "description"} with open(yaml_file) as fh: yml = yaml.load(fh) # skip if parameters are not defined if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") invalid_params = defaultdict(list) for param, param_attrs in yml["parameters"].items(): if not isinstance(param_attrs, dict): continue for key in required_keys: if key not in param_attrs: invalid_params[param].append(key) msg = [ "Parameter {} is missing required attribute(s): {}".format( k, ", ".join(v)) for k, v in invalid_params.items() ] msg = ". ".join(msg) assert not invalid_params, msg
def test_servers_have_required_metadata(yaml_file): """ Check all defined nova server instances have the required metadata: vnf_id, vf_module_id, and vnf_name """ with open(yaml_file) as fh: yml = yaml.load(fh) if "resources" not in yml: pytest.skip("No resources specified in the heat template") required_metadata = {"vnf_id", "vf_module_id", "vnf_name"} errors = [] for k, v in yml["resources"].items(): if v.get("type") != "OS::Nova::Server": continue if "properties" not in v: continue if "metadata" not in v["properties"]: continue metadata = set(v.get("properties", {}).get("metadata", {}).keys()) missing_metadata = required_metadata.difference(metadata) if missing_metadata: msg_template = ("OS::Nova::Server {} is missing the following " + "metadata properties: {}") errors.append(msg_template.format(k, missing_metadata)) assert not errors, "\n".join(errors)
def test_vm_type_consistent_on_nova_servers(yaml_file): """ Make sure all nova servers have properly formatted properties for their name, image and flavor """ with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") invalid_nova_servers = [] for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if v.get("type") != "OS::Nova::Server": continue if "properties" not in v: continue vm_types = get_vm_types_for_resource(v) if len(vm_types) != 1: invalid_nova_servers.append(k) assert not set( invalid_nova_servers ), "vm_types not consistant on the following resources: {}".format( ",".join(invalid_nova_servers))
def test_vm_type_assignments_on_nova_servers_only_use_get_param(yaml_file): """ Make sure all nova servers only use get_param for their properties """ with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") key_values = ["name", "flavor", "image"] invalid_nova_servers = set() for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if "properties" not in v: continue if "type" not in v: continue if v["type"] != "OS::Nova::Server": continue for k2, v2 in v["properties"].items(): if k2 in key_values: if not isinstance(v2, dict): invalid_nova_servers.add(k) elif "get_param" not in v2: invalid_nova_servers.add(k) msg = ( "These OS::Nova::Server resources do not derive one or more of " + "their {} properties via get_param: {}" ).format(", ".join(key_values), ", ".join(invalid_nova_servers)) assert not invalid_nova_servers, msg
def check_nova_parameter_format(prop, yaml_file): formats = { "string": { "name": re.compile(r"(.+?)_name_\d+$"), "flavor": re.compile(r"(.+?)_flavor_name$"), "image": re.compile(r"(.+?)_image_name$"), }, "comma_delimited_list": { "name": re.compile(r"(.+?)_names$") }, } with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") # skip if resources are not defined if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") invalid_parameters = [] for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if v.get("type") != "OS::Nova::Server": continue prop_val = v.get("properties", {}).get(prop, {}) prop_param = prop_val.get("get_param", "") if isinstance( prop_val, dict) else "" if not prop_param: pytest.skip("{} doesn't have property {}".format(k, prop)) elif isinstance(prop_param, list): prop_param = prop_param[0] template_param_type = yml.get("parameters", {}).get(prop_param, {}).get("type") if not template_param_type: pytest.skip( "could not determine param type for {}".format(prop_param)) format_match = formats.get(template_param_type, {}).get(prop) if not format_match or not format_match.match(prop_param): msg = ("Invalid parameter format ({}) on Resource ID ({}) property" " ({})").format(prop_param, k, prop) invalid_parameters.append(msg) assert not set(invalid_parameters), ", ".join(invalid_parameters)
def parametrize_heat_volume_pair(metafunc): """ Define a list of pairs of parsed yaml from the a heat and volume template """ pairs = [] if metafunc.config.getoption("self_test"): sub_dirs = ["pass", "fail"] volume_files = list_template_dir( metafunc, [".yaml", ".yml"], True, "volume", sub_dirs ) yaml_files = list_template_dir(metafunc, [".yaml", ".yml"], True, "", sub_dirs) else: volume_files = list_template_dir(metafunc, [".yaml", ".yml"], True, "volume") yaml_files = list_template_dir(metafunc, [".yaml", ".yml"], True) pattern = re.compile(r"\_volume$") for vfilename in volume_files: basename = pattern.sub("", path.splitext(vfilename)[0]) if basename + ".yml" in yaml_files: yfilename = basename + ".yml" else: yfilename = basename + ".yaml" try: with open(vfilename) as fh: vyml = yaml.load(fh) with open(yfilename) as fh: yyml = yaml.load(fh) if "fail" in vfilename: pairs.append( pytest.mark.xfail( {"name": basename, "yyml": yyml, "vyml": vyml}, strict=True ) ) else: pairs.append({"name": basename, "yyml": yyml, "vyml": vyml}) except yaml.YAMLError as e: print(e) # pylint: disable=superfluous-parens metafunc.parametrize("heat_volume_pair", pairs)
def load_yaml(yaml_file): """ Load the YAML file at the given path. If the file has previously been loaded, then a cached version will be returned. :param yaml_file: path to the YAML file :return: data structure loaded from the YAML file """ with open(yaml_file) as fh: return yaml.load(fh)
def test_environment_file_contains_required_sections(env_file): """ Check that all environments files only have the allowed sections """ required_keys = ["parameters"] with open(env_file) as fh: yml = yaml.load(fh) missing_keys = [v for v in required_keys if v not in yml] assert not missing_keys, "%s missing %s" % (env_file, missing_keys)
def parametrize_environment_pair(metafunc, template_type=""): """ Define a list of pairs of parsed yaml from the heat templates and environment files """ pairs = [] if metafunc.config.getoption("self_test"): sub_dirs = ["pass", "fail"] env_files = list_template_dir(metafunc, [".env"], True, template_type, sub_dirs) yaml_files = list_template_dir( metafunc, [".yaml", ".yml"], True, template_type, sub_dirs ) else: env_files = list_template_dir(metafunc, [".env"], True, template_type) yaml_files = list_template_dir(metafunc, [".yaml", ".yml"], True, template_type) for filename in env_files: basename = path.splitext(filename)[0] if basename + ".yml" in yaml_files: yfilename = basename + ".yml" else: yfilename = basename + ".yaml" try: with open(filename) as fh: eyml = yaml.load(fh) with open(yfilename) as fh: yyml = yaml.load(fh) if "fail" in filename: pairs.append( pytest.mark.xfail( {"name": basename, "yyml": yyml, "eyml": eyml}, strict=True ) ) else: pairs.append({"name": basename, "yyml": yyml, "eyml": eyml}) except yaml.YAMLError as e: print(e) # pylint: disable=superfluous-parens metafunc.parametrize("environment_pair", pairs)
def test_env_no_resource_registry(env_files): """ A VNF's Heat Orchestration template's Environment File's **MUST NOT** contain the "resource_registry:" section. """ for filename in env_files: with open(filename) as fi: yml = yaml.load(fi) assert "resource_registry" not in yml, ( '%s contains "resource_registry"' % filename )
def check_parameters_no_constraints(yaml_file, parameter): with open(yaml_file) as fh: yml = yaml.load(fh) param = yml.get("parameters", {}).get(parameter) if not param: pytest.skip("Parameter {} not defined in parameters section".format(parameter)) assert ( "constraints" not in param ), "Found constraints defined for parameter: {}".format(parameter)
def test_heat_template_structure_contains_resources(heat_template): """ Check that all heat templates have the required sections """ required_key_values = ["resources"] with open(heat_template) as fh: yml = yaml.load(fh) assert all([ k in yml for k in required_key_values ]), "{} doesn't contain the {} section, but it is required".format( heat_template, required_key_values[0])
def test_volume_outputs_consumed(template_dir, volume_template): """ Check that all outputs in a volume template is consumed by the corresponding heat template """ pair_module = VolumePairModule(volume_template) if not pair_module.exists: pytest.skip("No pair module found for volume template") with open(volume_template, "r") as f: volume = yaml.load(f) with open(pair_module.get_module_path(), "r") as f: pair = yaml.load(f) outputs = set(volume.get("outputs", {}).keys()) parameters = set(pair.get("parameters", {}).keys()) missing_output_parameters = outputs.difference(parameters) assert not missing_output_parameters, ( "The output parameters ({}) in {} were not all " "used by the expected module {}".format( ",".join(missing_output_parameters), volume_template, pair_module)) # Now make sure that none of the output parameters appear in any other # template template_files = set(glob.glob("*.yaml")).union(glob.glob(".yml")) errors = {} for template_path in template_files: if template_path in (pair_module, volume_template): continue # Skip these files since we already checked this pair with open(template_path, "r") as f: template = yaml.load(f) parameters = set(template.get("parameters", {}).keys()) misused_outputs = outputs.intersection(parameters) if misused_outputs: errors[template_path] = misused_outputs message = ", ".join("{} ({})".format(path, ", ".join(params)) for path, params in errors.items()) assert not errors, ( "Volume output parameters detected in unexpected modules: " + message)
def run_check_resource_parameter(yaml_file, prop, DESIRED, resource_type, check_resource=True, **kwargs): filepath, filename = os.path.split(yaml_file) environment_pair = get_environment_pair(yaml_file) if not environment_pair: # this is a nested file if not check_resource: # dont check env for nested files # This will be tested separately for parent template pytest.skip("This test doesn't apply to nested files") environment_pair = find_environment_file(yaml_file) if environment_pair: with open(yaml_file, "r") as f: yml = yaml.load(f) environment_pair["yyml"] = yml else: pytest.skip( "unable to determine environment file for nested yaml file") if check_resource: invalid_parameters = check_resource_parameter(environment_pair, prop, DESIRED, resource_type, **kwargs) else: invalid_parameters = check_param_in_env_file(environment_pair, prop, DESIRED) if kwargs.get("resource_type_inverse"): resource_type = "non-{}".format(resource_type) params = (": {}".format(", ".join(invalid_parameters)) if isinstance( invalid_parameters, Iterable) else "") assert not invalid_parameters, ("{} {} parameters in template {}{}" " found in {} environment file{}".format( resource_type, prop, filename, " not" if DESIRED else "", environment_pair.get("name"), params, ))
def check_server_parameter_name(heat_template, parameter, parameter_name): """ Check each OS::Nova::Server metadata property uses the same parameter name w/ get_param """ with open(heat_template) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") invalid_parameters = [] for k1, v1 in yml["resources"].items(): if not isinstance(v1, dict): continue if "type" not in v1: continue if v1["type"] != "OS::Nova::Server": continue metadata = v1.get("properties", {}).get("metadata", {}).get(parameter) if not metadata or not isinstance(metadata, dict): continue get_param = metadata.get("get_param") if not get_param: continue if get_param != parameter_name: invalid_parameters.append( { "resource": k1, "metadata property": parameter_name, "get_param": get_param, } ) assert not invalid_parameters, ( "metadata property {} must use get_param and " "the parameter name must be {}: {}".format( parameter, parameter_name, invalid_parameters ) )
def get_all_vm_types(yaml_files): """ Get all vm_types for a list of yaml files """ vm_types = [] for yaml_file in yaml_files: with open(yaml_file, "r") as f: yml = yaml.load(f) if "resources" not in yml: continue vm_types.extend(get_vm_types(yml["resources"])) return set(vm_types)
def load(self, filepath): """Load the Heat template given a filepath. """ self.filepath = filepath self.basename = os.path.basename(self.filepath) self.dirname = os.path.dirname(self.filepath) with open(self.filepath) as fi: self.yml = yaml.load(fi) self.heat_template_version = self.yml.get("heat_template_version", None) self.description = self.yml.get("description", "") self.parameter_groups = self.yml.get("parameter_groups") or {} self.parameters = self.yml.get("parameters") or {} self.resources = self.yml.get("resources") or {} self.outputs = self.yml.get("outputs") or {} self.conditions = self.yml.get("conditions") or {}
def test_alphanumeric_resource_ids_only(yaml_file): valid_format = re.compile(r"^[\w-]+$") with open(yaml_file) as fh: yml = yaml.load(fh) if "resources" not in yml: pytest.skip("No resources specified in the heat template") invalid_resource_ids = [ k for k in yml["resources"].keys() if not valid_format.match(k) ] msg = "Invalid character(s) detected in the following resource IDs: " + ", ".join( invalid_resource_ids) assert not set(invalid_resource_ids), msg
def test_no_vf_module_index_in_cinder(volume_template): """ vf_module_index is prohibited in volume templates """ with open(volume_template) as fh: yml = yaml.load(fh) if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") parameters = yml.get("parameters") if parameters and isinstance(parameters, dict): assert ("vf_module_index" not in parameters ), "{} must not use vf_module_index as a parameter".format( volume_template)
def test_network_has_subnet(yaml_file): """ if creating internal network, make sure there is a corresponding subnet that references it """ with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") networks = [] for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if "properties" not in v: continue # need to check if contrail networks also require subnet # and it is defined the same as neutron networks # if v.get("type") not in NETWORK_RESOURCE_TYPES: if v.get("type") not in ["OS::Neutron::Net"]: continue networks.append(k) for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if "properties" not in v: continue if v.get("type") != "OS::Neutron::Subnet": continue network_prop = v.get("properties", {}).get("network", {}).get("get_resource") if not network_prop: continue x = 0 for network in networks: if network == network_prop: networks.pop(x) break x += 1 assert not networks, "Networks detected without subnet {}".format(networks)
def test_06_heat_template_resource_section_has_resources(heat_template): found_resource = False with open(heat_template) as fh: yml = yaml.load(fh) resources = yml.get("resources") if resources: for k1, v1 in yml["resources"].items(): if not isinstance(v1, dict): continue found_resource = True break assert found_resource, "Heat templates must contain at least one resource"
def test_parameter_names(yaml_file): """ A VNF's Heat Orchestration Template's parameter name (i.e., <param name>) **MUST** contain only alphanumeric characters and underscores ('_'). """ with open(yaml_file) as fh: yml = yaml.load(fh) # skip if parameters are not defined if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") for key in yml["parameters"]: assert RE_VALID_PARAMETER_NAME.match( key), '%s parameter "%s" not alphanumeric or underscore' % ( yaml_file, key)
def test_neutron_port_network_param_is_string(yaml_file): """ Make sure all network properties use the allowed naming conventions """ with open(yaml_file) as fh: yml = yaml.load(fh) # skip if resources are not defined if "resources" not in yml: pytest.skip("No resources specified in the heat template") # skip if parameters are not defined if "parameters" not in yml: pytest.skip("No parameters specified in the heat template") invalid_ports = [] for k, v in yml["resources"].items(): if not isinstance(v, dict): continue if "properties" not in v: continue if property_uses_get_resource(v, "network"): continue if v.get("type") != "OS::Neutron::Port": continue prop = v.get("properties", {}).get("network", {}) network_param = prop.get("get_param", "") if isinstance(prop, dict) else "" if not network_param: continue param = yml.get("parameters").get(network_param) if not param: continue param_type = param.get("type") if not param_type: continue if param_type != "string": invalid_ports.append({"port": k, "param": network_param}) assert not invalid_ports, "network parameter must be defined as string {} ".format( invalid_ports)
def test_parameter_type(yaml_file): """A VNF's Heat Orchestration Template's parameter type **MUST** be one of the following values: """ types = ["string", "number", "json", "comma_delimited_list", "boolean"] with open(yaml_file) as fh: yml = yaml.load(fh) for key, param in yml.get("parameters", {}).items(): assert isinstance( param, dict), "%s parameter %s is not dict" % (yaml_file, key) if "type" not in param: continue typ = param["type"] assert typ in types, '%s parameter %s has invalid type "%s"' % ( yaml_file, key, typ, )