diff --git a/salt/pillar/saltclass.py b/salt/pillar/saltclass.py index bd0be0394931..e50f4155d2cb 100644 --- a/salt/pillar/saltclass.py +++ b/salt/pillar/saltclass.py @@ -48,7 +48,7 @@ def ext_pillar(minion_id, pillar, *args, **kwargs): if 'path' not in i: path = '/srv/saltclass' args[i]['path'] = path - log.warning('path variable unset, using default: %s', path) + log.info('path variable unset, using default: %s', path) else: path = i['path'] diff --git a/salt/utils/saltclass.py b/salt/utils/saltclass.py index e1dfd8bb10f4..616cec0a8c11 100644 --- a/salt/utils/saltclass.py +++ b/salt/utils/saltclass.py @@ -9,15 +9,19 @@ # Import Salt libs import salt.utils.path import salt.utils.yaml +from salt.exceptions import SaltException +from yaml.error import YAMLError # Import 3rd-party libs from salt.ext import six +# No need to invent bicycle +from collections import deque, OrderedDict + log = logging.getLogger(__name__) -# Renders jinja from a template file -def render_jinja(_file, salt_data): +def _render_jinja(_file, salt_data): j_env = Environment(loader=FileSystemLoader(os.path.dirname(_file))) j_env.globals.update({ '__opts__': salt_data['__opts__'], @@ -30,321 +34,422 @@ def render_jinja(_file, salt_data): return j_render -# Renders yaml from rendered jinja -def render_yaml(_file, salt_data): - return salt.utils.yaml.safe_load(render_jinja(_file, salt_data)) - - -# Returns a dict from a class yaml definition -def get_class(_class, salt_data): - l_files = [] - saltclass_path = salt_data['path'] - - straight = os.path.join(saltclass_path, - 'classes', - '{0}.yml'.format(_class)) - sub_straight = os.path.join(saltclass_path, - 'classes', - '{0}.yml'.format(_class.replace('.', os.sep))) - sub_init = os.path.join(saltclass_path, - 'classes', - _class.replace('.', os.sep), - 'init.yml') - - for root, dirs, files in salt.utils.path.os_walk(os.path.join(saltclass_path, 'classes'), followlinks=True): - for l_file in files: - l_files.append(os.path.join(root, l_file)) - - if straight in l_files: - return render_yaml(straight, salt_data) - - if sub_straight in l_files: - return render_yaml(sub_straight, salt_data) - - if sub_init in l_files: - return render_yaml(sub_init, salt_data) - - log.warning('%s: Class definition not found', _class) - return {} - - -# Return environment -def get_env_from_dict(exp_dict_list): - environment = '' - for s_class in exp_dict_list: - if 'environment' in s_class: - environment = s_class['environment'] - return environment +def _render_yaml(_file, salt_data): + result = None + try: + result = salt.utils.yaml.safe_load(_render_jinja(_file, salt_data)) + except YAMLError as e: + log.error('YAML rendering exception for file %s:\n%s', _file, e) + if result is None: + log.warning('Unable to render yaml from %s', _file) + return {} + return result -# Merge dict b into a -def dict_merge(a, b, path=None): +def _dict_merge(m_target, m_object, path=None, reverse=False): + ''' + Merge m_target <---merge-into---- m_object recursively. Override (^) logic here. + ''' if path is None: path = [] - for key in b: - if key in a: - if isinstance(a[key], list) and isinstance(b[key], list): - if b[key][0] == '^': - b[key].pop(0) - a[key] = b[key] + for key in m_object: + if key in m_target: + if isinstance(m_target[key], list) and isinstance(m_object[key], list): + if not reverse: + if m_object[key][0] == '^': + m_object[key].pop(0) + m_target[key] = m_object[key] + else: + m_target[key].extend(m_object[key]) else: - a[key].extend(b[key]) - elif isinstance(a[key], dict) and isinstance(b[key], dict): - dict_merge(a[key], b[key], path + [six.text_type(key)]) - elif a[key] == b[key]: + # In reverse=True mode if target list (from higher level class) + # already has ^ , then we do nothing + if m_target[key] and m_target[key][0] == '^': + pass + # if it doesn't - merge to the beginning + else: + m_target[key][0:0] = m_object[key] + elif isinstance(m_target[key], dict) and isinstance(m_object[key], dict): + _dict_merge(m_target[key], m_object[key], path + [six.text_type(key)], reverse=reverse) + elif m_target[key] == m_object[key]: pass else: - a[key] = b[key] + # If we're here a and b has different types. + # Update in case reverse=True + if not reverse: + m_target[key] = m_object[key] + # And just pass in case reverse=False since key a already has data from higher levels + else: + pass else: - a[key] = b[key] - return a - - -# Recursive search and replace in a dict -def dict_search_and_replace(d, old, new, expanded): - for (k, v) in six.iteritems(d): - if isinstance(v, dict): - dict_search_and_replace(d[k], old, new, expanded) - - if isinstance(v, list): - x = 0 - for i in v: - if isinstance(i, dict): - dict_search_and_replace(v[x], old, new, expanded) - if isinstance(i, six.string_types): - if i == old: - v[x] = new - x = x + 1 - - if v == old: - d[k] = new - - return d - - -# Retrieve original value from ${xx:yy:zz} to be expanded -def find_value_to_expand(x, v): - a = x - for i in v[2:-1].split(':'): - if a is None: - return v - if i in a: - a = a.get(i) + m_target[key] = m_object[key] + return m_target + + +def _get_variable_value(variable, pillar_data): + ''' + Retrieve original value from ${xx:yy:zz} to be expanded + ''' + rv = pillar_data + for i in variable[2:-1].split(':'): + try: + rv = rv[i] + except KeyError: + raise SaltException('Unable to expand {}'.format(variable)) + return rv + + +def _get_variables_from_pillar(text_pillar, escaped=True): + ''' + Get variable names from this pillar. + 'blah blah ${key1}${key2} blah ${key1}' will result in {'${key1}', '${key2}'} + :param text_pillar: string pillar + :param escaped: should we match \${escaped:reference} or ${not} + :return: set of matched substrings from pillar + ''' + matches_iter = re.finditer(r'(\\)?\${.*?}', six.text_type(text_pillar)) + result = set() + if not matches_iter: + pass + for match in matches_iter: + if escaped or not six.text_type(match.group()).startswith('\\'): + result.add(match.group()) + return result + + +def _update_pillar(pillar_path, variable, value, pillar_data): + rv = pillar_data + for key in pillar_path[:-1]: + rv = rv[key] + if isinstance(value, (list, dict)): + if rv[pillar_path[-1]] == variable: + rv[pillar_path[-1]] = value else: - return v - return a - - -# Look for regexes and expand them -def find_and_process_re(_str, v, k, b, expanded): - vre = re.finditer(r'(^|.)\$\{.*?\}', _str) - if vre: - for re_v in vre: - re_str = str(re_v.group()) - if re_str.startswith('\\'): - v_new = _str.replace(re_str, re_str.lstrip('\\')) - b = dict_search_and_replace(b, _str, v_new, expanded) - expanded.append(k) - elif not re_str.startswith('$'): - v_expanded = find_value_to_expand(b, re_str[1:]) - v_new = _str.replace(re_str[1:], v_expanded) - b = dict_search_and_replace(b, _str, v_new, expanded) - _str = v_new - expanded.append(k) - else: - v_expanded = find_value_to_expand(b, re_str) - if isinstance(v, six.string_types): - v_new = v.replace(re_str, v_expanded) - else: - v_new = _str.replace(re_str, v_expanded) - b = dict_search_and_replace(b, _str, v_new, expanded) - _str = v_new - v = v_new - expanded.append(k) - return b - - -# Return a dict that contains expanded variables if found -def expand_variables(a, b, expanded, path=None): - if path is None: - b = a.copy() - path = [] - - for (k, v) in six.iteritems(a): - if isinstance(v, dict): - expand_variables(v, b, expanded, path + [six.text_type(k)]) + raise SaltException('Type mismatch on variable {} expansion'.format(variable)) + else: + rv[pillar_path[-1]] = six.text_type(rv[pillar_path[-1]]).replace(variable, six.text_type(value)) + return rv[pillar_path[-1]] + + +def _find_expandable_pillars(pillar_data, **kwargs): + ''' + Recursively look for variable to expand in nested dicts, lists, strings + :param pillar_data: structure to look in + :return: list of tuples [(path, variable), ... ] where for pillar X:Y:Z path is ['X', 'Y', 'Z'] + and variable is a single ${A:B:C} expression. For a text pillar with several different variables inside + will return several entries in result. + ''' + pillar = kwargs.get('pillar', pillar_data) + path = kwargs.get('path', []) + result = kwargs.get('result', []) + escaped = kwargs.get('escaped', True) + + if isinstance(pillar, dict): + for k, v in pillar.items(): + _find_expandable_pillars(pillar_data=pillar_data, pillar=v, path=path + [k], + result=result, escaped=escaped) + elif isinstance(pillar, list): + # here is the cheapest place to pop orphaned ^ + if len(pillar) > 0 and pillar[0] == '^': + pillar.pop(0) + elif len(pillar) > 0 and pillar[0] == r'\^': + pillar[0] = '^' + for i, elem in enumerate(pillar): + _find_expandable_pillars(pillar_data=pillar_data, pillar=elem, path=path + [i], + result=result, escaped=escaped) + else: + for variable in _get_variables_from_pillar(six.text_type(pillar), escaped): + result.append((path, variable)) + + return result + + +def expand_variables(pillar_data): + ''' + Open every ${A:B:C} variable in pillar_data + ''' + path_var_mapping = _find_expandable_pillars(pillar_data, escaped=False) + # TODO: remove hardcoded 5 into options + for i in range(5): + new_path_var_mapping = [] + for path, variable in path_var_mapping: + # get value of ${A:B:C} + value = _get_variable_value(variable, pillar_data) + + # update pillar '${A:B:C}' -> value of ${A:B:C} + pillar = _update_pillar(path, variable, value, pillar_data) + + # check if we got more expandable variable (in case of nested expansion) + new_variables = _find_expandable_pillars(pillar, escaped=False) + + # update next iteration's variable + new_path_var_mapping.extend([(path + p, v) for p, v in new_variables]) + + # break if didn't find any cases of nested expansion + if not new_path_var_mapping: + break + path_var_mapping = new_path_var_mapping + + return pillar_data + + +def _validate(name, data): + ''' + Make sure classes, pillars, states and environment are of appropriate data types + ''' + # TODO: looks awful, there's a better way to write this + if 'classes' in data: + data['classes'] = [] if data['classes'] is None else data['classes'] # None -> [] + if not isinstance(data['classes'], list): + raise SaltException('Classes in {} is not a valid list'.format(name)) + if 'pillars' in data: + data['pillars'] = {} if data['pillars'] is None else data['pillars'] # None -> {} + if not isinstance(data['pillars'], dict): + raise SaltException('Pillars in {} is not a valid dict'.format(name)) + if 'states' in data: + data['states'] = [] if data['states'] is None else data['states'] # None -> [] + if not isinstance(data['states'], list): + raise SaltException('States in {} is not a valid list'.format(name)) + if 'environment' in data: + data['environment'] = '' if data['environment'] is None else data['environment'] # None -> '' + if not isinstance(data['environment'], six.string_types): + raise SaltException('Environment in {} is not a valid string'.format(name)) + return + + +def _resolve_prefix_glob(prefix_glob, salt_data): + ''' + Resolves prefix globs + ''' + result = [c for c in salt_data['class_paths'].keys() if c.startswith(prefix_glob[:-1])] + + # Concession to usual globbing habits from operating systems: + # include class A.B to result of glob A.B.* resolution + # if the class is defined with <>/classes/A/B/init.yml (but not with <>/classes/A/B.yml!) + # TODO: should we remove this? We already fail hard if there's a B.yml file and B directory in the same path + if prefix_glob.endswith('.*') and salt_data['class_paths'].get(prefix_glob[:-2], '').endswith('/init.yml'): + result.append(prefix_glob[:-2]) + return result + + +def resolve_classes_glob(base_class, glob, salt_data): + ''' + Finds classes for the glob. Can't return other globs. + + :param str base_class: class where glob was found in - we need this information to resolve suffix globs + :param str glob: + - prefix glob - A.B.* or A.B* + - suffix glob - .A.B + - combination of both - .A.B.* + - special - . (single dot) - to address "local" init.yml - the one found in the same directory + :param dict salt_data: salt_data + :return: list of strings or empty list - classes, resolved from the glob + ''' + base_class_init_notation = salt_data['class_paths'].get(base_class, '').endswith('init.yml') + ancestor_class, _, _ = base_class.rpartition('.') + + # If base_class A.B defined with file <>/classes/A/B/init.yml, glob . is ignored + # If base_class A.B defined with file <>/classes/A/B.yml, glob . addresses + # class A if and only if A is defined with <>/classes/A/init.yml. + # I.e. glob . references neighbour init.yml + if glob.strip() == '.': + if base_class_init_notation: + return [] else: - if isinstance(v, list): - for i in v: - if isinstance(i, dict): - expand_variables(i, b, expanded, path + [str(k)]) - if isinstance(i, six.string_types): - b = find_and_process_re(i, v, k, b, expanded) - - if isinstance(v, six.string_types): - b = find_and_process_re(v, v, k, b, expanded) - return b - - -def expand_classes_in_order(minion_dict, - salt_data, - seen_classes, - expanded_classes, - classes_to_expand): - # Get classes to expand from minion dictionary - if not classes_to_expand and 'classes' in minion_dict: - classes_to_expand = minion_dict['classes'] - - # Now loop on list to recursively expand them - for klass in classes_to_expand: - if klass not in seen_classes: - seen_classes.append(klass) - expanded_classes[klass] = get_class(klass, salt_data) - # Fix corner case where class is loaded but doesn't contain anything - if expanded_classes[klass] is None: - expanded_classes[klass] = {} - - # Merge newly found pillars into existing ones - new_pillars = expanded_classes[klass].get('pillars', {}) - if new_pillars: - dict_merge(salt_data['__pillar__'], new_pillars) - - # Now replace class element in classes_to_expand by expansion - if expanded_classes[klass].get('classes'): - l_id = classes_to_expand.index(klass) - classes_to_expand[l_id:l_id] = expanded_classes[klass]['classes'] - expand_classes_in_order(minion_dict, - salt_data, - seen_classes, - expanded_classes, - classes_to_expand) + ancestor_class_init_notation = salt_data['class_paths'].get(ancestor_class, '').endswith('init.yml') + return [ancestor_class] if ancestor_class_init_notation else [] + else: + if not base_class_init_notation: + base_class = ancestor_class + if glob.startswith('.'): + glob = base_class + glob + if glob.endswith('*'): + return _resolve_prefix_glob(glob, salt_data) + else: + return [glob] # if we're here glob is not glob anymore but actual class name + + +def get_saltclass_data(node_data, salt_data): + ''' + Main function. Short explanation of the algorithm for the most curious ones: + - build `salt_data['class_paths']` - OrderedDict( name of class : absolute path to it's file ) sorted by keys + - merge pillars found in node definition into existing from previous ext_pillars + - initialize `classes` deque of class names with data from node + - loop through `classes` until it's not emptied: pop class names `cls` from the end, expand, get nested classes, + resolve globs if needed, put them to the beginning of queue + - since all classes has already been expanded on the previous step, we simply traverse `expanded_classes` dict + as a tree depth-first to build `ordered_class_list` and then it's fairly simple + :return: dict pillars, list classes, list states, str environment + ''' + salt_data['class_paths'] = {} + for dirpath, dirnames, filenames in salt.utils.path.os_walk(os.path.join(salt_data['path'], 'classes'), + followlinks=True): + for filename in filenames: + # Die if there's an X.yml file and X directory in the same path + if filename[:-4] in dirnames: + raise SaltException('Conflict in class file structure - file {}/{} and directory {}/{}. ' + .format(dirpath, filename, dirpath, filename[:-4])) + abs_path = os.path.join(dirpath, filename) + rel_path = abs_path[len(str(os.path.join(salt_data['path'], 'classes' + os.sep))):] + if rel_path.endswith(os.sep + 'init.yml'): + name = str(rel_path[:-len(os.sep + 'init.yml')]).replace(os.sep, '.') else: - expand_classes_in_order(minion_dict, - salt_data, - seen_classes, - expanded_classes, - classes_to_expand) - - # We may have duplicates here and we want to remove them + name = str(rel_path[:-len('.yml')]).replace(os.sep, '.') + salt_data['class_paths'][name] = abs_path + salt_data['class_paths'] = OrderedDict(((k, salt_data['class_paths'][k]) for k in sorted(salt_data['class_paths']))) + + # Merge minion_pillars into salt_data + _dict_merge(salt_data['__pillar__'], node_data.get('pillars', {})) + + # Init classes queue with data from minion + classes = get_node_classes(node_data, salt_data) + + seen_classes = set() + expanded_classes = OrderedDict() + + # Build expanded_classes OrderedDict (we'll need it later) + # and pillars dict for a minion + # At this point classes queue consists only of + while classes: + cls = classes.pop() + # From here on cls is definitely not a glob + seen_classes.add(cls) + cls_filepath = salt_data['class_paths'].get(cls) + if not cls_filepath: + log.warning('%s: Class definition not found', cls) + continue + expanded_class = _render_yaml(cls_filepath, salt_data) + _validate(cls, expanded_class) + expanded_classes[cls] = expanded_class + if 'pillars' in expanded_class and expanded_class['pillars'] is not None: + _dict_merge(salt_data['__pillar__'], expanded_class['pillars'], reverse=True) + if 'classes' in expanded_class: + resolved_classes = [] + for c in reversed(expanded_class['classes']): + if c is not None and isinstance(c, six.string_types): + # Resolve globs + if c.endswith('*') or c.startswith('.'): + classes_from_glob = resolve_classes_glob(cls, c, salt_data) + classes_from_glob_filtered = [n for n in classes_from_glob + if n not in seen_classes and n not in classes] + classes.extendleft(classes_from_glob_filtered) + resolved_classes.extend(reversed(classes_from_glob_filtered)) + elif c not in seen_classes and c not in classes: + classes.appendleft(c) + resolved_classes.append(c) + else: + raise SaltException('Nonstring item in classes list in class {} - {}. '.format(cls, str(c))) + expanded_class['classes'] = resolved_classes[::-1] + + # Get ordered class and state lists from expanded_classes and minion_classes (traverse expanded_classes tree) + def traverse(this_class, result_list): + result_list.append(this_class) + leafs = expanded_classes.get(this_class, {}).get('classes', []) + for leaf in leafs: + traverse(leaf, result_list) + + # Start with node_data classes again, since we need to retain order + ordered_class_list = [] + for cls in get_node_classes(node_data, salt_data): + traverse(cls, ordered_class_list) + + # Remove duplicates tmp = [] - for t_element in classes_to_expand: - if t_element not in tmp: - tmp.append(t_element) - - classes_to_expand = tmp - - # Now that we've retrieved every class in order, - # let's return an ordered list of dicts - ord_expanded_classes = [] - ord_expanded_states = [] - for ord_klass in classes_to_expand: - ord_expanded_classes.append(expanded_classes[ord_klass]) - # And be smart and sort out states list - # Address the corner case where states is empty in a class definition - if 'states' in expanded_classes[ord_klass] and expanded_classes[ord_klass]['states'] is None: - expanded_classes[ord_klass]['states'] = {} - - if 'states' in expanded_classes[ord_klass]: - ord_expanded_states.extend(expanded_classes[ord_klass]['states']) - - # Add our minion dict as final element but check if we have states to process - if 'states' in minion_dict and minion_dict['states'] is None: - minion_dict['states'] = [] - - if 'states' in minion_dict: - ord_expanded_states.extend(minion_dict['states']) - - ord_expanded_classes.append(minion_dict) - - return ord_expanded_classes, classes_to_expand, ord_expanded_states - - -def expanded_dict_from_minion(minion_id, salt_data): - _file = '' - saltclass_path = salt_data['path'] - # Start - for root, dirs, files in salt.utils.path.os_walk(os.path.join(saltclass_path, 'nodes'), followlinks=True): - for minion_file in files: + for cls in reversed(ordered_class_list): + if cls not in tmp: + tmp.append(cls) + ordered_class_list = tmp[::-1] + + # Build state list and get 'environment' variable + ordered_state_list = node_data.get('states', []) + environment = node_data.get('environment', '') + for cls in ordered_class_list: + class_states = expanded_classes.get(cls, {}).get('states', []) + if not environment: + environment = expanded_classes.get(cls, {}).get('environment', '') + for state in class_states: + # Ignore states with override (^) markers in it's names + # Do it here because it's cheaper + if state not in ordered_state_list and state.find('^') == -1: + ordered_state_list.append(state) + + # Expand ${xx:yy:zz} here and pop override (^) markers + salt_data['__pillar__'] = expand_variables(salt_data['__pillar__']) + salt_data['__classes__'] = ordered_class_list + salt_data['__states__'] = ordered_state_list + return salt_data['__pillar__'], salt_data['__classes__'], salt_data['__states__'], environment + + +def get_node_data(minion_id, salt_data): + ''' + Build node_data structure from node definition file + ''' + node_file = '' + for dirpath, _, filenames in salt.utils.path.os_walk(os.path.join(salt_data['path'], 'nodes'), followlinks=True): + for minion_file in filenames: if minion_file == '{0}.yml'.format(minion_id): - _file = os.path.join(root, minion_file) + node_file = os.path.join(dirpath, minion_file) # Load the minion_id definition if existing, else an empty dict - node_dict = {} - if _file: - node_dict[minion_id] = render_yaml(_file, salt_data) - else: - log.warning('%s: Node definition not found', minion_id) - node_dict[minion_id] = {} - - # Merge newly found pillars into existing ones - dict_merge(salt_data['__pillar__'], node_dict[minion_id].get('pillars', {})) - - # Get 2 ordered lists: - # expanded_classes: A list of all the dicts - # classes_list: List of all the classes - expanded_classes, classes_list, states_list = expand_classes_in_order( - node_dict[minion_id], - salt_data, [], {}, []) - - # Here merge the pillars together - pillars_dict = {} - for exp_dict in expanded_classes: - if 'pillars' in exp_dict: - dict_merge(pillars_dict, exp_dict) - return expanded_classes, pillars_dict, classes_list, states_list + if node_file: + result = _render_yaml(node_file, salt_data) + _validate(minion_id, result) + return result + else: + log.info('%s: Node definition not found in saltclass', minion_id) + return {} + + +def get_node_classes(node_data, salt_data): + ''' + Extract classes from node_data structure. Resolve here all globs found in it. Can't do it with resolve_classes_glob + since node globs are more strict and support prefix globs only. + :return: deque with extracted classes + ''' + result = deque() + for c in reversed(node_data.get('classes', [])): + if c.startswith('.'): + raise SaltException('Unsupported glob type in {} - \'{}\'. ' + 'Only A.B* type globs are supported in node definition. ' + .format(salt_data['minion_id'], c)) + elif c.endswith('*'): + resolved_node_glob = _resolve_prefix_glob(c, salt_data) + for resolved_node_class in reversed(sorted(resolved_node_glob)): + result.appendleft(resolved_node_class) + else: + result.appendleft(c) + return result def get_pillars(minion_id, salt_data): - # Get 2 dicts and 2 lists - # expanded_classes: Full list of expanded dicts - # pillars_dict: dict containing merged pillars in order - # classes_list: All classes processed in order - # states_list: All states listed in order - (expanded_classes, - pillars_dict, - classes_list, - states_list) = expanded_dict_from_minion(minion_id, salt_data) - - # Retrieve environment - environment = get_env_from_dict(expanded_classes) - - # Expand ${} variables in merged dict - # pillars key shouldn't exist if we haven't found any minion_id ref - if 'pillars' in pillars_dict: - pillars_dict_expanded = expand_variables(pillars_dict['pillars'], {}, []) - else: - pillars_dict_expanded = expand_variables({}, {}, []) + ''' + :return: dict of pillars with additional meta field __saltclass__ which has info about classes, states, and env + ''' + node_data = get_node_data(minion_id, salt_data) + pillars, classes, states, environment = get_saltclass_data(node_data, salt_data) # Build the final pillars dict - pillars_dict = {} + pillars_dict = dict() pillars_dict['__saltclass__'] = {} - pillars_dict['__saltclass__']['states'] = states_list - pillars_dict['__saltclass__']['classes'] = classes_list + pillars_dict['__saltclass__']['states'] = states + pillars_dict['__saltclass__']['classes'] = classes pillars_dict['__saltclass__']['environment'] = environment pillars_dict['__saltclass__']['nodename'] = minion_id - pillars_dict.update(pillars_dict_expanded) + pillars_dict.update(pillars) return pillars_dict def get_tops(minion_id, salt_data): - # Get 2 dicts and 2 lists - # expanded_classes: Full list of expanded dicts - # pillars_dict: dict containing merged pillars in order - # classes_list: All classes processed in order - # states_list: All states listed in order - (expanded_classes, - pillars_dict, - classes_list, - states_list) = expanded_dict_from_minion(minion_id, salt_data) - - # Retrieve environment - environment = get_env_from_dict(expanded_classes) - - # Build final top dict - tops_dict = {} - tops_dict[environment] = states_list - - return tops_dict + ''' + :return: list of states for a minion + ''' + node_data = get_node_data(minion_id, salt_data) + _, _, states, environment = get_saltclass_data(node_data, salt_data) + + tops = dict() + tops[environment] = states + + return tops diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/A/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/A/init.yml new file mode 100644 index 000000000000..81d2291ba868 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/A/init.yml @@ -0,0 +1,16 @@ +states: + - state_A + +pillars: + same_plaintext_pillar: from_L0A + same_list_pillar: + - L0A-1 + - L0A-2 + L0A: + plaintext: plaintext_from_L0A + list: + - L0A-1 + - L0A-2 + dict: + k1: L0A-1 + k2: L0A-2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/B/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/B/init.yml new file mode 100644 index 000000000000..80e75d0da6a8 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/B/init.yml @@ -0,0 +1,16 @@ +classes: + - L0.B.otherclass + +states: + - state_B + +pillars: + same_plaintext_pillar: from_L0B + same_list_pillar: + - L0B-1 + - L0B-2 + L0B: + plaintext: plaintext_from_L0B + list: + - L0B-1 + - L0B-2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/B/otherclass.yml b/tests/integration/files/saltclass/examples-new/classes/L0/B/otherclass.yml new file mode 100644 index 000000000000..4cee11255ee1 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/B/otherclass.yml @@ -0,0 +1,2 @@ +pillars: + otherclass_pillar: otherclass_pillar diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/C/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/C/init.yml new file mode 100644 index 000000000000..6ca77d9f0923 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/C/init.yml @@ -0,0 +1,23 @@ +states: +{% for version in __pillar__.get('L0C', {}).get('versions', ['default_version']) %} + - state_C.{{ version }} +{% endfor %} + +pillars: + same_plaintext_pillar: from_L0C + same_list_pillar: + - ^ + - L0C-1 + - L0C-2 + L0C: + plaintext: plaintext_from_L0C + list: + - L0C-1 + - L0C-2 + dict: + k1: L0C-1 + k2: L0C-2 + expansion: + plaintext: ${L0C:plaintext} + list: ${L0C:list} + dict: ${L0C:dict} diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/X/X.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/X/X.yml new file mode 100644 index 000000000000..6ab1fa04797b --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/X/X.yml @@ -0,0 +1,12 @@ +classes: + - L0.D.Y.B + - L0.D.Y.A.* + - L0.D.Y.C.C + - L0.D.Y.D.D1 + +pillars: + X: X + +states: + - X + diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/X/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/X/init.yml new file mode 100644 index 000000000000..72c28cce1e0a --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/X/init.yml @@ -0,0 +1,5 @@ +pillars: + X-init: X-init + +states: + - X-init diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A1.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A1.yml new file mode 100644 index 000000000000..53b68fd31f34 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A1.yml @@ -0,0 +1,6 @@ +pillars: + A1: A1 + +states: + - A11 + - A12 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A2.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A2.yml new file mode 100644 index 000000000000..2cd3388107cb --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A2.yml @@ -0,0 +1,5 @@ +pillars: + A2: A2 + +states: + - A2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A3.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A3.yml new file mode 100644 index 000000000000..fe9576bff74e --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/A/A3.yml @@ -0,0 +1,6 @@ +pillars: + A3: A3 + +states: + - A31 + - A32 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/AA/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/AA/init.yml new file mode 100644 index 000000000000..5738f1c5e7c6 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/AA/init.yml @@ -0,0 +1,5 @@ +pillars: + absent: absent + +states: + - absent diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B1.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B1.yml new file mode 100644 index 000000000000..7339128a9dd9 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B1.yml @@ -0,0 +1,8 @@ +classes: + - . + +pillars: + B1: B1 + +states: + - B1 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B2.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B2.yml new file mode 100644 index 000000000000..3eb77f806fd9 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B2.yml @@ -0,0 +1,5 @@ +pillars: + B2: B2 + +states: + - B2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B3.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B3.yml new file mode 100644 index 000000000000..36a338ba84f9 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/B3.yml @@ -0,0 +1,5 @@ +pillars: + B3: B3 + +states: + - B3 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/init.yml new file mode 100644 index 000000000000..a6964eaa96a2 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/B/init.yml @@ -0,0 +1,8 @@ +classes: + - .* + +pillars: + B-init: B-init + +states: + - B-init diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C.yml new file mode 100644 index 000000000000..3c74144320d3 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C.yml @@ -0,0 +1,8 @@ +classes: + - .C1 + +pillars: + C: C + +states: + - C diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C1.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C1.yml new file mode 100644 index 000000000000..660aa8581932 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C1.yml @@ -0,0 +1,8 @@ +classes: + - .C2 + +pillars: + C1: C1 + +states: + - C1 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C2.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C2.yml new file mode 100644 index 000000000000..54f6a5885549 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C2.yml @@ -0,0 +1,5 @@ +pillars: + C2: C2 + +states: + - C2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C3.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C3.yml new file mode 100644 index 000000000000..edf41d897778 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/C/C3.yml @@ -0,0 +1,5 @@ +pillars: + C3: absent + +states: + - absent diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/D1.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/D1.yml new file mode 100644 index 000000000000..d0bcc803136d --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/D1.yml @@ -0,0 +1,8 @@ +classes: + - . + +pillars: + D1: D1 + +states: + - D1 diff --git a/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/init.yml b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/init.yml new file mode 100644 index 000000000000..8f671fdfa03a --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L0/D/Y/D/init.yml @@ -0,0 +1,8 @@ +classes: + - . + +pillars: + D-init: D-init + +states: + - D-init diff --git a/tests/integration/files/saltclass/examples-new/classes/L1/A/init.yml b/tests/integration/files/saltclass/examples-new/classes/L1/A/init.yml new file mode 100644 index 000000000000..7d70fd9eb51f --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L1/A/init.yml @@ -0,0 +1,9 @@ +classes: + - L0.A + - L0.B + +pillars: + single-list-override: + - ^ + - 1 + - 2 diff --git a/tests/integration/files/saltclass/examples-new/classes/L1/B/init.yml b/tests/integration/files/saltclass/examples-new/classes/L1/B/init.yml new file mode 100644 index 000000000000..d07647799816 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L1/B/init.yml @@ -0,0 +1,2 @@ +classes: + - L0.B diff --git a/tests/integration/files/saltclass/examples-new/classes/L1/C/init.yml b/tests/integration/files/saltclass/examples-new/classes/L1/C/init.yml new file mode 100644 index 000000000000..a27dea8878e2 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L1/C/init.yml @@ -0,0 +1,2 @@ +classes: + - L0.C diff --git a/tests/integration/files/saltclass/examples-new/classes/L2/A/init.yml b/tests/integration/files/saltclass/examples-new/classes/L2/A/init.yml new file mode 100644 index 000000000000..bbaf63d535f0 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L2/A/init.yml @@ -0,0 +1,12 @@ +environment: customenv + +classes: + - L1.A + - L1.B + +# watch L0.B pillars - it was included in the tree twice, +# but it's pillars must not be doubled! + +pillars: + fake_id1_list_pillar: + - from_L2A diff --git a/tests/integration/files/saltclass/examples-new/classes/L2/B/init.yml b/tests/integration/files/saltclass/examples-new/classes/L2/B/init.yml new file mode 100644 index 000000000000..89fb5860099e --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/L2/B/init.yml @@ -0,0 +1,12 @@ +environment: test + +classes: + - L1.B + - L1.C + +pillars: + L0C: + versions: + - ^ + - 1 + - 9999 diff --git a/tests/integration/files/saltclass/examples-new/classes/M0/A.yml b/tests/integration/files/saltclass/examples-new/classes/M0/A.yml new file mode 100644 index 000000000000..6b0a26ad1b97 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/M0/A.yml @@ -0,0 +1,8 @@ +states: + - A + +classes: + - .Z + +pillars: + list: [] diff --git a/tests/integration/files/saltclass/examples-new/classes/M0/B.yml b/tests/integration/files/saltclass/examples-new/classes/M0/B.yml new file mode 100644 index 000000000000..f48aca83b441 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/M0/B.yml @@ -0,0 +1,2 @@ +states: + - B diff --git a/tests/integration/files/saltclass/examples-new/classes/M0/Z.yml b/tests/integration/files/saltclass/examples-new/classes/M0/Z.yml new file mode 100644 index 000000000000..dc1d1189175c --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/classes/M0/Z.yml @@ -0,0 +1,7 @@ +states: + - Z + +pillars: + list: + - foo + - bar diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id1.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id1.yml new file mode 100644 index 000000000000..6131dfd5af95 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id1.yml @@ -0,0 +1,6 @@ +classes: + - L2.A + +pillars: + fake_id1_list_pillar: + - from_fake_id1 diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id2.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id2.yml new file mode 100644 index 000000000000..242564346512 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id2.yml @@ -0,0 +1,2 @@ +classes: + - L2.B diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id3.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id3.yml new file mode 100644 index 000000000000..596af648f7a2 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id3.yml @@ -0,0 +1,7 @@ +classes: + +pillars: + +states: + +environment: diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id4.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id4.yml new file mode 100644 index 000000000000..fdf01a38bef6 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id4.yml @@ -0,0 +1,8 @@ +pillars: + plaintext_with_override: saltclass + list: + - 3 + dict: + b: 2 + c: 1 + diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id5.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id5.yml new file mode 100644 index 000000000000..ad143b63f299 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id5.yml @@ -0,0 +1 @@ +pillars: rubbish diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id6.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id6.yml new file mode 100644 index 000000000000..8d1d32165a52 --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id6.yml @@ -0,0 +1,4 @@ +classes: + - L0.D.X.* + +environment: base diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id7.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id7.yml new file mode 100644 index 000000000000..3666a0be099f --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id7.yml @@ -0,0 +1,3 @@ +pillars: + _exists: _exists + nonexistent: 'blah ${_exists} blah ${fakepillar}' diff --git a/tests/integration/files/saltclass/examples-new/nodes/fake_id8.yml b/tests/integration/files/saltclass/examples-new/nodes/fake_id8.yml new file mode 100644 index 000000000000..a986766ddded --- /dev/null +++ b/tests/integration/files/saltclass/examples-new/nodes/fake_id8.yml @@ -0,0 +1,35 @@ +classes: + - M0.A + - M0.B + +pillars: + key1: abc + key2: def + key3: ${key1}${key2} + key4: 'key1: ${key1}, key2: ${key2}, key3: ${key3} \${nonexistent}' + network: + interfaces: + eth0: + ipaddr: 1.1.1.1 + iptables: + rules: + - '-A PREROUTING -s 1.2.3.4/32 -d ${network:interfaces:eth0:ipaddr} -p tcp --dport 1234 5.6.7.8:1234' + nested-expansion: "${X:X}${Y:Y}" + X: + X: "${X:head}${X:tail}" + head: ab + tail: ra + Y: + Y: "${Y:head}${Y:tail}" + head: ca + tail: "${Y:aux:part1}${Y:aux:part2}" + aux: + part1: dab + part2: ra + some: + tree: + pillar: ${nested-expansion} + A: + B: + C: ${some} + other_pillar: ${A} diff --git a/tests/unit/pillar/test_saltclass.py b/tests/unit/pillar/test_saltclass.py index 3df84804cf98..749a9780f19d 100644 --- a/tests/unit/pillar/test_saltclass.py +++ b/tests/unit/pillar/test_saltclass.py @@ -11,67 +11,237 @@ # Import Salt Libs import salt.pillar.saltclass as saltclass - +from salt.exceptions import SaltException base_path = os.path.dirname(os.path.realpath(__file__)) -fake_minion_id = 'fake_id' +fake_minion_id1 = 'fake_id1' +fake_minion_id2 = 'fake_id2' +fake_minion_id3 = 'fake_id3' +fake_minion_id4 = 'fake_id4' +fake_minion_id5 = 'fake_id5' +fake_minion_id6 = 'fake_id6' +fake_minion_id7 = 'fake_id7' +fake_minion_id8 = 'fake_id8' + fake_pillar = {} fake_args = ({'path': os.path.abspath( - os.path.join(base_path, '..', '..', 'integration', - 'files', 'saltclass', 'examples'))}) + os.path.join(base_path, '..', '..', 'integration', + 'files', 'saltclass', 'examples-new'))}) fake_opts = {} fake_salt = {} fake_grains = {} @skipIf(NO_MOCK, NO_MOCK_REASON) -class SaltclassPillarTestCase(TestCase, LoaderModuleMockMixin): +class SaltclassTestCase(TestCase, LoaderModuleMockMixin): ''' Tests for salt.pillar.saltclass + TODO: change node and class names in mocks to make them more readable - all these A, B, C, X, L0 are unreadable ''' + def setup_loader_modules(self): return {saltclass: {'__opts__': fake_opts, '__salt__': fake_salt, '__grains__': fake_grains}} - def _runner(self, expected_ret): - try: - full_ret = saltclass.ext_pillar(fake_minion_id, fake_pillar, fake_args) - parsed_ret = full_ret['__saltclass__']['classes'] - # Fail the test if we hit our NoneType error - except TypeError as err: - self.fail(err) - # Else give the parsed content result - self.assertListEqual(parsed_ret, expected_ret) + def test_simple_case(self): + expected_result = { + 'L0A': + { + 'dict': + {'k1': 'L0A-1', 'k2': 'L0A-2'}, + 'list': ['L0A-1', 'L0A-2'], + 'plaintext': u'plaintext_from_L0A' + }, + 'L0B': + { + 'list': ['L0B-1', 'L0B-2'], + 'plaintext': 'plaintext_from_L0B' + } + } + result = saltclass.ext_pillar(fake_minion_id1, {}, fake_args) + filtered_result = {k: result[k] for k in ('L0A', 'L0B') if k in result} + self.assertDictEqual(filtered_result, expected_result) - def test_succeeds(self): - ret = ['default.users', 'default.motd', 'default.empty', 'default', 'roles.app'] - self._runner(ret) + def test_metainformation(self): + expected_result = { + '__saltclass__': {'classes': ['L2.A', + 'L1.A', + 'L0.A', + 'L1.B', + 'L0.B', + 'L0.B.otherclass'], + 'environment': 'customenv', + 'nodename': 'fake_id1', + 'states': ['state_A', 'state_B']} + } + result = saltclass.ext_pillar(fake_minion_id1, {}, fake_args) + filtered_result = {'__saltclass__': result.get('__saltclass__')} + self.assertDictEqual(filtered_result, expected_result) + def test_plaintext_pillar_overwrite(self): + expected_result = { + 'same_plaintext_pillar': 'from_L0B' + } + result = saltclass.ext_pillar(fake_minion_id1, {}, fake_args) + filtered_result = {'same_plaintext_pillar': result.get('same_plaintext_pillar')} + self.assertDictEqual(filtered_result, expected_result) -@skipIf(NO_MOCK, NO_MOCK_REASON) -class SaltclassPillarTestCaseListExpansion(TestCase, LoaderModuleMockMixin): - ''' - Tests for salt.pillar.saltclass variable expansion in list - ''' - def setup_loader_modules(self): - return {saltclass: {'__opts__': fake_opts, - '__salt__': fake_salt, - '__grains__': fake_grains - }} - - def _runner(self, expected_ret): - full_ret = {} - parsed_ret = [] - try: - full_ret = saltclass.ext_pillar(fake_minion_id, fake_pillar, fake_args) - parsed_ret = full_ret['test_list'] - # Fail the test if we hit our NoneType error - except TypeError as err: - self.fail(err) - # Else give the parsed content result - self.assertListEqual(parsed_ret, expected_ret) - - def test_succeeds(self): - ret = [{'a': '192.168.10.10'}, '192.168.10.20'] - self._runner(ret) + def test_list_pillar_extension(self): + expected_result = { + 'same_list_pillar': ['L0A-1', 'L0A-2', 'L0B-1', 'L0B-2'] + } + result = saltclass.ext_pillar(fake_minion_id1, {}, fake_args) + filtered_result = {'same_list_pillar': result.get('same_list_pillar')} + self.assertDictEqual(filtered_result, expected_result) + + def test_list_override_with_no_ancestor(self): + expected_result = { + 'single-list-override': [1, 2] + } + result = saltclass.ext_pillar(fake_minion_id1, {}, fake_args) + filtered_result = {'single-list-override': result.get('single-list-override')} + self.assertDictEqual(filtered_result, expected_result) + + def test_list_override(self): + expected_result = { + 'same_list_pillar': ['L0C-1', 'L0C-2'] + } + result = saltclass.ext_pillar(fake_minion_id2, {}, fake_args) + filtered_result = {'same_list_pillar': result.get('same_list_pillar')} + self.assertDictEqual(filtered_result, expected_result) + + def test_pillar_expansion(self): + expected_result = { + 'expansion': + { + 'dict': {'k1': 'L0C-1', 'k2': 'L0C-2'}, + 'list': ['L0C-1', 'L0C-2'], + 'plaintext': 'plaintext_from_L0C' + } + } + result = saltclass.ext_pillar(fake_minion_id2, {}, fake_args) + filtered_result = {'expansion': result.get('expansion')} + self.assertDictEqual(filtered_result, expected_result) + + def test_pillars_in_jinja(self): + expected_states = ['state_B', 'state_C.1', 'state_C.9999'] + result = saltclass.ext_pillar(fake_minion_id2, {}, fake_args) + filtered_result = result['__saltclass__']['states'] + for v in expected_states: + self.assertIn(v, filtered_result) + + def test_cornercases(self): + expected_result = {'nonsaltclass_pillar': 'value'} + result = saltclass.ext_pillar(fake_minion_id3, {'nonsaltclass_pillar': 'value'}, fake_args) + filtered_result = {'nonsaltclass_pillar': result.get('nonsaltclass_pillar')} + self.assertDictEqual(filtered_result, expected_result) + + def test_nonsaltclass_pillars(self): + nonsaltclass_pillars = { + 'plaintext_no_override': 'not_from_saltclass', + 'plaintext_with_override': 'not_from_saltclass', + 'list': [1, 2], + 'dict': { + 'a': 1, + 'b': 1 + } + } + expected_result = { + 'plaintext_no_override': 'not_from_saltclass', + 'plaintext_with_override': 'saltclass', + 'list': [1, 2, 3], + 'dict': { + 'a': 1, + 'b': 2, + 'c': 1 + } + } + result = saltclass.ext_pillar(fake_minion_id4, nonsaltclass_pillars, fake_args) + filtered_result = {} + for key in expected_result: + filtered_result[key] = result.get(key) + self.assertDictEqual(filtered_result, expected_result) + + def test_wrong_yaml_format(self): + self.assertRaisesRegex(SaltException, r'^Pillars in fake_id5 is not a valid dict$', + saltclass.ext_pillar, fake_minion_id5, {}, fake_args) + + def test_globbing(self): + result = saltclass.ext_pillar(fake_minion_id6, {}, fake_args) + expected_result = {'A1': 'A1', + 'A2': 'A2', + 'A3': 'A3', + 'B-init': 'B-init', + 'B1': 'B1', + 'B2': 'B2', + 'B3': 'B3', + 'C': 'C', + 'C1': 'C1', + 'C2': 'C2', + 'D-init': 'D-init', + 'D1': 'D1', + 'X': 'X', + 'X-init': 'X-init', + '__saltclass__': {'classes': ['L0.D.X', + 'L0.D.X.X', + 'L0.D.Y.B', + 'L0.D.Y.B.B1', + 'L0.D.Y.B.B2', + 'L0.D.Y.B.B3', + 'L0.D.Y.A.A1', + 'L0.D.Y.A.A2', + 'L0.D.Y.A.A3', + 'L0.D.Y.C.C', + 'L0.D.Y.C.C1', + 'L0.D.Y.C.C2', + 'L0.D.Y.D.D1', + 'L0.D.Y.D'], + 'environment': 'base', + 'nodename': 'fake_id6', + 'states': ['X-init', + 'X', + 'B-init', + 'B1', + 'B2', + 'B3', + 'A11', + 'A12', + 'A2', + 'A31', + 'A32', + 'C', + 'C1', + 'C2', + 'D1', + 'D-init']}} + self.assertDictEqual(result, expected_result) + + def test_failed_expansion(self): + self.assertRaisesRegex(SaltException, r'^Unable to expand \${fakepillar}$', + saltclass.ext_pillar, fake_minion_id7, {}, fake_args) + + def test_complex_expansion(self): + result = saltclass.ext_pillar(fake_minion_id8, {}, fake_args) + expected_result = {'A': {'B': {'C': {'tree': {'pillar': 'abracadabra'}}}}, + 'X': {'X': 'abra', 'head': 'ab', 'tail': 'ra'}, + 'Y': {'Y': 'cadabra', + 'aux': {'part1': 'dab', 'part2': 'ra'}, + 'head': 'ca', + 'tail': 'dabra'}, + '__saltclass__': {'classes': ['M0.A', 'M0.Z', 'M0.B'], + 'environment': '', + 'nodename': 'fake_id8', + 'states': ['A', 'Z', 'B']}, + 'key1': 'abc', + 'key2': 'def', + 'key3': 'abcdef', + 'key4': 'key1: abc, key2: def, key3: abcdef \\${nonexistent}', + 'list': ['foo', 'bar'], + 'nested-expansion': 'abracadabra', + 'network': {'interfaces': {'eth0': {'ipaddr': '1.1.1.1'}}, + 'iptables': {'rules': [ + '-A PREROUTING -s 1.2.3.4/32 -d 1.1.1.1 -p tcp --dport 1234 5.6.7.8:1234']}}, + 'other_pillar': {'B': {'C': {'tree': {'pillar': 'abracadabra'}}}}, + 'some': {'tree': {'pillar': 'abracadabra'}}} + self.assertDictEqual(result, expected_result) diff --git a/tests/unit/tops/test_saltclass.py b/tests/unit/tops/test_saltclass.py new file mode 100644 index 000000000000..d11f787d4ce9 --- /dev/null +++ b/tests/unit/tops/test_saltclass.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Import python libs +from __future__ import absolute_import, print_function, unicode_literals +import os + +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.unit import TestCase, skipIf +from tests.support.mock import NO_MOCK, NO_MOCK_REASON + +# Import Salt Libs +import salt.tops.saltclass as saltclass + +base_path = os.path.dirname(os.path.realpath(__file__)) +fake_minion = 'fake_id2' + +fake_pillar = {} +fake_path = os.path.abspath(os.path.join( + base_path, '..', '..', 'integration', 'files', 'saltclass', 'examples-new')) +fake_opts = {} +fake_salt = {} +fake_grains = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class SaltclassTestCase(TestCase, LoaderModuleMockMixin): + ''' + New tests for salt.pillar.saltclass + ''' + + def setup_loader_modules(self): + return { + saltclass: { + '__opts__': { + 'master_tops': { + 'saltclass': { + 'path': fake_path + } + } + } + } + } + + def test_saltclass_tops(self): + expected_result = ['state_B', 'state_C.1', 'state_C.9999'] + result = saltclass.top(opts={'id': fake_minion}, grains={}) + filtered_result = result['test'] + self.assertEqual(filtered_result, expected_result)