def indent_checker(filename): with codecs.open(filename, mode='rb', encoding='utf-8') as f: indent_regex = re.compile(r"^(?P<indent>\s*(?:- )?)(?P<rest>.*)$") verb_regex = re.compile(".*: [|>]\d?$") lineno = 0 prev_indent = '' verbatim = False errors = [] for line in f: lineno += 1 match = indent_regex.match(line) if verb_regex.match(line): verbatim = True if len(match.group('rest')) == 0: if verbatim: verbatim = False continue if verbatim: continue curr_indent = match.group('indent') offset = len(curr_indent) - len(prev_indent) if offset > 0 and offset != 2: if match.group('indent').endswith('- '): errors.append( Error( lineno, "lines starting with '- ' should have same " "or less indentation than previous line")) else: errors.append( Error(lineno, "indentation should increase by 2 chars")) prev_indent = curr_indent return errors
def same_variable_defined_in_competing_groups(candidate, options): result = Result(candidate.path) # assume that group_vars file is under an inventory *directory* invfile = os.path.dirname(os.path.dirname(candidate.path)) global _inv try: if ANSIBLE > 1: loader = ansible.parsing.dataloader.DataLoader() try: from ansible.inventory.manager import InventoryManager inv = _inv or InventoryManager(loader=loader, sources=invfile) except ImportError: var_manager = VariableManager() inv = _inv or ansible.inventory.Inventory(loader=loader, variable_manager=var_manager, host_list=invfile) _inv = inv else: inv = _inv or ansible.inventory.Inventory(invfile) _inv = inv except AnsibleError as e: result.errors = [Error(None, "Inventory is broken: %s" % e.message)] return result if hasattr(inv, 'groups'): group = inv.groups.get(os.path.basename(candidate.path)) else: group = inv.get_group(os.path.basename(candidate.path)) if not group: # group file exists in group_vars but no related group # in inventory directory return result remove_inherited_and_overridden_group_vars(group, inv) group_vars = set(_vars[group].keys()) child_hosts = group.hosts child_groups = group.child_groups siblings = set() for child_host in child_hosts: siblings.update(child_host.groups) for child_group in child_groups: siblings.update(child_group.parent_groups) for sibling in siblings: if sibling != group: remove_inherited_and_overridden_group_vars(sibling, inv) sibling_vars = set(_vars[sibling].keys()) common_vars = sibling_vars & group_vars common_hosts = [host.name for host in set(child_hosts) & set(sibling.hosts)] if common_vars and common_hosts: for var in common_vars: error_msg_template = "Sibling groups {0} and {1} with common hosts {2} " + \ "both define variable {3}" error_msg = error_msg_template.format(group.name, sibling.name, ", ".join(common_hosts), var) result.errors.append(Error(None, error_msg)) return result
def same_variable_defined_in_competing_groups(candidate, options): result = Result(candidate.path) vaultpass = get_vault_password(options) # assume that group_vars file is under an inventory *directory* sdirs = candidate.path.split(os.sep) if sdirs.index('group_vars') == 0: invfile = os.getcwd() else: invfile = os.path.join(*sdirs[:sdirs.index('group_vars')]) grpname = os.path.splitext(sdirs[sdirs.index('group_vars') + 1])[0] global _inv try: inv = _inv or parse_inventory(invfile) except AnsibleError as e: result.errors = [Error(None, "Inventory is broken: %s" % e.message)] return result if hasattr(inv, 'groups'): group = inv.groups.get(grpname) else: group = inv.get_group(grpname) if not group: # group file exists in group_vars but no related group # in inventory directory return result remove_inherited_and_overridden_group_vars(group, inv, invfile, vaultpass) group_vars = set(_vars[group].keys()) child_hosts = group.hosts child_groups = group.child_groups siblings = set() for child_host in child_hosts: siblings.update(child_host.groups) for child_group in child_groups: siblings.update(child_group.parent_groups) for sibling in siblings: if sibling != group: remove_inherited_and_overridden_group_vars(sibling, inv, invfile, vaultpass) sibling_vars = set(_vars[sibling].keys()) common_vars = sibling_vars & group_vars common_hosts = [ host.name for host in set(child_hosts) & set(sibling.hosts) ] if common_vars and common_hosts: for var in common_vars: error_msg_template = "Sibling groups {0} and {1} with common hosts {2} " + \ "both define variable {3}" error_msg = error_msg_template.format( group.name, sibling.name, ", ".join(common_hosts), var) result.errors.append(Error(None, error_msg)) return result
def rolesfile_contains_scm_in_src(candidate, settings): result = Result(candidate.path) if candidate.path.endswith(".yml") and os.path.exists(candidate.path): try: with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: roles = parse_yaml_linenumbers(f.read(), candidate.path) for role in roles: if '+' in role.get('src'): error = Error(role['__line__'], "Use scm key rather " "than src: scm+url") result.errors.append(error) except Exception as e: result.errors = [Error(None, "Cannot parse YAML from %s: %s" % (candidate.path, str(e)))] return result
def files_should_have_actual_content(candidate, settings): errors = [] with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: content = yaml.safe_load(f.read()) if not content: errors = [Error(None, "%s appears to have no useful content" % candidate)] return Result(candidate.path, errors)
def parse(candidate, options): result = Result(candidate.path) try: parse_inventory(candidate.path) except Exception as e: result.errors = [Error(None, "Inventory is broken: %s" % e.message)] return result
def yaml_form_rather_than_key_value(candidate, settings): with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: content = parse_yaml_linenumbers(f.read(), candidate.path) errors = [] if content: fileinfo = dict(type=candidate.filetype, path=candidate.path) for task in get_action_tasks(content, fileinfo): normal_form = normalize_task(task, candidate.path) action = normal_form['action']['__ansible_module__'] arguments = normal_form['action']['__ansible_arguments__'] # Cope with `set_fact` where task['set_fact'] is None if not task.get(action): continue if isinstance(task[action], dict): continue # allow skipping based on tag e.g. if using splatting # https://docs.ansible.com/ansible/devel/reference_appendices\ # /faq.html#argsplat-unsafe if 'skip_ansible_lint' in (task.get('tags') or []): continue # strip additional newlines off task[action] if task[action].strip().split() != arguments: errors.append(Error(task['__line__'], "Task arguments appear " "to be in key value rather " "than YAML format")) return Result(candidate.path, errors)
def yamlrolesfile(candidate, settings): rolesfile = os.path.join(os.path.dirname(candidate.path), "rolesfile") result = Result(candidate) if os.path.exists(rolesfile) and not os.path.exists(rolesfile + ".yml"): result.errors = [Error(None, "Rolesfile %s does not " "have a .yml extension" % rolesfile)] return result rolesfile = os.path.join(os.path.dirname(candidate.path), "rolesfile.yml") if os.path.exists(rolesfile): with codecs.open(rolesfile, mode='rb', encoding='utf-8') as f: try: yaml.safe_load(f) except Exception as e: result.errors = [Error(None, "Cannot parse YAML from %s: %s" % (rolesfile, str(e)))] return result
def metamain(candidate, settings): try: fh = codecs.open(candidate.path, mode='rb', encoding='utf-8') except IOError, e: result = Result(candidate) result.errors = [ Error(None, "Could not open %s: %s" % (candidate.path, e)) ]
def code_passes_pycodestyle(candidate, options): result = utils.execute(["pycodestyle", candidate.path]) errors = [] if result.rc: for line in result.output.strip().split('\n'): lineno = int(line.split(':')[1]) errors.append(Error(lineno, line)) return Result(candidate.path, errors)
def no_vars_in_host_file(candidate, options): errors = [] with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: try: yaml.safe_load(f) except Exception: for (lineno, line) in enumerate(f): if ':vars]' in line: errors.append( Error(lineno + 1, "contains a vars definition")) return Result(candidate.path, errors)
def parse(candidate, options): result = Result(candidate.path) try: if ANSIBLE > 1: loader = ansible.parsing.dataloader.DataLoader() var_manager = ansible.vars.VariableManager() ansible.inventory.Inventory(loader=loader, variable_manager=var_manager, host_list=candidate.path) else: ansible.inventory.Inventory(candidate.path) except Exception, e: result.errors = [Error(None, "Inventory is broken: %s" % e.message)]
def repeated_names(playbook, settings): with codecs.open(playbook['path'], mode='rb', encoding='utf-8') as f: yaml = parse_yaml_linenumbers(f, playbook['path']) namelines = defaultdict(list) errors = [] if yaml: for task in get_action_tasks(yaml, playbook): if 'name' in task: namelines[task['name']].append(task['__line__']) for (name, lines) in namelines.items(): if len(lines) > 1: errors.append(Error(lines[-1], "Task/handler name %s appears multiple times" % name)) return Result(playbook, errors)
def same_variable_defined_in_competing_groups(candidate, options): result = Result(candidate.path) # assume that group_vars file is under an inventory *directory* invfile = os.path.dirname(os.path.dirname(candidate.path)) global _inv try: if ANSIBLE > 1: loader = ansible.parsing.dataloader.DataLoader() var_manager = ansible.vars.VariableManager() inv = _inv or ansible.inventory.Inventory( loader=loader, variable_manager=var_manager, host_list=invfile) _inv = inv else: inv = _inv or ansible.inventory.Inventory(invfile) _inv = inv except AnsibleError, e: result.errors = [Error(None, "Inventory is broken: %s" % e.message)] return result
def playbook_contains_logic(candidate, settings): errors = [] with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: plays = parse_yaml_linenumbers(f.read(), candidate.path) for logic in ['tasks', 'pre_tasks', 'post_tasks', 'vars', 'handlers']: for play in plays: if logic in play: if isinstance(play[logic], list): firstitemline = play[logic][0]['__line__'] - 1 elif isinstance(play[logic], dict): firstitemline = play[logic]['__line__'] else: continue # we can only access line number of first thing in the section # so we guess the section starts on the line above. errors.append( Error(firstitemline, "%s should not be required in a play" % logic)) return Result(candidate.path, errors)
def yaml_form_rather_than_key_value(candidate, settings): with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: content = parse_yaml_linenumbers(f.read(), candidate.path) errors = [] if content: fileinfo = dict(type=candidate.filetype, path=candidate.path) for task in get_action_tasks(content, fileinfo): normal_form = normalize_task(task, candidate.path) action = normal_form['action']['__ansible_module__'] arguments = normal_form['action']['__ansible_arguments__'] # Cope with `set_fact` where task['set_fact'] is None if not task.get(action): continue if isinstance(task[action], dict): continue # strip additional newlines off task[action] if task[action].strip().split() != arguments: errors.append( Error( task['__line__'], "Task arguments appear " "to be in key value rather " "than YAML format")) return Result(candidate.path, errors)
def yaml_form_rather_than_key_value(candidate, settings): with codecs.open(candidate.path, mode='rb', encoding='utf-8') as f: content = parse_yaml_linenumbers(f.read(), candidate.path) errors = [] if content: fileinfo = dict(type=candidate.filetype, path=candidate.path) for task in get_action_tasks(content, fileinfo): normal_form = normalize_task(task, candidate.path) action = normal_form['action']['__ansible_module__'] arguments = normal_form['action']['__ansible_arguments__'] # FIXME: This is a bug - perhaps when connection is local # or similar if action not in task: continue if isinstance(task[action], dict): continue if task[action] != ' '.join(arguments): errors.append( Error( task['__line__'], "Task arguments appear " "to be in key value rather " "than YAML format")) return Result(candidate.path, errors)
def host_vars_exist(candidate, settings): return Result(candidate.path, [Error(None, "Host vars are generally " "not required")])
def repeated_vars(candidate, settings): with codecs.open(candidate.realpath, 'r') as f: errors = hunt_repeated_yaml_keys(f) or dict() return Result(candidate, [Error(err_line, "Variable %s occurs more than once" % err_key) for err_key in errors for err_line in errors[err_key]])
def host_vars_exist(candidate, settings): """Group variables are preferred over host variables.""" return Result(candidate.path, [Error(None, "Host vars are generally " "not required")])
try: fh = codecs.open(candidate.path, mode='rb', encoding='utf-8') except IOError, e: result = Result(candidate) result.errors = [ Error(None, "Could not open %s: %s" % (candidate.path, e)) ] try: result = Result(candidate) data = yaml.safe_load(fh) if 'dependencies' in data: if data["dependencies"] == []: return result else: result.errors = [ Error(None, "Role dependencies are " "not empty") ] else: result.errors = [ Error( None, "Role meta/main.yml does " "not contain a dependencies section") ] except Exception, e: result.errors = [ Error(None, "Could not parse in %s: %s" % (candidate.path, e)) ] finally: fh.close() return result
def repeated_vars(candidate, settings): with codecs.open(candidate.path, 'r') as text: errors = hunt_repeated_yaml_keys(text) or dict() return Result([Error(errors[err], "Variable %s occurs more than once" % err) for err in errors])
def check_fail(candidate, settings): return Result(candidate, [Error(1, "test failed")])
# group file exists in group_vars but no related group # in inventory directory return result remove_inherited_and_overridden_group_vars(group, inv) group_vars = set(_vars[group].keys()) child_hosts = group.hosts child_groups = group.child_groups siblings = set() for child_host in child_hosts: siblings.update(child_host.groups) for child_group in child_groups: siblings.update(child_group.parent_groups) for sibling in siblings: if sibling != group: remove_inherited_and_overridden_group_vars(sibling, inv) sibling_vars = set(_vars[sibling].keys()) common_vars = sibling_vars & group_vars common_hosts = [ host.name for host in set(child_hosts) & set(sibling.hosts) ] if common_vars and common_hosts: for var in common_vars: error_msg_template = "Sibling groups {0} and {1} with common hosts {2} " + \ "both define variable {3}" error_msg = error_msg_template.format( group.name, sibling.name, ", ".join(common_hosts), var) result.errors.append(Error(None, error_msg)) return result