mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-22 04:40:42 +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 LOG
|
||||||
from ansiblelater import __version__
|
from ansiblelater import __version__
|
||||||
from ansiblelater import logger
|
from ansiblelater import logger
|
||||||
from ansiblelater.command import base
|
from ansiblelater.candidate import Candidate
|
||||||
from ansiblelater.command import candidates
|
from ansiblelater.settings import Settings
|
||||||
|
from ansiblelater.standard import SingleStandards
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -18,15 +19,28 @@ def main():
|
|||||||
description="Validate Ansible files against best practice guideline"
|
description="Validate Ansible files against best practice guideline"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
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(
|
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(
|
parser.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--standards",
|
"--standards",
|
||||||
dest="rules.filter",
|
dest="rules.filter",
|
||||||
|
metavar="FILTER",
|
||||||
action="append",
|
action="append",
|
||||||
help="limit standards to given ID's"
|
help="limit standards to given ID's"
|
||||||
)
|
)
|
||||||
@ -34,6 +48,7 @@ def main():
|
|||||||
"-x",
|
"-x",
|
||||||
"--exclude-standards",
|
"--exclude-standards",
|
||||||
dest="rules.exclude_filter",
|
dest="rules.exclude_filter",
|
||||||
|
metavar="EXCLUDE_FILTER",
|
||||||
action="append",
|
action="append",
|
||||||
help="exclude standards by given ID's"
|
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"
|
"-q", dest="logging.level", action="append_const", const=1, help="decrease log level"
|
||||||
)
|
)
|
||||||
parser.add_argument("rules.files", nargs="*")
|
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__
|
args = parser.parse_args().__dict__
|
||||||
|
|
||||||
settings = base.get_settings(args)
|
settings = Settings(args=args)
|
||||||
config = settings.config
|
config = settings.config
|
||||||
|
|
||||||
logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"])
|
logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"])
|
||||||
|
SingleStandards(config["rules"]["standards"]).rules
|
||||||
files = config["rules"]["files"]
|
|
||||||
standards = base.get_standards(config["rules"]["standards"])
|
|
||||||
|
|
||||||
workers = max(multiprocessing.cpu_count() - 2, 2)
|
workers = max(multiprocessing.cpu_count() - 2, 2)
|
||||||
p = multiprocessing.Pool(workers)
|
p = multiprocessing.Pool(workers)
|
||||||
tasks = []
|
tasks = []
|
||||||
for filename in files:
|
for filename in config["rules"]["files"]:
|
||||||
lines = None
|
candidate = Candidate.classify(filename, settings)
|
||||||
candidate = candidates.classify(filename, settings, standards)
|
|
||||||
if candidate:
|
if candidate:
|
||||||
if candidate.binary:
|
if candidate.binary:
|
||||||
LOG.info("Not reviewing binary file {name}".format(name=filename))
|
LOG.info("Not reviewing binary file {name}".format(name=filename))
|
||||||
@ -69,11 +83,9 @@ def main():
|
|||||||
if candidate.vault:
|
if candidate.vault:
|
||||||
LOG.info("Not reviewing vault file {name}".format(name=filename))
|
LOG.info("Not reviewing vault file {name}".format(name=filename))
|
||||||
continue
|
continue
|
||||||
if lines:
|
|
||||||
LOG.info("Reviewing {candidate} lines {no}".format(candidate=candidate, no=lines))
|
|
||||||
else:
|
else:
|
||||||
LOG.info("Reviewing all of {candidate}".format(candidate=candidate))
|
LOG.info("Reviewing all of {candidate}".format(candidate=candidate))
|
||||||
tasks.append((candidate, settings, lines))
|
tasks.append(candidate)
|
||||||
else:
|
else:
|
||||||
LOG.info("Couldn't classify file {name}".format(name=filename))
|
LOG.info("Couldn't classify file {name}".format(name=filename))
|
||||||
|
|
||||||
@ -89,9 +101,8 @@ def main():
|
|||||||
sys.exit(return_code)
|
sys.exit(return_code)
|
||||||
|
|
||||||
|
|
||||||
def _review_wrapper(args):
|
def _review_wrapper(candidate):
|
||||||
(candidate, settings, lines) = args
|
return candidate.review()
|
||||||
return candidate.review(settings, lines)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -6,11 +6,11 @@ import os
|
|||||||
import re
|
import re
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
|
|
||||||
from six import iteritems
|
|
||||||
|
|
||||||
from ansiblelater import LOG
|
from ansiblelater import LOG
|
||||||
from ansiblelater import utils
|
from ansiblelater import utils
|
||||||
from ansiblelater.logger import flag_extra
|
from ansiblelater.logger import flag_extra
|
||||||
|
from ansiblelater.standard import SingleStandards
|
||||||
|
from ansiblelater.standard import StandardBase
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Ansible 2.4 import of module loader
|
# Ansible 2.4 import of module loader
|
||||||
@ -36,8 +36,9 @@ class Candidate(object):
|
|||||||
self.vault = False
|
self.vault = False
|
||||||
self.filetype = type(self).__name__.lower()
|
self.filetype = type(self).__name__.lower()
|
||||||
self.expected_version = True
|
self.expected_version = True
|
||||||
self.standards = self._get_standards(settings, standards)
|
|
||||||
self.faulty = False
|
self.faulty = False
|
||||||
|
self.config = settings.config
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with codecs.open(filename, mode="rb", encoding="utf-8") as f:
|
with codecs.open(filename, mode="rb", encoding="utf-8") as f:
|
||||||
@ -46,9 +47,7 @@ class Candidate(object):
|
|||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
self.binary = True
|
self.binary = True
|
||||||
|
|
||||||
self.version = self._get_version(settings)
|
def _get_version(self):
|
||||||
|
|
||||||
def _get_version(self, settings):
|
|
||||||
path = self.path
|
path = self.path
|
||||||
version = None
|
version = None
|
||||||
|
|
||||||
@ -97,60 +96,57 @@ class Candidate(object):
|
|||||||
|
|
||||||
return version
|
return version
|
||||||
|
|
||||||
def _get_standards(self, settings, standards):
|
def _filter_standards(self):
|
||||||
target_standards = []
|
target_standards = []
|
||||||
includes = settings.config["rules"]["filter"]
|
includes = self.config["rules"]["filter"]
|
||||||
excludes = settings.config["rules"]["exclude_filter"]
|
excludes = self.config["rules"]["exclude_filter"]
|
||||||
|
|
||||||
if len(includes) == 0:
|
if len(includes) == 0:
|
||||||
includes = [s.id for s in standards]
|
includes = [s.sid for s in self.standards]
|
||||||
|
|
||||||
for standard in standards:
|
for standard in self.standards:
|
||||||
if standard.id in includes and standard.id not in excludes:
|
if standard.sid in includes and standard.sid not in excludes:
|
||||||
target_standards.append(standard)
|
target_standards.append(standard)
|
||||||
|
|
||||||
return target_standards
|
return target_standards
|
||||||
|
|
||||||
def review(self, settings, lines=None):
|
def review(self, lines=None):
|
||||||
errors = 0
|
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:
|
if type(self).__name__.lower() not in standard.types:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = standard.check(self, settings.config)
|
result = standard.check(self, self.config)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
utils.sysexit_with_message(
|
utils.sysexit_with_message(
|
||||||
"Standard '{}' returns an empty result object.".format(
|
"Standard '{id}' returns an empty result object.".format(id=standard.sid)
|
||||||
standard.check.__name__
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
"tag": "review",
|
"tag": "review",
|
||||||
"standard": standard.name,
|
"standard": standard.description,
|
||||||
"file": self.path,
|
"file": self.path,
|
||||||
"passed": True
|
"passed": True
|
||||||
}
|
}
|
||||||
|
|
||||||
if standard.id and standard.id.strip():
|
if standard.sid and standard.sid.strip():
|
||||||
labels["id"] = standard.id
|
labels["sid"] = standard.sid
|
||||||
|
|
||||||
for err in [
|
for err in result.errors:
|
||||||
err for err in result.errors if not err.lineno
|
|
||||||
or utils.is_line_in_ranges(err.lineno, utils.lines_ranges(lines))
|
|
||||||
]: # noqa
|
|
||||||
err_labels = copy.copy(labels)
|
err_labels = copy.copy(labels)
|
||||||
err_labels["passed"] = False
|
err_labels["passed"] = False
|
||||||
if isinstance(err, Error):
|
if isinstance(err, StandardBase.Error):
|
||||||
err_labels.update(err.to_dict())
|
err_labels.update(err.to_dict())
|
||||||
|
|
||||||
if not standard.version:
|
if not standard.version:
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
"{id}Best practice '{name}' not met:\n{path}:{error}".format(
|
"{sid}Best practice '{description}' not met:\n{path}:{error}".format(
|
||||||
id=self._format_id(standard.id),
|
sid=self._format_id(standard.sid),
|
||||||
name=standard.name,
|
description=standard.description,
|
||||||
path=self.path,
|
path=self.path,
|
||||||
error=err
|
error=err
|
||||||
),
|
),
|
||||||
@ -158,9 +154,9 @@ class Candidate(object):
|
|||||||
)
|
)
|
||||||
elif LooseVersion(standard.version) > LooseVersion(self.version):
|
elif LooseVersion(standard.version) > LooseVersion(self.version):
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
"{id}Future standard '{name}' not met:\n{path}:{error}".format(
|
"{sid}Future standard '{description}' not met:\n{path}:{error}".format(
|
||||||
id=self._format_id(standard.id),
|
sid=self._format_id(standard.sid),
|
||||||
name=standard.name,
|
description=standard.description,
|
||||||
path=self.path,
|
path=self.path,
|
||||||
error=err
|
error=err
|
||||||
),
|
),
|
||||||
@ -168,9 +164,9 @@ class Candidate(object):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
LOG.error(
|
LOG.error(
|
||||||
"{id}Standard '{name}' not met:\n{path}:{error}".format(
|
"{sid}Standard '{description}' not met:\n{path}:{error}".format(
|
||||||
id=self._format_id(standard.id),
|
sid=self._format_id(standard.sid),
|
||||||
name=standard.name,
|
description=standard.description,
|
||||||
path=self.path,
|
path=self.path,
|
||||||
error=err
|
error=err
|
||||||
),
|
),
|
||||||
@ -180,6 +176,44 @@ class Candidate(object):
|
|||||||
|
|
||||||
return errors
|
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):
|
def _format_id(self, standard_id):
|
||||||
if standard_id and standard_id.strip():
|
if standard_id and standard_id.strip():
|
||||||
standard_id = "[{id}] ".format(id=standard_id.strip())
|
standard_id = "[{id}] ".format(id=standard_id.strip())
|
||||||
@ -314,82 +348,3 @@ class Rolesfile(Unversioned):
|
|||||||
"""Object classified as Ansible roles file."""
|
"""Object classified as Ansible roles file."""
|
||||||
|
|
||||||
pass
|
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()
|
defaults = self._get_defaults()
|
||||||
self.config_file = args.get("config_file") or default_config_file
|
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 = dict(filter(lambda item: item[1] is not None, args.items()))
|
||||||
|
tmp_args.pop("config_file", None)
|
||||||
|
|
||||||
tmp_dict = {}
|
tmp_dict = {}
|
||||||
for key, value in tmp_args.items():
|
for key, value in tmp_args.items():
|
||||||
@ -102,13 +102,22 @@ class Settings(object):
|
|||||||
if f not in defaults["ansible"]["custom_modules"]:
|
if f not in defaults["ansible"]["custom_modules"]:
|
||||||
defaults["ansible"]["custom_modules"].append(f)
|
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
|
return defaults
|
||||||
|
|
||||||
def _get_defaults(self):
|
def _get_defaults(self):
|
||||||
rules_dir = os.path.join(resource_filename("ansiblelater", "data"))
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"rules": {
|
"rules": {
|
||||||
"standards": rules_dir,
|
"buildin": True,
|
||||||
|
"standards": [],
|
||||||
"filter": [],
|
"filter": [],
|
||||||
"exclude_filter": [],
|
"exclude_filter": [],
|
||||||
"ignore_dotfiles": True,
|
"ignore_dotfiles": True,
|
||||||
|
@ -1,28 +1,362 @@
|
|||||||
"""Standard definition."""
|
"""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):
|
import toolz
|
||||||
"""
|
import yaml
|
||||||
Standard definition for all defined rules.
|
from yamllint import linter
|
||||||
|
from yamllint.config import YamlLintConfig
|
||||||
|
|
||||||
Later lookup the config file for a path to a rules directory
|
from ansiblelater.exceptions import LaterAnsibleError
|
||||||
or fallback to default `ansiblelater/data/*`.
|
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):
|
||||||
|
|
||||||
"""
|
def __call__(cls, *args, **kwargs):
|
||||||
self.id = standard_dict.get("id", "")
|
mcls = type.__call__(cls, *args)
|
||||||
self.name = standard_dict.get("name")
|
setattr(mcls, "sid", cls.sid)
|
||||||
self.version = standard_dict.get("version")
|
setattr(mcls, "description", getattr(cls, "description", "__unknown__"))
|
||||||
self.check = standard_dict.get("check")
|
setattr(mcls, "helptext", getattr(cls, "helptext", ""))
|
||||||
self.types = standard_dict.get("types")
|
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
|
def __repr__(self): # noqa
|
||||||
return "Standard: {name} (version: {version}, types: {types})".format(
|
return "Standard: {description} (version: {version}, types: {types})".format(
|
||||||
name=self.name, version=self.version, types=self.types
|
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):
|
def sysexit_with_message(msg, code=1):
|
||||||
LOG.critical(msg)
|
LOG.critical(msg)
|
||||||
sysexit(code)
|
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:
|
A typical standards check will look like:
|
||||||
@ -7,18 +7,27 @@ A typical standards check will look like:
|
|||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
<!-- spellchecker-disable -->
|
<!-- spellchecker-disable -->
|
||||||
{{< highlight Python "linenos=table" >}}
|
{{< highlight Python "linenos=table" >}}
|
||||||
def check_playbook_for_something(candidate, settings):
|
class CheckBecomeUser(StandardBase):
|
||||||
result = Result(candidate.path) # empty result is a success with no output
|
|
||||||
with open(candidate.path, 'r') as f:
|
sid = "ANSIBLE0015"
|
||||||
for (lineno, line) in enumerate(f):
|
description = "Become should be combined with become_user"
|
||||||
if line is dodgy:
|
helptext = "the task has `become` enabled but `become_user` is missing"
|
||||||
# enumerate is 0-based so add 1 to lineno
|
version = "0.1"
|
||||||
result.errors.append(Error(lineno+1, "Line is dodgy: reasons"))
|
types = ["playbook", "task", "handler"]
|
||||||
return result
|
|
||||||
|
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 >}}
|
{{< /highlight >}}
|
||||||
<!-- spellchecker-enable -->
|
<!-- spellchecker-enable -->
|
||||||
<!-- prettier-ignore-end -->
|
<!-- 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`.
|
||||||
|
|
||||||
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`).
|
|
||||||
|
@ -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.
|
|
@ -4,31 +4,31 @@ title: Included rules
|
|||||||
|
|
||||||
Reviews are nothing without some rules or standards against which to review. ansible-later comes with a couple of built-in checks explained in the following table.
|
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 |
|
| Rule | ID | Description | Parameter |
|
||||||
| --------------------------------- | ----------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- |
|
| ----------------------------- | ----------- | ----------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||||
| check_yaml_empty_lines | LINT0001 | YAML should not contain unnecessarily empty lines. | {max: 1, max-start: 0, max-end: 1} |
|
| CheckYamlEmptyLines | 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} |
|
| CheckYamlIndent | 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} |
|
| CheckYamlHyphens | 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}} |
|
| CheckYamlDocumentStart | 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}} |
|
| CheckYamlColons | 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. | |
|
| CheckYamlFile | LINT0006 | Roles file should be in yaml format. | |
|
||||||
| check_yaml_has_content | LINT0007 | Files should contain useful content. | |
|
| CheckYamlHasContent | LINT0007 | Files should contain useful content. | |
|
||||||
| check_native_yaml | LINT0008 | Use YAML format for tasks and handlers rather than key=value. | |
|
| CheckNativeYaml | 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}} |
|
| CheckYamlDocumentEnd | 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. | |
|
| CheckLineBetweenTasks | 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 |
|
| CheckMetaMain | 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. | |
|
| CheckUniqueNamedTask | ANSIBLE0003 | Tasks and handlers must be uniquely named within a file. | |
|
||||||
| check_braces | ANSIBLE0004 | YAML should use consitent number of spaces around variables. | |
|
| CheckBraces | 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. | |
|
| CheckScmInSrc | 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 |
|
| CheckNamedTask | 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 |
|
| CheckNameFormat | 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. | |
|
| CheckCommandInsteadofModule | ANSIBLE0008 | Commands should not be used in place of modules. | |
|
||||||
| check_install_use_latest | ANSIBLE0009 | Package managers should not install with state=latest. | |
|
| CheckInstallUseLatest | ANSIBLE0009 | Package managers should not install with state=latest. | |
|
||||||
| check_shell_instead_command | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands. | |
|
| CheckShellInsteadCommand | 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. | |
|
| CheckCommandHasChanges | 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`. | |
|
| CheckEmptyStringCompare | 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`. | |
|
| CheckCompareToLiteralBool | 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` |
|
| CheckLiteralBoolFormat | 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`. | |
|
| CheckBecomeUser | ANSIBLE0015 | `become` should be always used combined with `become_user`. | |
|
||||||
| check_filter_separation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | |
|
| CheckFilterSeparation | ANSIBLE0016 | Jinja2 filters should be separated with spaces. | |
|
||||||
| check_command_instead_of_argument | ANSIBLE0017 | Commands should not be used in place of module arguments. | |
|
| CheckCommandInsteadOfArgument | ANSIBLE0017 | Commands should not be used in place of module arguments. | |
|
||||||
|
@ -21,8 +21,6 @@ main:
|
|||||||
ref: "/included_rules"
|
ref: "/included_rules"
|
||||||
- name: Build your own rules
|
- name: Build your own rules
|
||||||
sub:
|
sub:
|
||||||
- name: Standards file
|
|
||||||
ref: "/build_rules/standards_file"
|
|
||||||
- name: Candidates
|
- name: Candidates
|
||||||
ref: "/build_rules/candidates"
|
ref: "/build_rules/candidates"
|
||||||
- name: Standards checks
|
- name: Standards checks
|
||||||
|
98
poetry.lock
generated
98
poetry.lock
generated
@ -1,17 +1,17 @@
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "ansible"
|
name = "ansible"
|
||||||
version = "2.10.4"
|
version = "2.10.5"
|
||||||
description = "Radically simple IT automation"
|
description = "Radically simple IT automation"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
ansible-base = ">=2.10.3,<2.11"
|
ansible-base = ">=2.10.4,<2.11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ansible-base"
|
name = "ansible-base"
|
||||||
version = "2.10.4"
|
version = "2.10.5"
|
||||||
description = "Radically simple IT automation"
|
description = "Radically simple IT automation"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = true
|
optional = true
|
||||||
@ -286,7 +286,7 @@ smmap = ">=3.0.1,<4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gitpython"
|
name = "gitpython"
|
||||||
version = "3.1.11"
|
version = "3.1.12"
|
||||||
description = "Python Git Library"
|
description = "Python Git Library"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -297,7 +297,7 @@ gitdb = ">=4.0.1,<5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "importlib-metadata"
|
name = "importlib-metadata"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
description = "Read metadata from Python packages"
|
description = "Read metadata from Python packages"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@ -308,8 +308,8 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
|||||||
zipp = ">=0.5"
|
zipp = ">=0.5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
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", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
@ -321,7 +321,7 @@ python-versions = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.6.4"
|
version = "5.7.0"
|
||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -526,14 +526,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-cov"
|
name = "pytest-cov"
|
||||||
version = "2.10.1"
|
version = "2.11.1"
|
||||||
description = "Pytest plugin for measuring coverage."
|
description = "Pytest plugin for measuring coverage."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
coverage = ">=4.4"
|
coverage = ">=5.2.1"
|
||||||
pytest = ">=4.6"
|
pytest = ">=4.6"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@ -541,7 +541,7 @@ testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist",
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-mock"
|
name = "pytest-mock"
|
||||||
version = "3.4.0"
|
version = "3.5.1"
|
||||||
description = "Thin-wrapper around the mock package for easier use with pytest"
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -563,11 +563,11 @@ python-versions = ">=3.4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "5.3.1"
|
version = "5.4.1"
|
||||||
description = "YAML parser and emitter for Python"
|
description = "YAML parser and emitter for Python"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
@ -587,8 +587,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snowballstemmer"
|
name = "snowballstemmer"
|
||||||
version = "2.0.0"
|
version = "2.1.0"
|
||||||
description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms."
|
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
@ -607,7 +607,7 @@ pbr = ">=2.0.0,<2.1.0 || >2.1.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "testfixtures"
|
name = "testfixtures"
|
||||||
version = "6.17.0"
|
version = "6.17.1"
|
||||||
description = "A collection of helpers and mock objects for unit tests and doc tests."
|
description = "A collection of helpers and mock objects for unit tests and doc tests."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -693,10 +693,10 @@ content-hash = "4ec0d9863ad688c640a6c0a050896f0df6a8c7b7e4ffa5d50ef09b1505b5a3d2
|
|||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
ansible = [
|
ansible = [
|
||||||
{file = "ansible-2.10.4.tar.gz", hash = "sha256:98e718aea82199be62db7731373d660627aa1e938d34446588f2f49c228638ee"},
|
{file = "ansible-2.10.5.tar.gz", hash = "sha256:9775229aae31336a624ca5afe5533fea5e49ef4daa96a96791dd9871b2d8b8d1"},
|
||||||
]
|
]
|
||||||
ansible-base = [
|
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 = [
|
anyconfig = [
|
||||||
{file = "anyconfig-0.10.0-py2.py3-none-any.whl", hash = "sha256:156aa990976d068dec63e1e250ba130a32d48c4b7a8d4f12137b8b74074bbf3f"},
|
{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"},
|
{file = "gitdb-4.0.5.tar.gz", hash = "sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"},
|
||||||
]
|
]
|
||||||
gitpython = [
|
gitpython = [
|
||||||
{file = "GitPython-3.1.11-py3-none-any.whl", hash = "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b"},
|
{file = "GitPython-3.1.12-py3-none-any.whl", hash = "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5"},
|
||||||
{file = "GitPython-3.1.11.tar.gz", hash = "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"},
|
{file = "GitPython-3.1.12.tar.gz", hash = "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac"},
|
||||||
]
|
]
|
||||||
importlib-metadata = [
|
importlib-metadata = [
|
||||||
{file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"},
|
{file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
|
||||||
{file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"},
|
{file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
|
||||||
]
|
]
|
||||||
iniconfig = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
]
|
]
|
||||||
isort = [
|
isort = [
|
||||||
{file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"},
|
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
|
||||||
{file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"},
|
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
|
||||||
]
|
]
|
||||||
jinja2 = [
|
jinja2 = [
|
||||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
{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"},
|
{file = "pytest-6.2.1.tar.gz", hash = "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"},
|
||||||
]
|
]
|
||||||
pytest-cov = [
|
pytest-cov = [
|
||||||
{file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"},
|
{file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
|
||||||
{file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"},
|
{file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
|
||||||
]
|
]
|
||||||
pytest-mock = [
|
pytest-mock = [
|
||||||
{file = "pytest-mock-3.4.0.tar.gz", hash = "sha256:c3981f5edee6c4d1942250a60d9b39d38d5585398de1bfce057f925bdda720f4"},
|
{file = "pytest-mock-3.5.1.tar.gz", hash = "sha256:a1e2aba6af9560d313c642dae7e00a2a12b022b80301d9d7fc8ec6858e1dd9fc"},
|
||||||
{file = "pytest_mock-3.4.0-py3-none-any.whl", hash = "sha256:c0fc979afac4aaba545cbd01e9c20736eb3fefb0a066558764b07d3de8f04ed3"},
|
{file = "pytest_mock-3.5.1-py3-none-any.whl", hash = "sha256:379b391cfad22422ea2e252bdfc008edd08509029bcde3c25b2c0bd741e0424e"},
|
||||||
]
|
]
|
||||||
python-json-logger = [
|
python-json-logger = [
|
||||||
{file = "python-json-logger-2.0.1.tar.gz", hash = "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398"},
|
{file = "python-json-logger-2.0.1.tar.gz", hash = "sha256:f26eea7898db40609563bed0a7ca11af12e2a79858632706d835a0f961b7d398"},
|
||||||
]
|
]
|
||||||
pyyaml = [
|
pyyaml = [
|
||||||
{file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
|
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
|
||||||
{file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
|
{file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"},
|
||||||
{file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"},
|
{file = "PyYAML-5.4.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8"},
|
||||||
{file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"},
|
{file = "PyYAML-5.4.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185"},
|
||||||
{file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"},
|
{file = "PyYAML-5.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253"},
|
||||||
{file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"},
|
{file = "PyYAML-5.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc"},
|
||||||
{file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"},
|
{file = "PyYAML-5.4.1-cp36-cp36m-win32.whl", hash = "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5"},
|
||||||
{file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"},
|
{file = "PyYAML-5.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df"},
|
||||||
{file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"},
|
{file = "PyYAML-5.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018"},
|
||||||
{file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
|
{file = "PyYAML-5.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63"},
|
||||||
{file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"},
|
{file = "PyYAML-5.4.1-cp37-cp37m-win32.whl", hash = "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b"},
|
||||||
{file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"},
|
{file = "PyYAML-5.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf"},
|
||||||
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
|
{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 = [
|
six = [
|
||||||
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
{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"},
|
{file = "smmap-3.0.4.tar.gz", hash = "sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"},
|
||||||
]
|
]
|
||||||
snowballstemmer = [
|
snowballstemmer = [
|
||||||
{file = "snowballstemmer-2.0.0-py2.py3-none-any.whl", hash = "sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0"},
|
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
|
||||||
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
|
{file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
|
||||||
]
|
]
|
||||||
stevedore = [
|
stevedore = [
|
||||||
{file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"},
|
{file = "stevedore-3.3.0-py3-none-any.whl", hash = "sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"},
|
||||||
{file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"},
|
{file = "stevedore-3.3.0.tar.gz", hash = "sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee"},
|
||||||
]
|
]
|
||||||
testfixtures = [
|
testfixtures = [
|
||||||
{file = "testfixtures-6.17.0-py2.py3-none-any.whl", hash = "sha256:ebcc3e024d47bb58a60cdc678604151baa0c920ae2814004c89ac9066de31b2c"},
|
{file = "testfixtures-6.17.1-py2.py3-none-any.whl", hash = "sha256:9ed31e83f59619e2fa17df053b241e16e0608f4580f7b5a9333a0c9bdcc99137"},
|
||||||
{file = "testfixtures-6.17.0.tar.gz", hash = "sha256:fa7c170df68ca6367eda061e9ec339ae3e6d3679c31e04033f83ef97a7d7d0ce"},
|
{file = "testfixtures-6.17.1.tar.gz", hash = "sha256:5ec3a0dd6f71cc4c304fbc024a10cc293d3e0b852c868014b9f233203e149bda"},
|
||||||
]
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
[flake8]
|
[flake8]
|
||||||
# Explanation of errors
|
# Explanation of errors
|
||||||
#
|
#
|
||||||
|
# D100: Missing docstring in public module
|
||||||
|
# D101: Missing docstring in public class
|
||||||
# D102: Missing docstring in public method
|
# D102: Missing docstring in public method
|
||||||
# D103: Missing docstring in public function
|
# D103: Missing docstring in public function
|
||||||
# D107: Missing docstring in __init__
|
# D107: Missing docstring in __init__
|
||||||
# D202: No blank lines allowed after function docstring
|
# D202: No blank lines allowed after function docstring
|
||||||
# W503:Line break occurred before a binary operator
|
# 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
|
max-line-length = 99
|
||||||
inline-quotes = double
|
inline-quotes = double
|
||||||
exclude = .git, __pycache__, build, dist, test, *.pyc, *.egg-info, .cache, .eggs, env*
|
exclude = .git, __pycache__, build, dist, test, *.pyc, *.egg-info, .cache, .eggs, env*
|
||||||
|
Loading…
Reference in New Issue
Block a user