diff --git a/ansiblelater/__main__.py b/ansiblelater/__main__.py index 08fa302..63a71e1 100755 --- a/ansiblelater/__main__.py +++ b/ansiblelater/__main__.py @@ -8,8 +8,9 @@ import sys from ansiblelater import LOG from ansiblelater import __version__ from ansiblelater import logger -from ansiblelater.command import base -from ansiblelater.command import candidates +from ansiblelater.candidate import Candidate +from ansiblelater.settings import Settings +from ansiblelater.standard import SingleStandards def main(): @@ -18,15 +19,28 @@ def main(): description="Validate Ansible files against best practice guideline" ) parser.add_argument( - "-c", "--config", dest="config_file", help="location of configuration file" + "-c", "--config", dest="config_file", metavar="CONFIG", help="path to configuration file" ) parser.add_argument( - "-r", "--rules", dest="rules.standards", help="location of standards rules" + "-r", + "--rules-dir", + dest="rules.standards", + metavar="RULES", + action="append", + help="directory of standard rules" + ) + parser.add_argument( + "-B", + "--no-buildin", + dest="rules.buildin", + action="store_false", + help="disables build-in standard rules" ) parser.add_argument( "-s", "--standards", dest="rules.filter", + metavar="FILTER", action="append", help="limit standards to given ID's" ) @@ -34,6 +48,7 @@ def main(): "-x", "--exclude-standards", dest="rules.exclude_filter", + metavar="EXCLUDE_FILTER", action="append", help="exclude standards by given ID's" ) @@ -44,24 +59,23 @@ def main(): "-q", dest="logging.level", action="append_const", const=1, help="decrease log level" ) parser.add_argument("rules.files", nargs="*") - parser.add_argument("--version", action="version", version="%(prog)s {}".format(__version__)) + parser.add_argument( + "-V", "--version", action="version", version="%(prog)s {}".format(__version__) + ) args = parser.parse_args().__dict__ - settings = base.get_settings(args) + settings = Settings(args=args) config = settings.config logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"]) - - files = config["rules"]["files"] - standards = base.get_standards(config["rules"]["standards"]) + SingleStandards(config["rules"]["standards"]).rules workers = max(multiprocessing.cpu_count() - 2, 2) p = multiprocessing.Pool(workers) tasks = [] - for filename in files: - lines = None - candidate = candidates.classify(filename, settings, standards) + for filename in config["rules"]["files"]: + candidate = Candidate.classify(filename, settings) if candidate: if candidate.binary: LOG.info("Not reviewing binary file {name}".format(name=filename)) @@ -69,11 +83,9 @@ def main(): if candidate.vault: LOG.info("Not reviewing vault file {name}".format(name=filename)) continue - if lines: - LOG.info("Reviewing {candidate} lines {no}".format(candidate=candidate, no=lines)) else: LOG.info("Reviewing all of {candidate}".format(candidate=candidate)) - tasks.append((candidate, settings, lines)) + tasks.append(candidate) else: LOG.info("Couldn't classify file {name}".format(name=filename)) @@ -89,9 +101,8 @@ def main(): sys.exit(return_code) -def _review_wrapper(args): - (candidate, settings, lines) = args - return candidate.review(settings, lines) +def _review_wrapper(candidate): + return candidate.review() if __name__ == "__main__": diff --git a/ansiblelater/command/candidates.py b/ansiblelater/candidate.py similarity index 62% rename from ansiblelater/command/candidates.py rename to ansiblelater/candidate.py index f98d76f..275b948 100644 --- a/ansiblelater/command/candidates.py +++ b/ansiblelater/candidate.py @@ -6,11 +6,11 @@ import os import re from distutils.version import LooseVersion -from six import iteritems - from ansiblelater import LOG from ansiblelater import utils from ansiblelater.logger import flag_extra +from ansiblelater.standard import SingleStandards +from ansiblelater.standard import StandardBase try: # Ansible 2.4 import of module loader @@ -36,8 +36,9 @@ class Candidate(object): self.vault = False self.filetype = type(self).__name__.lower() self.expected_version = True - self.standards = self._get_standards(settings, standards) self.faulty = False + self.config = settings.config + self.settings = settings try: with codecs.open(filename, mode="rb", encoding="utf-8") as f: @@ -46,9 +47,7 @@ class Candidate(object): except UnicodeDecodeError: self.binary = True - self.version = self._get_version(settings) - - def _get_version(self, settings): + def _get_version(self): path = self.path version = None @@ -97,60 +96,57 @@ class Candidate(object): return version - def _get_standards(self, settings, standards): + def _filter_standards(self): target_standards = [] - includes = settings.config["rules"]["filter"] - excludes = settings.config["rules"]["exclude_filter"] + includes = self.config["rules"]["filter"] + excludes = self.config["rules"]["exclude_filter"] if len(includes) == 0: - includes = [s.id for s in standards] + includes = [s.sid for s in self.standards] - for standard in standards: - if standard.id in includes and standard.id not in excludes: + for standard in self.standards: + if standard.sid in includes and standard.sid not in excludes: target_standards.append(standard) return target_standards - def review(self, settings, lines=None): + def review(self, lines=None): errors = 0 + self.standards = SingleStandards(self.config["rules"]["standards"]).rules + self.version = self._get_version() - for standard in self.standards: + for standard in self._filter_standards(): if type(self).__name__.lower() not in standard.types: continue - result = standard.check(self, settings.config) + result = standard.check(self, self.config) if not result: utils.sysexit_with_message( - "Standard '{}' returns an empty result object.".format( - standard.check.__name__ - ) + "Standard '{id}' returns an empty result object.".format(id=standard.sid) ) labels = { "tag": "review", - "standard": standard.name, + "standard": standard.description, "file": self.path, "passed": True } - if standard.id and standard.id.strip(): - labels["id"] = standard.id + if standard.sid and standard.sid.strip(): + labels["sid"] = standard.sid - for err in [ - err for err in result.errors if not err.lineno - or utils.is_line_in_ranges(err.lineno, utils.lines_ranges(lines)) - ]: # noqa + for err in result.errors: err_labels = copy.copy(labels) err_labels["passed"] = False - if isinstance(err, Error): + if isinstance(err, StandardBase.Error): err_labels.update(err.to_dict()) if not standard.version: LOG.warning( - "{id}Best practice '{name}' not met:\n{path}:{error}".format( - id=self._format_id(standard.id), - name=standard.name, + "{sid}Best practice '{description}' not met:\n{path}:{error}".format( + sid=self._format_id(standard.sid), + description=standard.description, path=self.path, error=err ), @@ -158,9 +154,9 @@ class Candidate(object): ) elif LooseVersion(standard.version) > LooseVersion(self.version): LOG.warning( - "{id}Future standard '{name}' not met:\n{path}:{error}".format( - id=self._format_id(standard.id), - name=standard.name, + "{sid}Future standard '{description}' not met:\n{path}:{error}".format( + sid=self._format_id(standard.sid), + description=standard.description, path=self.path, error=err ), @@ -168,9 +164,9 @@ class Candidate(object): ) else: LOG.error( - "{id}Standard '{name}' not met:\n{path}:{error}".format( - id=self._format_id(standard.id), - name=standard.name, + "{sid}Standard '{description}' not met:\n{path}:{error}".format( + sid=self._format_id(standard.sid), + description=standard.description, path=self.path, error=err ), @@ -180,6 +176,44 @@ class Candidate(object): return errors + @staticmethod + def classify(filename, settings={}, standards=[]): + parentdir = os.path.basename(os.path.dirname(filename)) + basename = os.path.basename(filename) + + if parentdir in ["tasks"]: + return Task(filename, settings, standards) + if parentdir in ["handlers"]: + return Handler(filename, settings, standards) + if parentdir in ["vars", "defaults"]: + return RoleVars(filename, settings, standards) + if "group_vars" in filename.split(os.sep): + return GroupVars(filename, settings, standards) + if "host_vars" in filename.split(os.sep): + return HostVars(filename, settings, standards) + if parentdir in ["meta"]: + return Meta(filename, settings, standards) + if ( + parentdir in ["library", "lookup_plugins", "callback_plugins", "filter_plugins"] + or filename.endswith(".py") + ): + return Code(filename, settings, standards) + if "inventory" == basename or "hosts" == basename or parentdir in ["inventories"]: + return Inventory(filename, settings, standards) + if "rolesfile" in basename or "requirements" in basename: + return Rolesfile(filename, settings, standards) + if "Makefile" in basename: + return Makefile(filename, settings, standards) + if "templates" in filename.split(os.sep) or basename.endswith(".j2"): + return Template(filename, settings, standards) + if "files" in filename.split(os.sep): + return File(filename, settings, standards) + if basename.endswith(".yml") or basename.endswith(".yaml"): + return Playbook(filename, settings, standards) + if "README" in basename: + return Doc(filename, settings, standards) + return None + def _format_id(self, standard_id): if standard_id and standard_id.strip(): standard_id = "[{id}] ".format(id=standard_id.strip()) @@ -314,82 +348,3 @@ class Rolesfile(Unversioned): """Object classified as Ansible roles file.""" pass - - -class Error(object): - """Default error object created if a rule failed.""" - - def __init__(self, lineno, message, error_type=None, **kwargs): - """ - Initialize a new error object and returns None. - - :param lineno: Line number where the error from de rule occures - :param message: Detailed error description provided by the rule - - """ - self.lineno = lineno - self.message = message - self.kwargs = kwargs - for (key, value) in iteritems(kwargs): - setattr(self, key, value) - - def __repr__(self): # noqa - if self.lineno: - return "{no}: {msg}".format(no=self.lineno, msg=self.message) - else: - return " {msg}".format(msg=self.message) - - def to_dict(self): - result = dict(lineno=self.lineno, message=self.message) - for (key, value) in iteritems(self.kwargs): - result[key] = value - return result - - -class Result(object): - """Generic result object.""" - - def __init__(self, candidate, errors=None): - self.candidate = candidate - self.errors = errors or [] - - def message(self): - return "\n".join(["{0}:{1}".format(self.candidate, error) for error in self.errors]) - - -def classify(filename, settings={}, standards=[]): - parentdir = os.path.basename(os.path.dirname(filename)) - basename = os.path.basename(filename) - - if parentdir in ["tasks"]: - return Task(filename, settings, standards) - if parentdir in ["handlers"]: - return Handler(filename, settings, standards) - if parentdir in ["vars", "defaults"]: - return RoleVars(filename, settings, standards) - if "group_vars" in filename.split(os.sep): - return GroupVars(filename, settings, standards) - if "host_vars" in filename.split(os.sep): - return HostVars(filename, settings, standards) - if parentdir in ["meta"]: - return Meta(filename, settings, standards) - if ( - parentdir in ["library", "lookup_plugins", "callback_plugins", "filter_plugins"] - or filename.endswith(".py") - ): - return Code(filename, settings, standards) - if "inventory" == basename or "hosts" == basename or parentdir in ["inventories"]: - return Inventory(filename, settings, standards) - if "rolesfile" in basename or "requirements" in basename: - return Rolesfile(filename, settings, standards) - if "Makefile" in basename: - return Makefile(filename, settings, standards) - if "templates" in filename.split(os.sep) or basename.endswith(".j2"): - return Template(filename, settings, standards) - if "files" in filename.split(os.sep): - return File(filename, settings, standards) - if basename.endswith(".yml") or basename.endswith(".yaml"): - return Playbook(filename, settings, standards) - if "README" in basename: - return Doc(filename, settings, standards) - return None diff --git a/ansiblelater/command/__init__.py b/ansiblelater/command/__init__.py deleted file mode 100644 index 4ede8e6..0000000 --- a/ansiblelater/command/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# noqa diff --git a/ansiblelater/command/base.py b/ansiblelater/command/base.py deleted file mode 100644 index 9e8c98b..0000000 --- a/ansiblelater/command/base.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Base methods.""" - -import importlib -import os -import sys -from distutils.version import LooseVersion - -import ansible -import toolz - -from ansiblelater import settings -from ansiblelater import utils - - -def get_settings(args): - """ - Get new settings object. - - :param args: cli args from argparse - :returns: Settings object - - """ - config = settings.Settings(args=args) - - return config - - -def get_standards(filepath): - sys.path.append(os.path.abspath(os.path.expanduser(filepath))) - try: - standards = importlib.import_module("standards") - except ImportError as e: - utils.sysexit_with_message( - "Could not import standards from directory {path}: {msg}".format( - path=filepath, msg=str(e) - ) - ) - - if getattr(standards, "ansible_min_version", None) and \ - LooseVersion(standards.ansible_min_version) > LooseVersion(ansible.__version__): - utils.sysexit_with_message( - "Standards require ansible version {min_version} (current version {version}). " - "Please upgrade ansible.".format( - min_version=standards.ansible_min_version, version=ansible.__version__ - ) - ) - - if getattr(standards, "ansible_later_min_version", None) and \ - utils.get_property("__version__") != "0.0.0" and \ - LooseVersion(standards.ansible_later_min_version) > LooseVersion( - utils.get_property("__version__")): - utils.sysexit_with_message( - "Standards require ansible-later version {min_version} (current version {version}). " - "Please upgrade ansible-later.".format( - min_version=standards.ansible_later_min_version, - version=utils.get_property("__version__") - ) - ) - - normalized_std = (list(toolz.remove(lambda x: x.id == "", standards.standards))) - unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.id))) - all_std = len(normalized_std) - if not all_std == unique_std: - utils.sysexit_with_message( - "Detect duplicate ID's in standards definition. Please use unique ID's only." - ) - - return standards.standards diff --git a/ansiblelater/data/standards.py b/ansiblelater/data/standards.py deleted file mode 100644 index d5422f3..0000000 --- a/ansiblelater/data/standards.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Example standards definition.""" - -from ansiblelater.rules.ansiblefiles import check_become_user -from ansiblelater.rules.ansiblefiles import check_braces_spaces -from ansiblelater.rules.ansiblefiles import check_command_has_changes -from ansiblelater.rules.ansiblefiles import check_command_instead_of_argument -from ansiblelater.rules.ansiblefiles import check_command_instead_of_module -from ansiblelater.rules.ansiblefiles import check_compare_to_literal_bool -from ansiblelater.rules.ansiblefiles import check_empty_string_compare -from ansiblelater.rules.ansiblefiles import check_filter_separation -from ansiblelater.rules.ansiblefiles import check_install_use_latest -from ansiblelater.rules.ansiblefiles import check_literal_bool_format -from ansiblelater.rules.ansiblefiles import check_name_format -from ansiblelater.rules.ansiblefiles import check_named_task -from ansiblelater.rules.ansiblefiles import check_shell_instead_command -from ansiblelater.rules.ansiblefiles import check_unique_named_task -from ansiblelater.rules.deprecated import check_deprecated -from ansiblelater.rules.rolefiles import check_meta_main -from ansiblelater.rules.rolefiles import check_scm_in_src -from ansiblelater.rules.taskfiles import check_line_between_tasks -from ansiblelater.rules.yamlfiles import check_native_yaml -from ansiblelater.rules.yamlfiles import check_yaml_colons -from ansiblelater.rules.yamlfiles import check_yaml_document_end -from ansiblelater.rules.yamlfiles import check_yaml_document_start -from ansiblelater.rules.yamlfiles import check_yaml_empty_lines -from ansiblelater.rules.yamlfiles import check_yaml_file -from ansiblelater.rules.yamlfiles import check_yaml_has_content -from ansiblelater.rules.yamlfiles import check_yaml_hyphens -from ansiblelater.rules.yamlfiles import check_yaml_indent -from ansiblelater.standard import Standard - -deprecated_features = Standard( - dict( - id="ANSIBLE9999", - name="Deprecated features should not be used", - check=check_deprecated, - types=["playbook", "task", "handler"] - ) -) - -tasks_should_be_separated = Standard( - dict( - id="ANSIBLE0001", - name="Single tasks should be separated by empty line", - check=check_line_between_tasks, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -role_must_contain_meta_main = Standard( - dict( - id="ANSIBLE0002", - name="Roles must contain suitable meta/main.yml", - check=check_meta_main, - version="0.1", - types=["meta"] - ) -) - -tasks_are_uniquely_named = Standard( - dict( - id="ANSIBLE0003", - name="Tasks and handlers must be uniquely named within a single file", - check=check_unique_named_task, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -use_spaces_between_variable_braces = Standard( - dict( - id="ANSIBLE0004", - name="YAML should use consistent number of spaces around variables", - check=check_braces_spaces, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -roles_scm_not_in_src = Standard( - dict( - id="ANSIBLE0005", - name="Use scm key rather than src: scm+url", - check=check_scm_in_src, - version="0.1", - types=["rolesfile"] - ) -) - -tasks_are_named = Standard( - dict( - id="ANSIBLE0006", - name="Tasks and handlers must be named", - check=check_named_task, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -tasks_names_are_formatted = Standard( - dict( - id="ANSIBLE0007", - name="Name of tasks and handlers must be formatted", - check=check_name_format, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -commands_should_not_be_used_in_place_of_modules = Standard( - dict( - id="ANSIBLE0008", - name="Commands should not be used in place of modules", - check=check_command_instead_of_module, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -package_installs_should_not_use_latest = Standard( - dict( - id="ANSIBLE0009", - name="Package installs should use present, not latest", - check=check_install_use_latest, - types=["playbook", "task", "handler"] - ) -) - -use_shell_only_when_necessary = Standard( - dict( - id="ANSIBLE0010", - name="Shell should only be used when essential", - check=check_shell_instead_command, - types=["playbook", "task", "handler"] - ) -) - -commands_should_be_idempotent = Standard( - dict( - id="ANSIBLE0011", - name="Commands should be idempotent", - check=check_command_has_changes, - version="0.1", - types=["playbook", "task"] - ) -) - -dont_compare_to_empty_string = Standard( - dict( - id="ANSIBLE0012", - name="Don't compare to \"\" - use `when: var` or `when: not var`", - check=check_empty_string_compare, - version="0.1", - types=["playbook", "task", "handler", "template"] - ) -) - -dont_compare_to_literal_bool = Standard( - dict( - id="ANSIBLE0013", - name="Don't compare to True or False - use `when: var` or `when: not var`", - check=check_compare_to_literal_bool, - version="0.1", - types=["playbook", "task", "handler", "template"] - ) -) - -literal_bool_should_be_formatted = Standard( - dict( - id="ANSIBLE0014", - name="Literal bools should start with a capital letter", - check=check_literal_bool_format, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"] - ) -) - -use_become_with_become_user = Standard( - dict( - id="ANSIBLE0015", - name="Become should be combined with become_user", - check=check_become_user, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -use_spaces_around_filters = Standard( - dict( - id="ANSIBLE0016", - name="Jinja2 filters should be separated with spaces", - check=check_filter_separation, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"] - ) -) - -commands_should_not_be_used_in_place_of_argument = Standard( - dict( - id="ANSIBLE0017", - name="Commands should not be used in place of module arguments", - check=check_command_instead_of_argument, - version="0.2", - types=["playbook", "task", "handler"] - ) -) - -files_should_not_contain_unnecessarily_empty_lines = Standard( - dict( - id="LINT0001", - name="YAML should not contain unnecessarily empty lines", - check=check_yaml_empty_lines, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_be_indented = Standard( - dict( - id="LINT0002", - name="YAML should be correctly indented", - check=check_yaml_indent, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_use_consistent_spaces_after_hyphens = Standard( - dict( - id="LINT0003", - name="YAML should use consistent number of spaces after hyphens", - check=check_yaml_hyphens, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_contain_document_start_marker = Standard( - dict( - id="LINT0004", - name="YAML should contain document start marker", - check=check_yaml_document_start, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -spaces_around_colons = Standard( - dict( - id="LINT0005", - name="YAML should use consistent number of spaces around colons", - check=check_yaml_colons, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -rolesfile_should_be_in_yaml = Standard( - dict( - id="LINT0006", - name="Roles file should be in yaml format", - check=check_yaml_file, - version="0.1", - types=["rolesfile"] - ) -) - -files_should_not_be_purposeless = Standard( - dict( - id="LINT0007", - name="Files should contain useful content", - check=check_yaml_has_content, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "defaults", "meta"] - ) -) - -use_yaml_rather_than_key_value = Standard( - dict( - id="LINT0008", - name="Use YAML format for tasks and handlers rather than key=value", - check=check_native_yaml, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -files_should_contain_document_end_marker = Standard( - dict( - id="LINT0009", - name="YAML should contain document end marker", - check=check_yaml_document_end, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -ansible_min_version = "2.5" -ansible_later_min_version = "0.3.0" - -standards = [ - # Ansible - tasks_should_be_separated, - role_must_contain_meta_main, - tasks_are_uniquely_named, - use_spaces_between_variable_braces, - roles_scm_not_in_src, - tasks_are_named, - tasks_names_are_formatted, - commands_should_not_be_used_in_place_of_modules, - package_installs_should_not_use_latest, - use_shell_only_when_necessary, - commands_should_be_idempotent, - dont_compare_to_empty_string, - dont_compare_to_literal_bool, - literal_bool_should_be_formatted, - use_become_with_become_user, - use_spaces_around_filters, - commands_should_not_be_used_in_place_of_argument, - deprecated_features, - # Lint - files_should_not_contain_unnecessarily_empty_lines, - files_should_be_indented, - files_should_use_consistent_spaces_after_hyphens, - files_should_contain_document_start_marker, - spaces_around_colons, - rolesfile_should_be_in_yaml, - files_should_not_be_purposeless, - use_yaml_rather_than_key_value, - files_should_contain_document_end_marker, -] diff --git a/ansiblelater/rules/CheckBecomeUser.py b/ansiblelater/rules/CheckBecomeUser.py new file mode 100644 index 0000000..6eb3a1c --- /dev/null +++ b/ansiblelater/rules/CheckBecomeUser.py @@ -0,0 +1,22 @@ +from ansiblelater.standard import StandardBase + + +class CheckBecomeUser(StandardBase): + + sid = "ANSIBLE0015" + description = "Become should be combined with become_user" + helptext = "the task has `become` enabled but `become_user` is missing" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + true_value = [True, "true", "True", "TRUE", "yes", "Yes", "YES"] + + if not errors: + gen = (task for task in tasks if "become" in task) + for task in gen: + if task["become"] in true_value and "become_user" not in task.keys(): + errors.append(self.Error(task["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckBracesSpaces.py b/ansiblelater/rules/CheckBracesSpaces.py new file mode 100644 index 0000000..3cf3cdf --- /dev/null +++ b/ansiblelater/rules/CheckBracesSpaces.py @@ -0,0 +1,47 @@ +import re + +from ansiblelater.standard import StandardBase +from ansiblelater.utils import count_spaces + + +class CheckBracesSpaces(StandardBase): + + sid = "ANSIBLE0004" + description = "YAML should use consistent number of spaces around variables" + helptext = "no suitable numbers of spaces (min: {min} max: {max})" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + conf = settings["ansible"]["double-braces"] + + matches = [] + braces = re.compile("{{(.*?)}}") + + if not errors: + for i, line in yamllines: + if "!unsafe" in line: + continue + match = braces.findall(line) + if match: + for item in match: + matches.append((i, item)) + + for i, line in matches: + [leading, trailing] = count_spaces(line) + sum_spaces = leading + trailing + + if ( + sum_spaces < conf["min-spaces-inside"] * 2 + or sum_spaces > conf["min-spaces-inside"] * 2 + ): + errors.append( + self.Error( + i, + self.helptext.format( + min=conf["min-spaces-inside"], max=conf["max-spaces-inside"] + ) + ) + ) + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckCommandHasChanges.py b/ansiblelater/rules/CheckCommandHasChanges.py new file mode 100644 index 0000000..20a851d --- /dev/null +++ b/ansiblelater/rules/CheckCommandHasChanges.py @@ -0,0 +1,29 @@ +from ansiblelater.standard import StandardBase + + +class CheckCommandHasChanges(StandardBase): + + sid = "ANSIBLE0011" + description = "Commands should be idempotent" + helptext = ( + "commands should only read while using `changed_when` or try to be " + "idempotent while using controls like `creates`, `removes` or `when`" + ) + version = "0.1" + types = ["playbook", "task"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + commands = ["command", "shell", "raw"] + + if not errors: + for task in tasks: + if task["action"]["__ansible_module__"] in commands: + if ( + "changed_when" not in task and "when" not in task + and "when" not in task.get("__ansible_action_meta__", []) + and "creates" not in task["action"] and "removes" not in task["action"] + ): + errors.append(self.Error(task["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckCommandInsteadOfArgument.py b/ansiblelater/rules/CheckCommandInsteadOfArgument.py new file mode 100644 index 0000000..21f68e3 --- /dev/null +++ b/ansiblelater/rules/CheckCommandInsteadOfArgument.py @@ -0,0 +1,64 @@ +# Copyright (c) 2013-2014 Will Thames +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import os + +from ansiblelater.standard import StandardBase + + +class CheckCommandInsteadOfArgument(StandardBase): + + sid = "ANSIBLE0017" + description = "Commands should not be used in place of module arguments" + helptext = "{exec} used in place of file modules argument {arg}" + version = "0.2" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + commands = ["command", "shell", "raw"] + arguments = { + "chown": "owner", + "chmod": "mode", + "chgrp": "group", + "ln": "state=link", + "mkdir": "state=directory", + "rmdir": "state=absent", + "rm": "state=absent" + } + + if not errors: + for task in tasks: + if task["action"]["__ansible_module__"] in commands: + first_cmd_arg = self.get_first_cmd_arg(task) + executable = os.path.basename(first_cmd_arg) + + if ( + first_cmd_arg and executable in arguments + and task["action"].get("warn", True) + ): + errors.append( + self.Error( + task["__line__"], + self.helptext.format(exec=executable, arg=arguments[executable]) + ) + ) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckCommandInsteadOfModule.py b/ansiblelater/rules/CheckCommandInsteadOfModule.py new file mode 100644 index 0000000..890d257 --- /dev/null +++ b/ansiblelater/rules/CheckCommandInsteadOfModule.py @@ -0,0 +1,53 @@ +import os + +from ansiblelater.standard import StandardBase + + +class CheckCommandInsteadOfModule(StandardBase): + + sid = "ANSIBLE0008" + description = "Commands should not be used in place of modules" + helptext = "{exec} command used in place of {module} module" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + commands = ["command", "shell", "raw"] + modules = { + "git": "git", + "hg": "hg", + "curl": "get_url or uri", + "wget": "get_url or uri", + "svn": "subversion", + "service": "service", + "mount": "mount", + "rpm": "yum or rpm_key", + "yum": "yum", + "apt-get": "apt-get", + "unzip": "unarchive", + "tar": "unarchive", + "chkconfig": "service", + "rsync": "synchronize", + "supervisorctl": "supervisorctl", + "systemctl": "systemd", + "sed": "template or lineinfile" + } + + if not errors: + for task in tasks: + if task["action"]["__ansible_module__"] in commands: + first_cmd_arg = self.get_first_cmd_arg(task) + executable = os.path.basename(first_cmd_arg) + if ( + first_cmd_arg and executable in modules + and task["action"].get("warn", True) and "register" not in task + ): + errors.append( + self.Error( + task["__line__"], + self.helptext.format(exec=executable, module=modules[executable]) + ) + ) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckCompareToLiteralBool.py b/ansiblelater/rules/CheckCompareToLiteralBool.py new file mode 100644 index 0000000..fe4ce9a --- /dev/null +++ b/ansiblelater/rules/CheckCompareToLiteralBool.py @@ -0,0 +1,37 @@ +import re + +from ansiblelater.candidate import Template +from ansiblelater.standard import StandardBase + + +class CheckCompareToLiteralBool(StandardBase): + + sid = "ANSIBLE0013" + description = "Don't compare to True or False" + helptext = ("use `when: var` rather than `when: var == True` (or conversely `when: not var`)") + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + + if not errors: + if isinstance(candidate, Template): + matches = [] + jinja_string = re.compile("({{|{%)(.*?)(}}|%})") + + for i, line in yamllines: + match = jinja_string.findall(line) + if match: + for item in match: + matches.append((i, item[1])) + + yamllines = matches + + literal_bool_compare = re.compile("[=!]= ?(True|true|False|false)") + + for i, line in yamllines: + if literal_bool_compare.findall(line): + errors.append(self.Error(i, self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckDeprecated.py b/ansiblelater/rules/CheckDeprecated.py new file mode 100644 index 0000000..8301482 --- /dev/null +++ b/ansiblelater/rules/CheckDeprecated.py @@ -0,0 +1,26 @@ +from ansiblelater.standard import StandardBase + + +class CheckDeprecated(StandardBase): + + sid = "ANSIBLE9999" + description = "Deprecated features should not be used" + helptext = "'{old}' is deprecated and should not be used anymore. Use '{new}' instead." + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings, full=True) + + if not errors: + for task in tasks: + if "skip_ansible_lint" in (task.get("tags") or []): + errors.append( + self.Error( + task["__line__"], + self.helptext.format( + old="skip_ansible_lint", new="skip_ansible_later" + ) + ) + ) + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckEmptyStringCompare.py b/ansiblelater/rules/CheckEmptyStringCompare.py new file mode 100644 index 0000000..f335990 --- /dev/null +++ b/ansiblelater/rules/CheckEmptyStringCompare.py @@ -0,0 +1,37 @@ +import re + +from ansiblelater.candidate import Template +from ansiblelater.standard import StandardBase + + +class CheckEmptyStringCompare(StandardBase): + + sid = "ANSIBLE0012" + description = "Don't compare to empty string \"\"" + helptext = ("use `when: var` rather than `when: var !=` (or conversely `when: not var`)") + version = "0.1" + types = ["playbook", "task", "handler", "template"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + + if not errors: + if isinstance(candidate, Template): + matches = [] + jinja_string = re.compile("({{|{%)(.*?)(}}|%})") + + for i, line in yamllines: + match = jinja_string.findall(line) + if match: + for item in match: + matches.append((i, item[1])) + + yamllines = matches + + empty_string_compare = re.compile("[=!]= ?[\"'][\"']") + + for i, line in yamllines: + if empty_string_compare.findall(line): + errors.append(self.Error(i, self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckFilterSeparation.py b/ansiblelater/rules/CheckFilterSeparation.py new file mode 100644 index 0000000..a6d6155 --- /dev/null +++ b/ansiblelater/rules/CheckFilterSeparation.py @@ -0,0 +1,31 @@ +import re + +from ansiblelater.standard import StandardBase + + +class CheckFilterSeparation(StandardBase): + + sid = "ANSIBLE0016" + description = "Jinja2 filters should be separated with spaces" + helptext = "no suitable numbers of spaces (required: 1)" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + + matches = [] + braces = re.compile("{{(.*?)}}") + filters = re.compile(r"(?<=\|)([\s]{2,}[^\s}]+|[^\s]+)|([^\s{]+[\s]{2,}|[^\s]+)(?=\|)") + + if not errors: + for i, line in yamllines: + match = braces.findall(line) + if match: + for item in match: + matches.append((i, item)) + + for i, line in matches: + if filters.findall(line): + errors.append(self.Error(i, self.helptext)) + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckInstallUseLatest.py b/ansiblelater/rules/CheckInstallUseLatest.py new file mode 100644 index 0000000..7025c04 --- /dev/null +++ b/ansiblelater/rules/CheckInstallUseLatest.py @@ -0,0 +1,29 @@ +from ansiblelater.standard import StandardBase + + +class CheckInstallUseLatest(StandardBase): + + sid = "ANSIBLE0009" + description = "Package installs should use present, not latest" + helptext = "package installs should use `state=present` with or without a version" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + package_managers = [ + "yum", "apt", "dnf", "homebrew", "pacman", "openbsd_package", "pkg5", "portage", + "pkgutil", "slackpkg", "swdepot", "zypper", "bundler", "pip", "pear", "npm", "yarn", + "gem", "easy_install", "bower", "package", "apk", "openbsd_pkg", "pkgng", "sorcery", + "xbps" + ] + + if not errors: + for task in tasks: + if ( + task["action"]["__ansible_module__"] in package_managers + and task["action"].get("state") == "latest" + ): + errors.append(self.Error(task["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckLiteralBoolFormat.py b/ansiblelater/rules/CheckLiteralBoolFormat.py new file mode 100644 index 0000000..70013b4 --- /dev/null +++ b/ansiblelater/rules/CheckLiteralBoolFormat.py @@ -0,0 +1,24 @@ +import re + +from ansiblelater.standard import StandardBase + + +class CheckLiteralBoolFormat(StandardBase): + + sid = "ANSIBLE0014" + description = "Literal bools should start with a capital letter" + helptext = "literal bools should be written as `True/False` or `yes/no`" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + + uppercase_bool = re.compile(r"([=!]=|:)\s*(true|false|TRUE|FALSE|Yes|No|YES|NO)\s*$") + + if not errors: + for i, line in yamllines: + if uppercase_bool.findall(line): + errors.append(self.Error(i, self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckMetaMain.py b/ansiblelater/rules/CheckMetaMain.py new file mode 100644 index 0000000..97b5a81 --- /dev/null +++ b/ansiblelater/rules/CheckMetaMain.py @@ -0,0 +1,23 @@ +from nested_lookup import nested_lookup + +from ansiblelater.standard import StandardBase + + +class CheckMetaMain(StandardBase): + + sid = "ANSIBLE0002" + description = "Roles must contain suitable meta/main.yml" + helptext = "file should contain '{key}' key" + version = "0.1" + types = ["meta"] + + def check(self, candidate, settings): + content, errors = self.get_raw_yaml(candidate, settings) + keys = ["author", "description", "min_ansible_version", "platforms", "dependencies"] + + if not errors: + for key in keys: + if not nested_lookup(key, content): + errors.append(self.Error(None, self.helptext.format(key=key))) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckNameFormat.py b/ansiblelater/rules/CheckNameFormat.py new file mode 100644 index 0000000..8747db8 --- /dev/null +++ b/ansiblelater/rules/CheckNameFormat.py @@ -0,0 +1,26 @@ +from collections import defaultdict + +from ansiblelater.standard import StandardBase + + +class CheckNameFormat(StandardBase): + + sid = "ANSIBLE0007" + description = "Name of tasks and handlers must be formatted" + helptext = "name '{name}' should start with uppercase" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + namelines = defaultdict(list) + + if not errors: + for task in tasks: + if "name" in task: + namelines[task["name"]].append(task["__line__"]) + for (name, lines) in namelines.items(): + if name and not name[0].isupper(): + errors.append(self.Error(lines[-1], self.helptext.format(name=name))) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckNamedTask.py b/ansiblelater/rules/CheckNamedTask.py new file mode 100644 index 0000000..2b2b9d9 --- /dev/null +++ b/ansiblelater/rules/CheckNamedTask.py @@ -0,0 +1,27 @@ +from ansiblelater.standard import StandardBase + + +class CheckNamedTask(StandardBase): + + sid = "ANSIBLE0006" + description = "Tasks and handlers must be named" + helptext = "module '{module}' used without or empty `name` attribute" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + nameless_tasks = [ + "meta", "debug", "include_role", "import_role", "include_tasks", "import_tasks", + "include_vars", "block" + ] + + if not errors: + for task in tasks: + module = task["action"]["__ansible_module__"] + if ("name" not in task or not task["name"]) and module not in nameless_tasks: + errors.append( + self.Error(task["__line__"], self.helptext.format(module=module)) + ) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckNativeYaml.py b/ansiblelater/rules/CheckNativeYaml.py new file mode 100644 index 0000000..fbdf01e --- /dev/null +++ b/ansiblelater/rules/CheckNativeYaml.py @@ -0,0 +1,36 @@ +from ansiblelater.standard import StandardBase + + +class CheckNativeYaml(StandardBase): + + sid = "LINT0008" + description = "Use YAML format for tasks and handlers rather than key=value" + helptext = "task arguments appear to be in key value rather than YAML format" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_action_tasks(candidate, settings) + + if not errors: + for task in tasks: + normal_form, error = self.get_normalized_task(task, candidate, settings) + if error: + errors.extend(error) + break + + action = normal_form["action"]["__ansible_module__"] + arguments = [ + bytes(x, "utf-8").decode("unicode_escape") + for x in 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] + task_action = bytes(task[action].strip(), "utf-8").decode("unicode_escape") + if task_action.split() != arguments: + errors.append(self.Error(task["__line__"], self.helptext)) + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckScmInSrc.py b/ansiblelater/rules/CheckScmInSrc.py new file mode 100644 index 0000000..4868d79 --- /dev/null +++ b/ansiblelater/rules/CheckScmInSrc.py @@ -0,0 +1,23 @@ +from ansible.parsing.yaml.objects import AnsibleMapping + +from ansiblelater.standard import StandardBase + + +class CheckScmInSrc(StandardBase): + + sid = "ANSIBLE0005" + description = "Use `scm:` key rather than `src: scm+url`" + helptext = "usage of `src: scm+url` not recommended" + version = "0.1" + types = ["rolesfile"] + + def check(self, candidate, settings): + roles, errors = self.get_tasks(candidate, settings) + + if not errors: + for role in roles: + if isinstance(role, AnsibleMapping): + if "+" in role.get("src"): + errors.append(self.Error(role["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckShellInsteadCommand.py b/ansiblelater/rules/CheckShellInsteadCommand.py new file mode 100644 index 0000000..5b29de0 --- /dev/null +++ b/ansiblelater/rules/CheckShellInsteadCommand.py @@ -0,0 +1,34 @@ +import re + +from ansiblelater.standard import StandardBase + + +class CheckShellInsteadCommand(StandardBase): + + sid = "ANSIBLE0010" + description = "Shell should only be used when essential" + helptext = "shell should only be used when piping, redirecting or chaining commands" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + + if not errors: + for task in tasks: + if task["action"]["__ansible_module__"] == "shell": + # skip processing if args.executable is used as this + # parameter is no longer support by command module + if "executable" in task["action"]: + continue + + if "cmd" in task["action"]: + cmd = task["action"].get("cmd", []) + else: + cmd = " ".join(task["action"].get("__ansible_arguments__", [])) + + unjinja = re.sub(r"\{\{[^\}]*\}\}", "JINJA_VAR", cmd) + if not any([ch in unjinja for ch in "&|<>;$\n*[]{}?"]): + errors.append(self.Error(task["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckTaskSeparation.py b/ansiblelater/rules/CheckTaskSeparation.py new file mode 100644 index 0000000..080b3ce --- /dev/null +++ b/ansiblelater/rules/CheckTaskSeparation.py @@ -0,0 +1,43 @@ +import re +from collections import defaultdict + +from ansiblelater.standard import StandardBase + + +class CheckTaskSeparation(StandardBase): + + sid = "ANSIBLE0001" + description = "Single tasks should be separated by empty line" + helptext = "missing task separation (required: 1 empty line)" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + options = defaultdict(dict) + options.update(remove_empty=False) + options.update(remove_markers=False) + + yamllines, line_errors = self.get_normalized_yaml(candidate, settings, options) + tasks, task_errors = self.get_normalized_tasks(candidate, settings) + + task_regex = re.compile(r"-\sname:(.*)") + prevline = "#file_start_marker" + + allowed_prevline = ["---", "tasks:", "pre_tasks:", "post_tasks:", "block:"] + + errors = task_errors + line_errors + if not errors: + for i, line in yamllines: + match = task_regex.search(line) + if match and prevline: + name = match.group(1).strip() + + if not any(task.get("name") == name for task in tasks): + continue + + if not any(item in prevline for item in allowed_prevline): + errors.append(self.Error(i, self.helptext)) + + prevline = line.strip() + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckUniqueNamedTask.py b/ansiblelater/rules/CheckUniqueNamedTask.py new file mode 100644 index 0000000..558a145 --- /dev/null +++ b/ansiblelater/rules/CheckUniqueNamedTask.py @@ -0,0 +1,27 @@ +from collections import defaultdict + +from ansiblelater.standard import StandardBase + + +class CheckUniqueNamedTask(StandardBase): + + sid = "ANSIBLE0003" + description = "Tasks and handlers must be uniquely named within a single file" + helptext = "name '{name}' appears multiple times" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + + namelines = defaultdict(list) + + if not errors: + for task in tasks: + if "name" in task: + namelines[task["name"]].append(task["__line__"]) + for (name, lines) in namelines.items(): + if name and len(lines) > 1: + errors.append(self.Error(lines[-1], self.helptext.format(name=name))) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlColons.py b/ansiblelater/rules/CheckYamlColons.py new file mode 100644 index 0000000..0d1513f --- /dev/null +++ b/ansiblelater/rules/CheckYamlColons.py @@ -0,0 +1,15 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlColons(StandardBase): + + sid = "LINT0005" + description = "YAML should use consistent number of spaces around colons" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{colons: {conf}}}".format(conf=settings["yamllint"]["colons"]) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlDocumentEnd.py b/ansiblelater/rules/CheckYamlDocumentEnd.py new file mode 100644 index 0000000..2b280cf --- /dev/null +++ b/ansiblelater/rules/CheckYamlDocumentEnd.py @@ -0,0 +1,17 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlDocumentEnd(StandardBase): + + sid = "LINT0009" + description = "YAML should contain document end marker" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{document-end: {conf}}}".format( + conf=settings["yamllint"]["document-end"] + ) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlDocumentStart.py b/ansiblelater/rules/CheckYamlDocumentStart.py new file mode 100644 index 0000000..86ed72e --- /dev/null +++ b/ansiblelater/rules/CheckYamlDocumentStart.py @@ -0,0 +1,17 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlDocumentStart(StandardBase): + + sid = "LINT0004" + description = "YAML should contain document start marker" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{document-start: {conf}}}".format( + conf=settings["yamllint"]["document-start"] + ) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlEmptyLines.py b/ansiblelater/rules/CheckYamlEmptyLines.py new file mode 100644 index 0000000..9ff1f41 --- /dev/null +++ b/ansiblelater/rules/CheckYamlEmptyLines.py @@ -0,0 +1,15 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlEmptyLines(StandardBase): + + sid = "LINT0001" + description = "YAML should not contain unnecessarily empty lines" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{empty-lines: {conf}}}".format(conf=settings["yamllint"]["empty-lines"]) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlFile.py b/ansiblelater/rules/CheckYamlFile.py new file mode 100644 index 0000000..dcae58d --- /dev/null +++ b/ansiblelater/rules/CheckYamlFile.py @@ -0,0 +1,23 @@ +import os + +from ansiblelater.standard import StandardBase + + +class CheckYamlFile(StandardBase): + + sid = "LINT0006" + description = "Roles file should be in yaml format" + helptext = "file does not have a .yml extension" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + errors = [] + extensions = [".yml", ".yaml"] + + if os.path.isfile(candidate.path) and os.path.splitext(candidate.path)[1] in extensions: + content, errors = self.get_raw_yaml(candidate, settings) + else: + errors.append(self.Error(None, self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlHasContent.py b/ansiblelater/rules/CheckYamlHasContent.py new file mode 100644 index 0000000..1e489d9 --- /dev/null +++ b/ansiblelater/rules/CheckYamlHasContent.py @@ -0,0 +1,18 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlHasContent(StandardBase): + + sid = "LINT0007" + description = "Files should contain useful content" + helptext = "the file appears to have no useful content" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "defaults", "meta"] + + def check(self, candidate, settings): + yamllines, errors = self.get_normalized_yaml(candidate, settings) + + if (not candidate.faulty and len(yamllines) == 0) and not errors: + errors.append(self.Error(None, self.helptext)) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlHyphens.py b/ansiblelater/rules/CheckYamlHyphens.py new file mode 100644 index 0000000..9b65de7 --- /dev/null +++ b/ansiblelater/rules/CheckYamlHyphens.py @@ -0,0 +1,15 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlHyphens(StandardBase): + + sid = "LINT0003" + description = "YAML should use consistent number of spaces after hyphens" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{hyphens: {conf}}}".format(conf=settings["yamllint"]["hyphens"]) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/CheckYamlIndent.py b/ansiblelater/rules/CheckYamlIndent.py new file mode 100644 index 0000000..e5f7618 --- /dev/null +++ b/ansiblelater/rules/CheckYamlIndent.py @@ -0,0 +1,17 @@ +from ansiblelater.standard import StandardBase + + +class CheckYamlIndent(StandardBase): + + sid = "LINT0002" + description = "YAML should not contain unnecessarily empty lines" + version = "0.1" + types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] + + def check(self, candidate, settings): + options = "rules: {{document-start: {conf}}}".format( + conf=settings["yamllint"]["document-start"] + ) + errors = self.run_yamllint(candidate, options) + + return self.Result(candidate.path, errors) diff --git a/ansiblelater/rules/__init__.py b/ansiblelater/rules/__init__.py deleted file mode 100644 index 4ede8e6..0000000 --- a/ansiblelater/rules/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# noqa diff --git a/ansiblelater/rules/ansiblefiles.py b/ansiblelater/rules/ansiblefiles.py deleted file mode 100644 index 597cd7d..0000000 --- a/ansiblelater/rules/ansiblefiles.py +++ /dev/null @@ -1,371 +0,0 @@ -"""Checks related to ansible specific best practices.""" - -import os -import re -from collections import defaultdict - -from ansiblelater.command.candidates import Error -from ansiblelater.command.candidates import Result -from ansiblelater.command.candidates import Template -from ansiblelater.utils import count_spaces -from ansiblelater.utils.rulehelper import get_first_cmd_arg -from ansiblelater.utils.rulehelper import get_normalized_tasks -from ansiblelater.utils.rulehelper import get_normalized_yaml - - -def check_braces_spaces(candidate, settings): - yamllines, errors = get_normalized_yaml(candidate, settings) - conf = settings["ansible"]["double-braces"] - description = "no suitable numbers of spaces (min: {min} max: {max})".format( - min=conf["min-spaces-inside"], max=conf["max-spaces-inside"] - ) - - matches = [] - braces = re.compile("{{(.*?)}}") - - if not errors: - for i, line in yamllines: - if "!unsafe" in line: - continue - match = braces.findall(line) - if match: - for item in match: - matches.append((i, item)) - - for i, line in matches: - [leading, trailing] = count_spaces(line) - sum_spaces = leading + trailing - - if ( - sum_spaces < conf["min-spaces-inside"] * 2 - or sum_spaces > conf["min-spaces-inside"] * 2 - ): - errors.append(Error(i, description)) - return Result(candidate.path, errors) - - -def check_named_task(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - nameless_tasks = [ - "meta", "debug", "include_role", "import_role", "include_tasks", "import_tasks", - "include_vars", "block" - ] - description = "module '{module}' used without or empty name attribute" - - if not errors: - for task in tasks: - module = task["action"]["__ansible_module__"] - if ("name" not in task or not task["name"]) and module not in nameless_tasks: - errors.append(Error(task["__line__"], description.format(module=module))) - - return Result(candidate.path, errors) - - -def check_name_format(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - description = "name '{name}' should start with uppercase" - namelines = defaultdict(list) - - if not errors: - for task in tasks: - if "name" in task: - namelines[task["name"]].append(task["__line__"]) - for (name, lines) in namelines.items(): - if name and not name[0].isupper(): - errors.append(Error(lines[-1], description.format(name=name))) - - return Result(candidate.path, errors) - - -def check_unique_named_task(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - description = "name '{name}' appears multiple times" - - namelines = defaultdict(list) - - if not errors: - for task in tasks: - if "name" in task: - namelines[task["name"]].append(task["__line__"]) - for (name, lines) in namelines.items(): - if name and len(lines) > 1: - errors.append(Error(lines[-1], description.format(name=name))) - - return Result(candidate.path, errors) - - -def check_command_instead_of_module(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - commands = ["command", "shell", "raw"] - modules = { - "git": "git", - "hg": "hg", - "curl": "get_url or uri", - "wget": "get_url or uri", - "svn": "subversion", - "service": "service", - "mount": "mount", - "rpm": "yum or rpm_key", - "yum": "yum", - "apt-get": "apt-get", - "unzip": "unarchive", - "tar": "unarchive", - "chkconfig": "service", - "rsync": "synchronize", - "supervisorctl": "supervisorctl", - "systemctl": "systemd", - "sed": "template or lineinfile" - } - description = "{exec} command used in place of {module} module" - - if not errors: - for task in tasks: - if task["action"]["__ansible_module__"] in commands: - first_cmd_arg = get_first_cmd_arg(task) - executable = os.path.basename(first_cmd_arg) - if ( - first_cmd_arg and executable in modules and task["action"].get("warn", True) - and "register" not in task - ): - errors.append( - Error( - task["__line__"], - description.format(exec=executable, module=modules[executable]) - ) - ) - - return Result(candidate.path, errors) - - -def check_install_use_latest(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - package_managers = [ - "yum", "apt", "dnf", "homebrew", "pacman", "openbsd_package", "pkg5", "portage", "pkgutil", - "slackpkg", "swdepot", "zypper", "bundler", "pip", "pear", "npm", "yarn", "gem", - "easy_install", "bower", "package", "apk", "openbsd_pkg", "pkgng", "sorcery", "xbps" - ] - description = "package installs should use state=present with or without a version" - - if not errors: - for task in tasks: - if ( - task["action"]["__ansible_module__"] in package_managers - and task["action"].get("state") == "latest" - ): - errors.append(Error(task["__line__"], description)) - - return Result(candidate.path, errors) - - -def check_shell_instead_command(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - description = "shell should only be used when piping, redirecting or chaining commands" - - if not errors: - for task in tasks: - if task["action"]["__ansible_module__"] == "shell": - # skip processing if args.executable is used as this - # parameter is no longer support by command module - if "executable" in task["action"]: - continue - - if "cmd" in task["action"]: - cmd = task["action"].get("cmd", []) - else: - cmd = " ".join(task["action"].get("__ansible_arguments__", [])) - - unjinja = re.sub(r"\{\{[^\}]*\}\}", "JINJA_VAR", cmd) - if not any([ch in unjinja for ch in "&|<>;$\n*[]{}?"]): - errors.append(Error(task["__line__"], description)) - - return Result(candidate.path, errors) - - -def check_command_has_changes(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - commands = ["command", "shell", "raw"] - description = "commands should either read information (and thus set changed_when) or not " \ - "do something if it has already been done (using creates/removes) " \ - "or only do it if another check has a particular result (when)" - - if not errors: - for task in tasks: - if task["action"]["__ansible_module__"] in commands: - if ( - "changed_when" not in task and "when" not in task - and "when" not in task.get("__ansible_action_meta__", []) - and "creates" not in task["action"] and "removes" not in task["action"] - ): - errors.append(Error(task["__line__"], description)) - - return Result(candidate.path, errors) - - -def check_command_instead_of_argument(candidate, settings): - # Copyright (c) 2013-2014 Will Thames - # - # Permission is hereby granted, free of charge, to any person obtaining a copy - # of this software and associated documentation files (the "Software"), to deal - # in the Software without restriction, including without limitation the rights - # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - # copies of the Software, and to permit persons to whom the Software is - # furnished to do so, subject to the following conditions: - # - # The above copyright notice and this permission notice shall be included in - # all copies or substantial portions of the Software. - # - # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - # THE SOFTWARE. - - tasks, errors = get_normalized_tasks(candidate, settings) - commands = ["command", "shell", "raw"] - arguments = { - "chown": "owner", - "chmod": "mode", - "chgrp": "group", - "ln": "state=link", - "mkdir": "state=directory", - "rmdir": "state=absent", - "rm": "state=absent" - } - description = "{exec} used in place of file modules argument {arg}" - - if not errors: - for task in tasks: - if task["action"]["__ansible_module__"] in commands: - first_cmd_arg = get_first_cmd_arg(task) - executable = os.path.basename(first_cmd_arg) - - if ( - first_cmd_arg and executable in arguments and task["action"].get("warn", True) - ): - errors.append( - Error( - task["__line__"], - description.format(exec=executable, arg=arguments[executable]) - ) - ) - - return Result(candidate.path, errors) - - -def check_empty_string_compare(candidate, settings): - yamllines, errors = get_normalized_yaml(candidate, settings) - description = "use `when: var` rather than `when: var != ""` (or " \ - "conversely `when: not var` rather than `when: var == ""`)" - - if not errors: - if isinstance(candidate, Template): - matches = [] - jinja_string = re.compile("({{|{%)(.*?)(}}|%})") - - for i, line in yamllines: - match = jinja_string.findall(line) - if match: - for item in match: - matches.append((i, item[1])) - - yamllines = matches - - empty_string_compare = re.compile("[=!]= ?[\"'][\"']") - - for i, line in yamllines: - if empty_string_compare.findall(line): - errors.append(Error(i, description)) - - return Result(candidate.path, errors) - - -def check_compare_to_literal_bool(candidate, settings): - yamllines, errors = get_normalized_yaml(candidate, settings) - description = "use `when: var` rather than `when: var == True` " \ - "(or conversely `when: not var`)" - - if not errors: - if isinstance(candidate, Template): - matches = [] - jinja_string = re.compile("({{|{%)(.*?)(}}|%})") - - for i, line in yamllines: - match = jinja_string.findall(line) - if match: - for item in match: - matches.append((i, item[1])) - - yamllines = matches - - literal_bool_compare = re.compile("[=!]= ?(True|true|False|false)") - - for i, line in yamllines: - if literal_bool_compare.findall(line): - errors.append(Error(i, description)) - - return Result(candidate.path, errors) - - -def check_delegate_to_localhost(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - description = "connection: local ensures that unexpected delegated_vars " \ - "don't get set (e.g. {{ inventory_hostname }} " \ - "used by vars_files)" - - if not errors: - for task in tasks: - if task.get("delegate_to") == "localhost": - errors.append(Error(task["__line__"], description)) - - return Result(candidate.path, errors) - - -def check_literal_bool_format(candidate, settings): - yamllines, errors = get_normalized_yaml(candidate, settings) - description = "literal bools should be written as 'True/False' or 'yes/no'" - - uppercase_bool = re.compile(r"([=!]=|:)\s*(true|false|TRUE|FALSE|Yes|No|YES|NO)\s*$") - - if not errors: - for i, line in yamllines: - if uppercase_bool.findall(line): - errors.append(Error(i, description)) - - return Result(candidate.path, errors) - - -def check_become_user(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings) - description = "the task has 'become:' enabled but 'become_user:' is missing" - true_value = [True, "true", "True", "TRUE", "yes", "Yes", "YES"] - - if not errors: - gen = (task for task in tasks if "become" in task) - for task in gen: - if task["become"] in true_value and "become_user" not in task.keys(): - errors.append(Error(task["__line__"], description)) - - return Result(candidate.path, errors) - - -def check_filter_separation(candidate, settings): - yamllines, errors = get_normalized_yaml(candidate, settings) - description = "no suitable numbers of spaces (required: 1)" - - matches = [] - braces = re.compile("{{(.*?)}}") - filters = re.compile(r"(?<=\|)([\s]{2,}[^\s}]+|[^\s]+)|([^\s{]+[\s]{2,}|[^\s]+)(?=\|)") - - if not errors: - for i, line in yamllines: - match = braces.findall(line) - if match: - for item in match: - matches.append((i, item)) - - for i, line in matches: - if filters.findall(line): - errors.append(Error(i, description)) - return Result(candidate.path, errors) diff --git a/ansiblelater/rules/deprecated.py b/ansiblelater/rules/deprecated.py deleted file mode 100644 index 72fb827..0000000 --- a/ansiblelater/rules/deprecated.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Checks related to ansible specific best practices.""" - -from ansiblelater.command.candidates import Error -from ansiblelater.command.candidates import Result -from ansiblelater.utils.rulehelper import get_normalized_tasks - - -def check_deprecated(candidate, settings): - tasks, errors = get_normalized_tasks(candidate, settings, full=True) - description = "'{old}' is deprecated and should not be used anymore. Use '{new}' instead." - - if not errors: - for task in tasks: - if "skip_ansible_lint" in (task.get("tags") or []): - errors.append( - Error( - task["__line__"], - description.format(old="skip_ansible_lint", new="skip_ansible_later") - ) - ) - return Result(candidate.path, errors) diff --git a/ansiblelater/rules/rolefiles.py b/ansiblelater/rules/rolefiles.py deleted file mode 100644 index fcf3dbd..0000000 --- a/ansiblelater/rules/rolefiles.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Checks related to ansible roles files.""" - -from ansible.parsing.yaml.objects import AnsibleMapping -from nested_lookup import nested_lookup - -from ansiblelater.command.candidates import Error -from ansiblelater.command.candidates import Result -from ansiblelater.utils.rulehelper import get_raw_yaml -from ansiblelater.utils.rulehelper import get_tasks - - -def check_meta_main(candidate, settings): - content, errors = get_raw_yaml(candidate, settings) - keys = ["author", "description", "min_ansible_version", "platforms", "dependencies"] - description = "file should contain '{key}' key" - - if not errors: - for key in keys: - if not nested_lookup(key, content): - errors.append(Error(None, description.format(key=key))) - - return Result(candidate.path, errors) - - -def check_scm_in_src(candidate, settings): - roles, errors = get_tasks(candidate, settings) - description = "usage of src: scm+url not recommended" - - if not errors: - for role in roles: - if isinstance(role, AnsibleMapping): - if "+" in role.get("src"): - errors.append(Error(role["__line__"], description)) - - return Result(candidate.path, errors) diff --git a/ansiblelater/rules/taskfiles.py b/ansiblelater/rules/taskfiles.py deleted file mode 100644 index 5d1bfaa..0000000 --- a/ansiblelater/rules/taskfiles.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Checks related to ansible task files.""" - -import re -from collections import defaultdict - -from ansiblelater.command.candidates import Error -from ansiblelater.command.candidates import Result -from ansiblelater.utils.rulehelper import get_normalized_tasks -from ansiblelater.utils.rulehelper import get_normalized_yaml - - -def check_line_between_tasks(candidate, settings): - options = defaultdict(dict) - options.update(remove_empty=False) - options.update(remove_markers=False) - - lines, line_errors = get_normalized_yaml(candidate, settings, options) - tasks, task_errors = get_normalized_tasks(candidate, settings) - description = "missing task separation (required: 1 empty line)" - - task_regex = re.compile(r"-\sname:(.*)") - prevline = "#file_start_marker" - - allowed_prevline = ["---", "tasks:", "pre_tasks:", "post_tasks:", "block:"] - - errors = task_errors + line_errors - if not errors: - for i, line in lines: - match = task_regex.search(line) - if match and prevline: - name = match.group(1).strip() - - if not any(task.get("name") == name for task in tasks): - continue - - if not any(item in prevline for item in allowed_prevline): - errors.append(Error(i, description)) - - prevline = line.strip() - - return Result(candidate.path, errors) diff --git a/ansiblelater/rules/yamlfiles.py b/ansiblelater/rules/yamlfiles.py deleted file mode 100644 index d773560..0000000 --- a/ansiblelater/rules/yamlfiles.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Checks related to generic YAML syntax (yamllint).""" - -import os - -from ansiblelater.command.candidates import Error -from ansiblelater.command.candidates import Result -from ansiblelater.utils.rulehelper import get_action_tasks -from ansiblelater.utils.rulehelper import get_normalized_task -from ansiblelater.utils.rulehelper import get_normalized_yaml -from ansiblelater.utils.rulehelper import get_raw_yaml -from ansiblelater.utils.rulehelper import run_yamllint - - -def check_yaml_has_content(candidate, settings): - lines, errors = get_normalized_yaml(candidate, settings) - description = "the file appears to have no useful content" - - if (lines and len(lines) == 0) and not errors: - errors.append(Error(None, description)) - - return Result(candidate.path, errors) - - -def check_native_yaml(candidate, settings): - tasks, errors = get_action_tasks(candidate, settings) - description = "task arguments appear to be in key value rather than YAML format" - - if not errors: - for task in tasks: - normal_form, error = get_normalized_task(task, candidate, settings) - if error: - errors.extend(error) - break - - action = normal_form["action"]["__ansible_module__"] - arguments = [ - bytes(x, "utf-8").decode("unicode_escape") - for x in 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] - task_action = bytes(task[action].strip(), "utf-8").decode("unicode_escape") - if task_action.split() != arguments: - errors.append(Error(task["__line__"], description)) - return Result(candidate.path, errors) - - -def check_yaml_empty_lines(candidate, settings): - options = "rules: {{empty-lines: {conf}}}".format(conf=settings["yamllint"]["empty-lines"]) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_indent(candidate, settings): - options = "rules: {{indentation: {conf}}}".format(conf=settings["yamllint"]["indentation"]) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_hyphens(candidate, settings): - options = "rules: {{hyphens: {conf}}}".format(conf=settings["yamllint"]["hyphens"]) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_document_start(candidate, settings): - options = "rules: {{document-start: {conf}}}".format( - conf=settings["yamllint"]["document-start"] - ) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_document_end(candidate, settings): - options = "rules: {{document-end: {conf}}}".format(conf=settings["yamllint"]["document-end"]) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_colons(candidate, settings): - options = "rules: {{colons: {conf}}}".format(conf=settings["yamllint"]["colons"]) - errors = run_yamllint(candidate, options) - return Result(candidate.path, errors) - - -def check_yaml_file(candidate, settings): - errors = [] - filename = candidate.path - - if os.path.isfile(filename) and os.path.splitext(filename)[1][1:] != "yml": - errors.append(Error(None, "file does not have a .yml extension")) - elif os.path.isfile(filename) and os.path.splitext(filename)[1][1:] == "yml": - content, errors = get_raw_yaml(candidate, settings) - - return Result(candidate.path, errors) diff --git a/ansiblelater/settings.py b/ansiblelater/settings.py index 3fd1a34..1d7cba6 100644 --- a/ansiblelater/settings.py +++ b/ansiblelater/settings.py @@ -45,8 +45,8 @@ class Settings(object): defaults = self._get_defaults() self.config_file = args.get("config_file") or default_config_file - args.pop("config_file", None) tmp_args = dict(filter(lambda item: item[1] is not None, args.items())) + tmp_args.pop("config_file", None) tmp_dict = {} for key, value in tmp_args.items(): @@ -102,13 +102,22 @@ class Settings(object): if f not in defaults["ansible"]["custom_modules"]: defaults["ansible"]["custom_modules"].append(f) + if defaults["rules"]["buildin"]: + defaults["rules"]["standards"].append( + os.path.join(resource_filename("ansiblelater", "rules")) + ) + + defaults["rules"]["standards"] = [ + os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["standards"] + ] + return defaults def _get_defaults(self): - rules_dir = os.path.join(resource_filename("ansiblelater", "data")) defaults = { "rules": { - "standards": rules_dir, + "buildin": True, + "standards": [], "filter": [], "exclude_filter": [], "ignore_dotfiles": True, diff --git a/ansiblelater/standard.py b/ansiblelater/standard.py index fc54244..d398c0d 100644 --- a/ansiblelater/standard.py +++ b/ansiblelater/standard.py @@ -1,28 +1,362 @@ """Standard definition.""" +import codecs +import importlib +import inspect +import os +import pathlib +import re +from abc import ABCMeta +from abc import abstractmethod +from collections import defaultdict -class Standard(object): - """ - Standard definition for all defined rules. +import toolz +import yaml +from yamllint import linter +from yamllint.config import YamlLintConfig - Later lookup the config file for a path to a rules directory - or fallback to default `ansiblelater/data/*`. - """ +from ansiblelater.exceptions import LaterAnsibleError +from ansiblelater.exceptions import LaterError +from ansiblelater.utils import Singleton +from ansiblelater.utils import sysexit_with_message +from ansiblelater.utils.yamlhelper import UnsafeTag +from ansiblelater.utils.yamlhelper import action_tasks +from ansiblelater.utils.yamlhelper import normalize_task +from ansiblelater.utils.yamlhelper import normalized_yaml +from ansiblelater.utils.yamlhelper import parse_yaml_linenumbers - def __init__(self, standard_dict): - """ - Initialize a new standard object and returns None. - :param standard_dict: Dictionary object containing all neseccary attributes +class StandardMeta(type): - """ - self.id = standard_dict.get("id", "") - self.name = standard_dict.get("name") - self.version = standard_dict.get("version") - self.check = standard_dict.get("check") - self.types = standard_dict.get("types") + def __call__(cls, *args, **kwargs): + mcls = type.__call__(cls, *args) + setattr(mcls, "sid", cls.sid) + setattr(mcls, "description", getattr(cls, "description", "__unknown__")) + setattr(mcls, "helptext", getattr(cls, "helptext", "")) + setattr(mcls, "version", getattr(cls, "version", None)) + setattr(mcls, "types", getattr(cls, "types", [])) + return mcls + + +class StandardExtendedMeta(StandardMeta, ABCMeta): + pass + + +class StandardBase(object, metaclass=StandardExtendedMeta): + + @property + @abstractmethod + def sid(self): + pass + + @abstractmethod + def check(self, candidate, settings): + pass def __repr__(self): # noqa - return "Standard: {name} (version: {version}, types: {types})".format( - name=self.name, version=self.version, types=self.types + return "Standard: {description} (version: {version}, types: {types})".format( + description=self.description, version=self.version, types=self.types ) + + @staticmethod + def get_tasks(candidate, settings): + errors = [] + yamllines = [] + + if not candidate.faulty: + try: + with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: + yamllines = parse_yaml_linenumbers(f, candidate.path) + except LaterError as ex: + e = ex.original + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + except LaterAnsibleError as e: + errors.append( + StandardBase.Error(e.line, "syntax error: {msg}".format(msg=e.message)) + ) + candidate.faulty = True + + return yamllines, errors + + @staticmethod + def get_action_tasks(candidate, settings): + tasks = [] + errors = [] + + if not candidate.faulty: + try: + with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: + yamllines = parse_yaml_linenumbers(f, candidate.path) + + if yamllines: + tasks = action_tasks(yamllines, candidate) + except LaterError as ex: + e = ex.original + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + except LaterAnsibleError as e: + errors.append(StandardBase.Error(e.line, "syntax error: {}".format(e.message))) + candidate.faulty = True + + return tasks, errors + + @staticmethod + def get_normalized_task(task, candidate, settings): + normalized = None + errors = [] + + if not candidate.faulty: + try: + normalized = normalize_task( + task, candidate.path, settings["ansible"]["custom_modules"] + ) + except LaterError as ex: + e = ex.original + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + except LaterAnsibleError as e: + errors.append( + StandardBase.Error(e.line, "syntax error: {msg}".format(msg=e.message)) + ) + candidate.faulty = True + + return normalized, errors + + @staticmethod + def get_normalized_tasks(candidate, settings, full=False): + normalized = [] + errors = [] + + if not candidate.faulty: + try: + with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: + yamllines = parse_yaml_linenumbers(f, candidate.path) + + if yamllines: + tasks = action_tasks(yamllines, candidate) + for task in tasks: + # An empty `tags` block causes `None` to be returned if + # the `or []` is not present - `task.get("tags", [])` + # does not suffice. + + # Deprecated. + if "skip_ansible_lint" in (task.get("tags") or []) and not full: + # No need to normalize_task if we are skipping it. + continue + + if "skip_ansible_later" in (task.get("tags") or []) and not full: + # No need to normalize_task if we are skipping it. + continue + + normalized.append( + normalize_task( + task, candidate.path, settings["ansible"]["custom_modules"] + ) + ) + + except LaterError as ex: + e = ex.original + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + except LaterAnsibleError as e: + errors.append( + StandardBase.Error(e.line, "syntax error: {msg}".format(msg=e.message)) + ) + candidate.faulty = True + + return normalized, errors + + @staticmethod + def get_normalized_yaml(candidate, settings, options=None): + errors = [] + yamllines = [] + + if not candidate.faulty: + if not options: + options = defaultdict(dict) + options.update(remove_empty=True) + options.update(remove_markers=True) + + try: + yamllines = normalized_yaml(candidate.path, options) + except LaterError as ex: + e = ex.original + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + except LaterAnsibleError as e: + errors.append( + StandardBase.Error(e.line, "syntax error: {msg}".format(msg=e.message)) + ) + candidate.faulty = True + + return yamllines, errors + + @staticmethod + def get_raw_yaml(candidate, settings): + content = None + errors = [] + + if not candidate.faulty: + try: + with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: + yaml.add_constructor( + UnsafeTag.yaml_tag, UnsafeTag.yaml_constructor, Loader=yaml.SafeLoader + ) + content = yaml.safe_load(f) + except yaml.YAMLError as e: + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + + return content, errors + + @staticmethod + def run_yamllint(candidate, options="extends: default"): + errors = [] + + if not candidate.faulty: + try: + with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: + yaml.add_constructor( + UnsafeTag.yaml_tag, UnsafeTag.yaml_constructor, Loader=yaml.SafeLoader + ) + yaml.safe_load(f) + + for problem in linter.run(f, YamlLintConfig(options)): + errors.append(StandardBase.Error(problem.line, problem.desc)) + except yaml.YAMLError as e: + errors.append( + StandardBase.Error( + e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem) + ) + ) + candidate.faulty = True + + return errors + + @staticmethod + def get_first_cmd_arg(task): + if "cmd" in task["action"]: + first_cmd_arg = task["action"]["cmd"].split()[0] + elif "argv" in task["action"]: + first_cmd_arg = task["action"]["argv"][0] + else: + first_cmd_arg = task["action"]["__ansible_arguments__"][0] + + return first_cmd_arg + + class Error(object): + """Default error object created if a rule failed.""" + + def __init__(self, lineno, message, error_type=None, **kwargs): + """ + Initialize a new error object and returns None. + + :param lineno: Line number where the error from de rule occures + :param message: Detailed error description provided by the rule + + """ + self.lineno = lineno + self.message = message + self.kwargs = kwargs + for (key, value) in kwargs.items(): + setattr(self, key, value) + + def __repr__(self): # noqa + if self.lineno: + return "{no}: {msg}".format(no=self.lineno, msg=self.message) + else: + return " {msg}".format(msg=self.message) + + def to_dict(self): + result = dict(lineno=self.lineno, message=self.message) + for (key, value) in self.kwargs.items(): + result[key] = value + return result + + class Result(object): + """Generic result object.""" + + def __init__(self, candidate, errors=None): + self.candidate = candidate + self.errors = errors or [] + + def message(self): + return "\n".join(["{0}:{1}".format(self.candidate, error) for error in self.errors]) + + +class StandardLoader(): + + def __init__(self, source): + self.rules = [] + + for s in source: + for p in pathlib.Path(s).glob("*.py"): + filename = os.path.splitext(os.path.basename(p))[0] + if not re.match(r"^[A-Za-z]+$", filename): + continue + + spec = importlib.util.spec_from_file_location(filename, p) + module = importlib.util.module_from_spec(spec) + + try: + spec.loader.exec_module(module) + except (ImportError, NameError) as e: + sysexit_with_message( + "Failed to load roles file {module}: \n {msg}".format( + msg=str(e), module=filename + ) + ) + + try: + for name, obj in inspect.getmembers(module): + if self._is_plugin(obj): + self.rules.append(obj()) + except TypeError as e: + sysexit_with_message("Failed to load roles file: \n {msg}".format(msg=str(e))) + + self.validate() + + def _is_plugin(self, obj): + return inspect.isclass(obj) and issubclass( + obj, StandardBase + ) and obj is not StandardBase and not None + + def validate(self): + normalized_std = (list(toolz.remove(lambda x: x.sid == "", self.rules))) + unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.sid))) + all_std = len(normalized_std) + if not all_std == unique_std: + sysexit_with_message( + "Detect duplicate ID's in standards definition. Please use unique ID's only." + ) + + +class SingleStandards(StandardLoader, metaclass=Singleton): + """Singleton config class.""" + + pass diff --git a/ansiblelater/test/config/config.ini b/ansiblelater/test/config/config.ini deleted file mode 100644 index 9c82381..0000000 --- a/ansiblelater/test/config/config.ini +++ /dev/null @@ -1,2 +0,0 @@ -[rules] -standards = tests/config diff --git a/ansiblelater/test/config/standards.py b/ansiblelater/test/config/standards.py deleted file mode 100644 index 346512f..0000000 --- a/ansiblelater/test/config/standards.py +++ /dev/null @@ -1,259 +0,0 @@ -from ansiblelater import Standard -from ansiblelater.rules.ansiblefiles import check_braces_spaces -from ansiblelater.rules.ansiblefiles import check_command_has_changes -from ansiblelater.rules.ansiblefiles import check_command_instead_of_module -from ansiblelater.rules.ansiblefiles import check_compare_to_literal_bool -from ansiblelater.rules.ansiblefiles import check_empty_string_compare -from ansiblelater.rules.ansiblefiles import check_install_use_latest -from ansiblelater.rules.ansiblefiles import check_name_format -from ansiblelater.rules.ansiblefiles import check_named_task -from ansiblelater.rules.ansiblefiles import check_shell_instead_command -from ansiblelater.rules.ansiblefiles import check_unique_named_task -from ansiblelater.rules.rolefiles import check_meta_main -from ansiblelater.rules.rolefiles import check_scm_in_src -from ansiblelater.rules.taskfiles import check_line_between_tasks -from ansiblelater.rules.yamlfiles import check_native_yaml -from ansiblelater.rules.yamlfiles import check_yaml_colons -from ansiblelater.rules.yamlfiles import check_yaml_document_start -from ansiblelater.rules.yamlfiles import check_yaml_empty_lines -from ansiblelater.rules.yamlfiles import check_yaml_file -from ansiblelater.rules.yamlfiles import check_yaml_has_content -from ansiblelater.rules.yamlfiles import check_yaml_hyphens -from ansiblelater.rules.yamlfiles import check_yaml_indent - -tasks_should_be_separated = Standard( - dict( - id="ANSIBLE0001", - name="Single tasks should be separated by empty line", - check=check_line_between_tasks, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -role_must_contain_meta_main = Standard( - dict( - id="ANSIBLE0002", - name="Roles must contain suitable meta/main.yml", - check=check_meta_main, - version="0.1", - types=["meta"] - ) -) - -tasks_are_uniquely_named = Standard( - dict( - id="ANSIBLE0003", - name="Tasks and handlers must be uniquely named within a single file", - check=check_unique_named_task, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -use_spaces_between_variable_braces = Standard( - dict( - id="ANSIBLE0004", - name="YAML should use consistent number of spaces around variables", - check=check_braces_spaces, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -roles_scm_not_in_src = Standard( - dict( - id="ANSIBLE0005", - name="Use scm key rather than src: scm+url", - check=check_scm_in_src, - version="0.1", - types=["rolesfile"] - ) -) - -tasks_are_named = Standard( - dict( - id="ANSIBLE0006", - name="Tasks and handlers must be named", - check=check_named_task, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -tasks_names_are_formatted = Standard( - dict( - id="ANSIBLE0007", - name="Name of tasks and handlers must be formatted", - check=check_name_format, - version="0.1", - types=["playbook", "task", "handler"], - ) -) - -commands_should_not_be_used_in_place_of_modules = Standard( - dict( - id="ANSIBLE0008", - name="Commands should not be used in place of modules", - check=check_command_instead_of_module, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -package_installs_should_not_use_latest = Standard( - dict( - id="ANSIBLE0009", - name="Package installs should use present, not latest", - check=check_install_use_latest, - types=["playbook", "task", "handler"] - ) -) - -use_shell_only_when_necessary = Standard( - dict( - id="ANSIBLE0010", - name="Shell should only be used when essential", - check=check_shell_instead_command, - types=["playbook", "task", "handler"] - ) -) - -commands_should_be_idempotent = Standard( - dict( - id="ANSIBLE0011", - name="Commands should be idempotent", - check=check_command_has_changes, - version="0.1", - types=["playbook", "task"] - ) -) - -dont_compare_to_empty_string = Standard( - dict( - id="ANSIBLE0012", - name="Don't compare to \"\" - use `when: var` or `when: not var`", - check=check_empty_string_compare, - version="0.1", - types=["playbook", "task", "handler", "template"] - ) -) - -dont_compare_to_literal_bool = Standard( - dict( - id="ANSIBLE0013", - name="Don't compare to True or False - use `when: var` or `when: not var`", - check=check_compare_to_literal_bool, - version="0.1", - types=["playbook", "task", "handler", "template"] - ) -) - -files_should_not_contain_unnecessarily_empty_lines = Standard( - dict( - id="LINT0001", - name="YAML should not contain unnecessarily empty lines", - check=check_yaml_empty_lines, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_be_indented = Standard( - dict( - id="LINT0002", - name="YAML should be correctly indented", - check=check_yaml_indent, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_use_consistent_spaces_after_hyphens = Standard( - dict( - id="LINT0003", - name="YAML should use consistent number of spaces after hyphens", - check=check_yaml_hyphens, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -files_should_contain_document_start_marker = Standard( - dict( - id="LINT0004", - name="YAML should contain document start marker", - check=check_yaml_document_start, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -spaces_around_colons = Standard( - dict( - id="LINT0005", - name="YAML should use consistent number of spaces around colons", - check=check_yaml_colons, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"] - ) -) - -rolesfile_should_be_in_yaml = Standard( - dict( - id="LINT0006", - name="Roles file should be in yaml format", - check=check_yaml_file, - version="0.1", - types=["rolesfile"] - ) -) - -files_should_not_be_purposeless = Standard( - dict( - id="LINT0007", - name="Files should contain useful content", - check=check_yaml_has_content, - version="0.1", - types=["playbook", "task", "handler", "rolevars", "defaults", "meta"] - ) -) - -use_yaml_rather_than_key_value = Standard( - dict( - id="LINT0008", - name="Use YAML format for tasks and handlers rather than key=value", - check=check_native_yaml, - version="0.1", - types=["playbook", "task", "handler"] - ) -) - -ansible_min_version = "2.1" -ansible_later_min_version = "0.1.0" - -standards = [ - # Ansible - tasks_should_be_separated, - role_must_contain_meta_main, - tasks_are_uniquely_named, - use_spaces_between_variable_braces, - roles_scm_not_in_src, - tasks_are_named, - tasks_names_are_formatted, - commands_should_not_be_used_in_place_of_modules, - package_installs_should_not_use_latest, - use_shell_only_when_necessary, - commands_should_be_idempotent, - dont_compare_to_empty_string, - dont_compare_to_literal_bool, - # Lint - files_should_not_contain_unnecessarily_empty_lines, - files_should_be_indented, - files_should_use_consistent_spaces_after_hyphens, - files_should_contain_document_start_marker, - spaces_around_colons, - rolesfile_should_be_in_yaml, - files_should_not_be_purposeless, - use_yaml_rather_than_key_value, -] diff --git a/ansiblelater/test/data/yaml_fail.yml b/ansiblelater/test/data/yaml_fail.yml deleted file mode 100644 index c08574e..0000000 --- a/ansiblelater/test/data/yaml_fail.yml +++ /dev/null @@ -1,5 +0,0 @@ -- start: - - overindented - - misaligned -- next: - - underindented diff --git a/ansiblelater/test/data/yaml_success.yml b/ansiblelater/test/data/yaml_success.yml deleted file mode 100644 index 0b6a4d3..0000000 --- a/ansiblelater/test/data/yaml_success.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Standards: 0.1 ---- -- block: - - name: hello - command: echo hello - - - name: task2 - debug: - msg: hello - when: some_var_is_true - -- name: another task - debug: - msg: another msg - -- fail: - msg: this is actually valid indentation diff --git a/ansiblelater/utils/__init__.py b/ansiblelater/utils/__init__.py index 70e7df3..37838fd 100644 --- a/ansiblelater/utils/__init__.py +++ b/ansiblelater/utils/__init__.py @@ -111,3 +111,14 @@ def sysexit(code=1): def sysexit_with_message(msg, code=1): LOG.critical(msg) sysexit(code) + + +class Singleton(type): + """Meta singleton class.""" + + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/ansiblelater/utils/rulehelper.py b/ansiblelater/utils/rulehelper.py deleted file mode 100644 index 23e1c7f..0000000 --- a/ansiblelater/utils/rulehelper.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Abstracted methods to simplify role writeup.""" - -import codecs -from collections import defaultdict - -import yaml -from yamllint import linter -from yamllint.config import YamlLintConfig - -from ansiblelater.command.candidates import Error -from ansiblelater.exceptions import LaterAnsibleError -from ansiblelater.exceptions import LaterError - -from .yamlhelper import UnsafeTag -from .yamlhelper import action_tasks -from .yamlhelper import normalize_task -from .yamlhelper import normalized_yaml -from .yamlhelper import parse_yaml_linenumbers - - -def get_tasks(candidate, settings): - errors = [] - yamllines = [] - - if not candidate.faulty: - try: - with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: - yamllines = parse_yaml_linenumbers(f, candidate.path) - except LaterError as ex: - e = ex.original - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - except LaterAnsibleError as e: - errors.append(Error(e.line, "syntax error: {msg}".format(msg=e.message))) - candidate.faulty = True - - return yamllines, errors - - -def get_action_tasks(candidate, settings): - tasks = [] - errors = [] - - if not candidate.faulty: - try: - with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: - yamllines = parse_yaml_linenumbers(f, candidate.path) - - if yamllines: - tasks = action_tasks(yamllines, candidate) - except LaterError as ex: - e = ex.original - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - except LaterAnsibleError as e: - errors.append(Error(e.line, "syntax error: {}".format(e.message))) - candidate.faulty = True - - return tasks, errors - - -def get_normalized_task(task, candidate, settings): - normalized = None - errors = [] - - if not candidate.faulty: - try: - normalized = normalize_task( - task, candidate.path, settings["ansible"]["custom_modules"] - ) - except LaterError as ex: - e = ex.original - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - except LaterAnsibleError as e: - errors.append(Error(e.line, "syntax error: {msg}".format(msg=e.message))) - candidate.faulty = True - - return normalized, errors - - -def get_normalized_tasks(candidate, settings, full=False): - normalized = [] - errors = [] - - if not candidate.faulty: - try: - with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: - yamllines = parse_yaml_linenumbers(f, candidate.path) - - if yamllines: - tasks = action_tasks(yamllines, candidate) - for task in tasks: - # An empty `tags` block causes `None` to be returned if - # the `or []` is not present - `task.get("tags", [])` - # does not suffice. - - # Deprecated. - if "skip_ansible_lint" in (task.get("tags") or []) and not full: - # No need to normalize_task if we are skipping it. - continue - - if "skip_ansible_later" in (task.get("tags") or []) and not full: - # No need to normalize_task if we are skipping it. - continue - - normalized.append( - normalize_task( - task, candidate.path, settings["ansible"]["custom_modules"] - ) - ) - - except LaterError as ex: - e = ex.original - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - except LaterAnsibleError as e: - errors.append(Error(e.line, "syntax error: {msg}".format(msg=e.message))) - candidate.faulty = True - - return normalized, errors - - -def get_normalized_yaml(candidate, settings, options=None): - errors = [] - yamllines = None - - if not candidate.faulty: - if not options: - options = defaultdict(dict) - options.update(remove_empty=True) - options.update(remove_markers=True) - - try: - yamllines = normalized_yaml(candidate.path, options) - except LaterError as ex: - e = ex.original - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - except LaterAnsibleError as e: - errors.append(Error(e.line, "syntax error: {msg}".format(msg=e.message))) - candidate.faulty = True - - return yamllines, errors - - -def get_raw_yaml(candidate, settings): - content = None - errors = [] - - if not candidate.faulty: - try: - with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: - yaml.add_constructor( - UnsafeTag.yaml_tag, UnsafeTag.yaml_constructor, Loader=yaml.SafeLoader - ) - content = yaml.safe_load(f) - except yaml.YAMLError as e: - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - - return content, errors - - -def run_yamllint(candidate, options="extends: default"): - errors = [] - - if not candidate.faulty: - try: - with codecs.open(candidate.path, mode="rb", encoding="utf-8") as f: - yaml.add_constructor( - UnsafeTag.yaml_tag, UnsafeTag.yaml_constructor, Loader=yaml.SafeLoader - ) - yaml.safe_load(f) - - for problem in linter.run(f, YamlLintConfig(options)): - errors.append(Error(problem.line, problem.desc)) - except yaml.YAMLError as e: - errors.append( - Error(e.problem_mark.line + 1, "syntax error: {msg}".format(msg=e.problem)) - ) - candidate.faulty = True - - return errors - - -def get_first_cmd_arg(task): - if "cmd" in task["action"]: - first_cmd_arg = task["action"]["cmd"].split()[0] - elif "argv" in task["action"]: - first_cmd_arg = task["action"]["argv"][0] - else: - first_cmd_arg = task["action"]["__ansible_arguments__"][0] - - return first_cmd_arg diff --git a/docs/content/build_rules/standards_check.md b/docs/content/build_rules/standards_check.md index a9549f9..cc885b9 100644 --- a/docs/content/build_rules/standards_check.md +++ b/docs/content/build_rules/standards_check.md @@ -1,5 +1,5 @@ --- -title: Minimal standards checks +title: Minimal standard checks --- A typical standards check will look like: @@ -7,18 +7,27 @@ A typical standards check will look like: {{< highlight Python "linenos=table" >}} -def check_playbook_for_something(candidate, settings): - result = Result(candidate.path) # empty result is a success with no output - with open(candidate.path, 'r') as f: - for (lineno, line) in enumerate(f): - if line is dodgy: - # enumerate is 0-based so add 1 to lineno - result.errors.append(Error(lineno+1, "Line is dodgy: reasons")) - return result +class CheckBecomeUser(StandardBase): + + sid = "ANSIBLE0015" + description = "Become should be combined with become_user" + helptext = "the task has `become` enabled but `become_user` is missing" + version = "0.1" + types = ["playbook", "task", "handler"] + + def check(self, candidate, settings): + tasks, errors = self.get_normalized_tasks(candidate, settings) + true_value = [True, "true", "True", "TRUE", "yes", "Yes", "YES"] + + if not errors: + gen = (task for task in tasks if "become" in task) + for task in gen: + if task["become"] in true_value and "become_user" not in task.keys(): + errors.append(self.Error(task["__line__"], self.helptext)) + + return self.Result(candidate.path, errors) {{< /highlight >}} -All standards check take a candidate object, which has a path attribute. The type can be inferred from the class name (i.e. `type(candidate).__name__`) or from the table [here](#candidates). - -They return a `Result` object, which contains a possibly empty list of `Error` objects. `Error` objects are formed of a line number and a message. If the error applies to the whole file being reviewed, set the line number to `None`. Line numbers are important as `ansible-later` can review just ranges of files to only review changes (e.g. through piping the output of `git diff` to `ansible-later`). +They return a `Result` object, which contains a possibly empty list of `Error` objects. `Error` objects are formed of a line number and a message. If the error applies to the whole file being reviewed, set the line number to `None`. diff --git a/docs/content/build_rules/standards_file.md b/docs/content/build_rules/standards_file.md deleted file mode 100644 index 1dec295..0000000 --- a/docs/content/build_rules/standards_file.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: The standards file ---- - -A standards file comprises a list of standards, and optionally some methods to -check those standards. - -Create a file called standards.py (this can import other modules) - - - -{{< highlight Python "linenos=table" >}} -from ansiblelater include Standard, Result - -tasks_are_uniquely_named = Standard(dict( - # ID's are optional but if you use ID's they have to be unique - id="ANSIBLE0003", - # Short description of the standard goal - name="Tasks and handlers must be uniquely named within a single file", - check=check_unique_named_task, - version="0.1", - types=["playbook", "task", "handler"], -)) - -standards = [ - tasks_are_uniquely_named, - role_must_contain_meta_main, -] -{{< /highlight >}} - - - -When you add new standards, you should increment the version of your standards. Your playbooks and roles should declare what version of standards you are using, otherwise ansible-later assumes you're using the latest. The declaration is done by adding standards version as first line in the file. - - - - -{{< highlight INI "linenos=table" >}} -# Standards: 1.2 -{{< /highlight >}} - - - - -To add standards that are advisory, don't set the version. These will cause a message to be displayed but won't constitute a failure. When a standard version is higher than declared version, a message will be displayed 'WARN: Future standard' and won't constitute a failure. - -An example standards file is available [here](https://github.com/thegeeklab/ansible-later/blob/main/ansiblelater/data/standards.py). - -If you only want to check one or two standards quickly (perhaps you want to review your entire code base for deprecated bare words), you can use the `-s` flag with the name of your standard. You can pass `-s` multiple times. - - - -{{< highlight Shell "linenos=table" >}} -git ls-files | xargs ansible-later -s "bare words are deprecated for with_items" -{{< /highlight >}} - - - -You can see the name of the standards being checked for each different file by running `ansible-later` with the `-v` option. diff --git a/docs/content/included_rules/_index.md b/docs/content/included_rules/_index.md index 56f64d8..3fd873d 100644 --- a/docs/content/included_rules/_index.md +++ b/docs/content/included_rules/_index.md @@ -4,31 +4,31 @@ title: Included rules Reviews are nothing without some rules or standards against which to review. ansible-later comes with a couple of built-in checks explained in the following table. -| Rule | ID | Description | Parameter | -| --------------------------------- | ----------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | -| check_yaml_empty_lines | LINT0001 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} | -| check_yaml_indent | LINT0002 | YAML should be correctly indented. | {spaces: 2, check-multi-line-strings: false, indent-sequences: true} | -| check_yaml_hyphens | LINT0003 | YAML should use consitent number of spaces after hyphens (-). | {max-spaces-after: 1} | -| check_yaml_document_start | LINT0004 | YAML should contain document start marker. | {document-start: {present: true}} | -| check_yaml_colons | LINT0005 | YAML should use consitent number of spaces around colons. | {colons: {max-spaces-before: 0, max-spaces-after: 1}} | -| check_yaml_file | LINT0006 | Roles file should be in yaml format. | | -| check_yaml_has_content | LINT0007 | Files should contain useful content. | | -| check_native_yaml | LINT0008 | Use YAML format for tasks and handlers rather than key=value. | | -| check_yaml_document_end | LINT0009 | YAML should contain document end marker. | {document-end: {present: true}} | -| check_line_between_tasks | ANSIBLE0001 | Single tasks should be separated by an empty line. | | -| check_meta_main | ANSIBLE0002 | Meta file should contain a basic subset of parameters. | author, description, min_ansible_version, platforms, dependencies | -| check_unique_named_task | ANSIBLE0003 | Tasks and handlers must be uniquely named within a file. | | -| check_braces | ANSIBLE0004 | YAML should use consitent number of spaces around variables. | | -| check_scm_in_src | ANSIBLE0005 | Use scm key rather than src: scm+url in requirements file. | | -| check_named_task | ANSIBLE0006 | Tasks and handlers must be named. | excludes: meta, debug, include\_\*, import\_\*, block | -| check_name_format | ANSIBLE0007 | Name of tasks and handlers must be formatted. | formats: first letter capital | -| check_command_instead_of_module | ANSIBLE0008 | Commands should not be used in place of modules. | | -| check_install_use_latest | ANSIBLE0009 | Package managers should not install with state=latest. | | -| check_shell_instead_command | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands. | | -| check_command_has_changes | ANSIBLE0011 | Commands should be idempotent and only used with some checks. | | -| check_empty_string_compare | ANSIBLE0012 | Don't compare to "" - use `when: var` or `when: not var`. | | -| check_compare_to_literal_bool | ANSIBLE0013 | Don't compare to True/False - use `when: var` or `when: not var`. | | -| check_literal_bool_format | ANSIBLE0014 | Literal bools should be written as `True/False` or `yes/no`. | forbidden values are `true false TRUE FALSE Yes No YES NO` | -| check_become_user | ANSIBLE0015 | `become` should be always used combined with `become_user`. | | -| check_filter_separation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | | -| check_command_instead_of_argument | ANSIBLE0017 | Commands should not be used in place of module arguments. | | +| Rule | ID | Description | Parameter | +| ----------------------------- | ----------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- | +| CheckYamlEmptyLines | LINT0001 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} | +| CheckYamlIndent | LINT0002 | YAML should be correctly indented. | {spaces: 2, check-multi-line-strings: false, indent-sequences: true} | +| CheckYamlHyphens | LINT0003 | YAML should use consitent number of spaces after hyphens (-). | {max-spaces-after: 1} | +| CheckYamlDocumentStart | LINT0004 | YAML should contain document start marker. | {document-start: {present: true}} | +| CheckYamlColons | LINT0005 | YAML should use consitent number of spaces around colons. | {colons: {max-spaces-before: 0, max-spaces-after: 1}} | +| CheckYamlFile | LINT0006 | Roles file should be in yaml format. | | +| CheckYamlHasContent | LINT0007 | Files should contain useful content. | | +| CheckNativeYaml | LINT0008 | Use YAML format for tasks and handlers rather than key=value. | | +| CheckYamlDocumentEnd | LINT0009 | YAML should contain document end marker. | {document-end: {present: true}} | +| CheckLineBetweenTasks | ANSIBLE0001 | Single tasks should be separated by an empty line. | | +| CheckMetaMain | ANSIBLE0002 | Meta file should contain a basic subset of parameters. | author, description, min_ansible_version, platforms, dependencies | +| CheckUniqueNamedTask | ANSIBLE0003 | Tasks and handlers must be uniquely named within a file. | | +| CheckBraces | ANSIBLE0004 | YAML should use consitent number of spaces around variables. | | +| CheckScmInSrc | ANSIBLE0005 | Use scm key rather than src: scm+url in requirements file. | | +| CheckNamedTask | ANSIBLE0006 | Tasks and handlers must be named. | excludes: meta, debug, include\_\*, import\_\*, block | +| CheckNameFormat | ANSIBLE0007 | Name of tasks and handlers must be formatted. | formats: first letter capital | +| CheckCommandInsteadofModule | ANSIBLE0008 | Commands should not be used in place of modules. | | +| CheckInstallUseLatest | ANSIBLE0009 | Package managers should not install with state=latest. | | +| CheckShellInsteadCommand | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands. | | +| CheckCommandHasChanges | ANSIBLE0011 | Commands should be idempotent and only used with some checks. | | +| CheckEmptyStringCompare | ANSIBLE0012 | Don't compare to "" - use `when: var` or `when: not var`. | | +| CheckCompareToLiteralBool | ANSIBLE0013 | Don't compare to True/False - use `when: var` or `when: not var`. | | +| CheckLiteralBoolFormat | ANSIBLE0014 | Literal bools should be written as `True/False` or `yes/no`. | forbidden values are `true false TRUE FALSE Yes No YES NO` | +| CheckBecomeUser | ANSIBLE0015 | `become` should be always used combined with `become_user`. | | +| CheckFilterSeparation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | | +| CheckCommandInsteadOfArgument | ANSIBLE0017 | Commands should not be used in place of module arguments. | | diff --git a/docs/data/menu/main.yml b/docs/data/menu/main.yml index d3b79b2..196c80d 100644 --- a/docs/data/menu/main.yml +++ b/docs/data/menu/main.yml @@ -21,8 +21,6 @@ main: ref: "/included_rules" - name: Build your own rules sub: - - name: Standards file - ref: "/build_rules/standards_file" - name: Candidates ref: "/build_rules/candidates" - name: Standards checks diff --git a/poetry.lock b/poetry.lock index 65bf2ec..6089266 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,17 +1,17 @@ [[package]] name = "ansible" -version = "2.10.4" +version = "2.10.5" description = "Radically simple IT automation" category = "main" optional = true python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" [package.dependencies] -ansible-base = ">=2.10.3,<2.11" +ansible-base = ">=2.10.4,<2.11" [[package]] name = "ansible-base" -version = "2.10.4" +version = "2.10.5" description = "Radically simple IT automation" category = "main" optional = true @@ -286,7 +286,7 @@ smmap = ">=3.0.1,<4" [[package]] name = "gitpython" -version = "3.1.11" +version = "3.1.12" description = "Python Git Library" category = "dev" optional = false @@ -297,7 +297,7 @@ gitdb = ">=4.0.1,<5" [[package]] name = "importlib-metadata" -version = "3.3.0" +version = "3.4.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -308,8 +308,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" @@ -321,7 +321,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.6.4" +version = "5.7.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -526,14 +526,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.10.1" +version = "2.11.1" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -coverage = ">=4.4" +coverage = ">=5.2.1" pytest = ">=4.6" [package.extras] @@ -541,7 +541,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", [[package]] name = "pytest-mock" -version = "3.4.0" +version = "3.5.1" description = "Thin-wrapper around the mock package for easier use with pytest" category = "dev" optional = false @@ -563,11 +563,11 @@ python-versions = ">=3.4" [[package]] name = "pyyaml" -version = "5.3.1" +version = "5.4.1" description = "YAML parser and emitter for Python" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "six" @@ -587,8 +587,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "snowballstemmer" -version = "2.0.0" -description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." +version = "2.1.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false python-versions = "*" @@ -607,7 +607,7 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] name = "testfixtures" -version = "6.17.0" +version = "6.17.1" description = "A collection of helpers and mock objects for unit tests and doc tests." category = "dev" optional = false @@ -693,10 +693,10 @@ content-hash = "4ec0d9863ad688c640a6c0a050896f0df6a8c7b7e4ffa5d50ef09b1505b5a3d2 [metadata.files] ansible = [ - {file = "ansible-2.10.4.tar.gz", hash = "sha256:98e718aea82199be62db7731373d660627aa1e938d34446588f2f49c228638ee"}, + {file = "ansible-2.10.5.tar.gz", hash = "sha256:9775229aae31336a624ca5afe5533fea5e49ef4daa96a96791dd9871b2d8b8d1"}, ] ansible-base = [ - {file = "ansible-base-2.10.4.tar.gz", hash = "sha256:d4dad569864c08d8efb6ad99acf48ec46d7d118f8ced64f1185f8eac2c280ec3"}, + {file = "ansible-base-2.10.5.tar.gz", hash = "sha256:33ae323923b841f3d822f355380ce7c92610440362efeed67b4b39db41e555af"}, ] anyconfig = [ {file = "anyconfig-0.10.0-py2.py3-none-any.whl", hash = "sha256:156aa990976d068dec63e1e250ba130a32d48c4b7a8d4f12137b8b74074bbf3f"}, @@ -876,20 +876,20 @@ gitdb = [ {file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"}, ] gitpython = [ - {file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"}, - {file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"}, + {file = "GitPython-3.1.12-py3-none-any.whl", hash = "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"}, + {file = "GitPython-3.1.12.tar.gz", hash = "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac"}, ] importlib-metadata = [ - {file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"}, - {file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"}, + {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"}, + {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, - {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, + {file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"}, + {file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"}, ] jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, @@ -993,30 +993,38 @@ pytest = [ {file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"}, ] pytest-cov = [ - {file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"}, - {file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"}, + {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, + {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, ] pytest-mock = [ - {file = "pytest-mock-3.4.0.tar.gz", hash = "sha256:c3981f5edee6c4d1942250a60d9b39d38d5585398de1bfce057f925bdda720f4"}, - {file = "pytest_mock-3.4.0-py3-none-any.whl", hash = "sha256:c0fc979afac4aaba545cbd01e9c20736eb3fefb0a066558764b07d3de8f04ed3"}, + {file = "pytest-mock-3.5.1.tar.gz", hash = "sha256:a1e2aba6af9560d313c642dae7e00a2a12b022b80301d9d7fc8ec6858e1dd9fc"}, + {file = "pytest_mock-3.5.1-py3-none-any.whl", hash = "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e"}, ] python-json-logger = [ {file = "python-json-logger-2.0.1.tar.gz", hash = "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398"}, ] pyyaml = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, - {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, + {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, + {file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"}, + {file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"}, + {file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"}, + {file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"}, + {file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"}, + {file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"}, + {file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"}, + {file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"}, + {file = "PyYAML-5.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46"}, + {file = "PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb"}, + {file = "PyYAML-5.4.1-cp38-cp38-win32.whl", hash = "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc"}, + {file = "PyYAML-5.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696"}, + {file = "PyYAML-5.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77"}, + {file = "PyYAML-5.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183"}, + {file = "PyYAML-5.4.1-cp39-cp39-win32.whl", hash = "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10"}, + {file = "PyYAML-5.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db"}, + {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] six = [ {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, @@ -1027,16 +1035,16 @@ smmap = [ {file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"}, - {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, + {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, + {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, ] stevedore = [ {file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"}, {file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"}, ] testfixtures = [ - {file = "testfixtures-6.17.0-py2.py3-none-any.whl", hash = "sha256:ebcc3e024d47bb58a60cdc678604151baa0c920ae2814004c89ac9066de31b2c"}, - {file = "testfixtures-6.17.0.tar.gz", hash = "sha256:fa7c170df68ca6367eda061e9ec339ae3e6d3679c31e04033f83ef97a7d7d0ce"}, + {file = "testfixtures-6.17.1-py2.py3-none-any.whl", hash = "sha256:9ed31e83f59619e2fa17df053b241e16e0608f4580f7b5a9333a0c9bdcc99137"}, + {file = "testfixtures-6.17.1.tar.gz", hash = "sha256:5ec3a0dd6f71cc4c304fbc024a10cc293d3e0b852c868014b9f233203e149bda"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, diff --git a/setup.cfg b/setup.cfg index 0111df1..de19fc4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,14 @@ [flake8] # Explanation of errors # +# D100: Missing docstring in public module +# D101: Missing docstring in public class # D102: Missing docstring in public method # D103: Missing docstring in public function # D107: Missing docstring in __init__ # D202: No blank lines allowed after function docstring # W503:Line break occurred before a binary operator -ignore = D102, D103, D107, D202, W503 +ignore = D100, D101, D102, D103, D107, D202, W503 max-line-length = 99 inline-quotes = double exclude = .git, __pycache__, build, dist, test, *.pyc, *.egg-info, .cache, .eggs, env*