refactor plugin system to use a class-based approach (#68)

* refactor plugin system to use a class-based approach

* disable some docstring linter errors and fix imports

* cleanup

* fix docs

* add metavars to cli arguments for better helptext

* add option to disable buildin rules

* remove print

* remove dead code
This commit is contained in:
Robert Kaussow 2021-01-30 16:52:48 +01:00 committed by GitHub
parent 16dcc0a3f7
commit 43d7edca32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1353 additions and 1759 deletions

View File

@ -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__":

View File

@ -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

View File

@ -1 +0,0 @@
# noqa

View File

@ -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

View File

@ -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,
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,64 @@
# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
#
# 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -1 +0,0 @@
# noqa

View File

@ -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 <will@thames.id.au>
#
# 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -1,2 +0,0 @@
[rules]
standards = tests/config

View File

@ -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,
]

View File

@ -1,5 +0,0 @@
- start:
- overindented
- misaligned
- next:
- underindented

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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:
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< 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 >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->
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`.

View File

@ -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)
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< 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 >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->
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.
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<!-- spellchecker-disable -->
{{< highlight INI "linenos=table" >}}
# Standards: 1.2
{{< /highlight >}}
<!-- spellchecker-enable -->
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
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.
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< highlight Shell "linenos=table" >}}
git ls-files | xargs ansible-later -s "bare words are deprecated for with_items"
{{< /highlight >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->
You can see the name of the standards being checked for each different file by running `ansible-later` with the `-v` option.

View File

@ -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. | |

View File

@ -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

98
poetry.lock generated
View File

@ -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"},

View File

@ -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*