mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-24 22:00:40 +00:00
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:
parent
16dcc0a3f7
commit
43d7edca32
@ -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__":
|
||||
|
@ -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
|
@ -1 +0,0 @@
|
||||
# noqa
|
@ -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
|
@ -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,
|
||||
]
|
22
ansiblelater/rules/CheckBecomeUser.py
Normal file
22
ansiblelater/rules/CheckBecomeUser.py
Normal 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)
|
47
ansiblelater/rules/CheckBracesSpaces.py
Normal file
47
ansiblelater/rules/CheckBracesSpaces.py
Normal 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)
|
29
ansiblelater/rules/CheckCommandHasChanges.py
Normal file
29
ansiblelater/rules/CheckCommandHasChanges.py
Normal 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)
|
64
ansiblelater/rules/CheckCommandInsteadOfArgument.py
Normal file
64
ansiblelater/rules/CheckCommandInsteadOfArgument.py
Normal 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)
|
53
ansiblelater/rules/CheckCommandInsteadOfModule.py
Normal file
53
ansiblelater/rules/CheckCommandInsteadOfModule.py
Normal 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)
|
37
ansiblelater/rules/CheckCompareToLiteralBool.py
Normal file
37
ansiblelater/rules/CheckCompareToLiteralBool.py
Normal 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)
|
26
ansiblelater/rules/CheckDeprecated.py
Normal file
26
ansiblelater/rules/CheckDeprecated.py
Normal 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)
|
37
ansiblelater/rules/CheckEmptyStringCompare.py
Normal file
37
ansiblelater/rules/CheckEmptyStringCompare.py
Normal 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)
|
31
ansiblelater/rules/CheckFilterSeparation.py
Normal file
31
ansiblelater/rules/CheckFilterSeparation.py
Normal 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)
|
29
ansiblelater/rules/CheckInstallUseLatest.py
Normal file
29
ansiblelater/rules/CheckInstallUseLatest.py
Normal 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)
|
24
ansiblelater/rules/CheckLiteralBoolFormat.py
Normal file
24
ansiblelater/rules/CheckLiteralBoolFormat.py
Normal 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)
|
23
ansiblelater/rules/CheckMetaMain.py
Normal file
23
ansiblelater/rules/CheckMetaMain.py
Normal 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)
|
26
ansiblelater/rules/CheckNameFormat.py
Normal file
26
ansiblelater/rules/CheckNameFormat.py
Normal 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)
|
27
ansiblelater/rules/CheckNamedTask.py
Normal file
27
ansiblelater/rules/CheckNamedTask.py
Normal 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)
|
36
ansiblelater/rules/CheckNativeYaml.py
Normal file
36
ansiblelater/rules/CheckNativeYaml.py
Normal 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)
|
23
ansiblelater/rules/CheckScmInSrc.py
Normal file
23
ansiblelater/rules/CheckScmInSrc.py
Normal 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)
|
34
ansiblelater/rules/CheckShellInsteadCommand.py
Normal file
34
ansiblelater/rules/CheckShellInsteadCommand.py
Normal 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)
|
43
ansiblelater/rules/CheckTaskSeparation.py
Normal file
43
ansiblelater/rules/CheckTaskSeparation.py
Normal 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)
|
27
ansiblelater/rules/CheckUniqueNamedTask.py
Normal file
27
ansiblelater/rules/CheckUniqueNamedTask.py
Normal 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)
|
15
ansiblelater/rules/CheckYamlColons.py
Normal file
15
ansiblelater/rules/CheckYamlColons.py
Normal 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)
|
17
ansiblelater/rules/CheckYamlDocumentEnd.py
Normal file
17
ansiblelater/rules/CheckYamlDocumentEnd.py
Normal 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)
|
17
ansiblelater/rules/CheckYamlDocumentStart.py
Normal file
17
ansiblelater/rules/CheckYamlDocumentStart.py
Normal 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)
|
15
ansiblelater/rules/CheckYamlEmptyLines.py
Normal file
15
ansiblelater/rules/CheckYamlEmptyLines.py
Normal 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)
|
23
ansiblelater/rules/CheckYamlFile.py
Normal file
23
ansiblelater/rules/CheckYamlFile.py
Normal 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)
|
18
ansiblelater/rules/CheckYamlHasContent.py
Normal file
18
ansiblelater/rules/CheckYamlHasContent.py
Normal 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)
|
15
ansiblelater/rules/CheckYamlHyphens.py
Normal file
15
ansiblelater/rules/CheckYamlHyphens.py
Normal 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)
|
17
ansiblelater/rules/CheckYamlIndent.py
Normal file
17
ansiblelater/rules/CheckYamlIndent.py
Normal 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)
|
@ -1 +0,0 @@
|
||||
# noqa
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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)
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -1,2 +0,0 @@
|
||||
[rules]
|
||||
standards = tests/config
|
@ -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,
|
||||
]
|
@ -1,5 +0,0 @@
|
||||
- start:
|
||||
- overindented
|
||||
- misaligned
|
||||
- next:
|
||||
- underindented
|
@ -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
|
@ -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]
|
||||
|
@ -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
|
@ -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`.
|
||||
|
@ -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.
|
@ -5,30 +5,30 @@ 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. | |
|
||||
| ----------------------------- | ----------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| 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. | |
|
||||
|
@ -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
98
poetry.lock
generated
@ -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"},
|
||||
|
@ -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*
|
||||
|
Loading…
Reference in New Issue
Block a user