refactor: drop default standards version and rename to rules

This commit is contained in:
Robert Kaussow 2024-01-25 21:09:05 +01:00
parent 7f5c2a51b1
commit 137e559d57
Signed by: xoxys
GPG Key ID: 4E692A2EAECC03C0
48 changed files with 197 additions and 321 deletions

View File

@ -7,8 +7,8 @@ import sys
from ansiblelater import LOG, __version__, logger
from ansiblelater.candidate import Candidate
from ansiblelater.rule import SingleRules
from ansiblelater.settings import Settings
from ansiblelater.standard import SingleStandards
def main():
@ -22,33 +22,33 @@ def main():
parser.add_argument(
"-r",
"--rules-dir",
dest="rules.standards",
metavar="RULES",
dest="rules.dir",
metavar="DIR",
action="append",
help="directory of standard rules",
help="directory of rules",
)
parser.add_argument(
"-B",
"--no-buildin",
dest="rules.buildin",
"--no-builtin",
dest="rules.builtin",
action="store_false",
help="disables build-in standard rules",
help="disables built-in rules",
)
parser.add_argument(
"-s",
"--standards",
dest="rules.filter",
metavar="FILTER",
"-i",
"--include-rules",
dest="rules.include_filter",
metavar="TAGS",
action="append",
help="limit standards to given ID's",
help="limit rules to given id/tags",
)
parser.add_argument(
"-x",
"--exclude-standards",
"--exclude-rules",
dest="rules.exclude_filter",
metavar="EXCLUDE_FILTER",
metavar="TAGS",
action="append",
help="exclude standards by given ID's",
help="exclude rules by given it/tags",
)
parser.add_argument(
"-v", dest="logging.level", action="append_const", const=-1, help="increase log level"
@ -65,7 +65,7 @@ def main():
config = settings.config
logger.update_logger(LOG, config["logging"]["level"], config["logging"]["json"])
SingleStandards(config["rules"]["standards"])
SingleRules(config["rules"]["dir"])
workers = max(multiprocessing.cpu_count() - 2, 2)
p = multiprocessing.Pool(workers)

View File

@ -3,14 +3,12 @@
import codecs
import copy
import os
import re
from ansible.plugins.loader import module_loader
from packaging.version import Version
from ansiblelater import LOG, utils
from ansiblelater import LOG
from ansiblelater.logger import flag_extra
from ansiblelater.standard import SingleStandards, StandardBase
from ansiblelater.rule import RuleBase, SingleRules
class Candidate:
@ -21,7 +19,7 @@ class Candidate:
bundled with necessary meta informations for rule processing.
"""
def __init__(self, filename, settings={}, standards=[]): # noqa
def __init__(self, filename, settings={}, rules=[]): # noqa
self.path = filename
self.binary = False
self.vault = False
@ -37,163 +35,114 @@ class Candidate:
except UnicodeDecodeError:
self.binary = True
def _get_version(self):
name = type(self).__name__
path = self.path
version = None
config_version = self.config["rules"]["version"].strip()
if config_version:
version_config_re = re.compile(r"([\d.]+)")
match = version_config_re.match(config_version)
if match:
version = match.group(1)
if not self.binary:
if isinstance(self, RoleFile):
parentdir = os.path.dirname(os.path.abspath(self.path))
while parentdir != os.path.dirname(parentdir):
meta_file = os.path.join(parentdir, "meta", "main.yml")
if os.path.exists(meta_file):
path = meta_file
break
parentdir = os.path.dirname(parentdir)
version_file_re = re.compile(r"^# Standards:\s*([\d.]+)")
with codecs.open(path, mode="rb", encoding="utf-8") as f:
for line in f:
match = version_file_re.match(line)
if match:
version = match.group(1)
if version:
LOG.info(f"{name} {path} declares standards version {version}")
return version
def _filter_standards(self):
target_standards = []
includes = self.config["rules"]["filter"]
def _filter_rules(self):
target_rules = []
includes = self.config["rules"]["include_filter"]
excludes = self.config["rules"]["exclude_filter"]
if len(includes) == 0:
includes = [s.sid for s in self.standards]
includes = [s.sid for s in self.rules]
for standard in self.standards:
if standard.sid in includes and standard.sid not in excludes:
target_standards.append(standard)
for rule in self.rules:
if rule.sid in includes and rule.sid not in excludes:
target_rules.append(rule)
return target_standards
return target_rules
def review(self):
errors = 0
self.standards = SingleStandards(self.config["rules"]["standards"]).rules
self.version_config = self._get_version()
self.version = self.version_config or utils.standards_latest(self.standards)
self.rules = SingleRules(self.config["rules"]["dir"]).rules
for standard in self._filter_standards():
if type(self).__name__.lower() not in standard.types:
for rule in self._filter_rules():
if type(self).__name__.lower() not in rule.types:
continue
result = standard.check(self, self.config)
result = rule.check(self, self.config)
if not result:
LOG.error(
f"Standard '{standard.sid}' returns an empty result object. Check failed!"
)
LOG.error(f"rule '{rule.sid}' returns an empty result object. Check failed!")
continue
labels = {
"tag": "review",
"standard": standard.description,
"rule": rule.description,
"file": self.path,
"passed": True,
}
if standard.sid and standard.sid.strip():
labels["sid"] = standard.sid
if rule.sid and rule.sid.strip():
labels["sid"] = rule.sid
for err in result.errors:
err_labels = copy.copy(labels)
err_labels["passed"] = False
sid = self._format_id(standard.sid)
sid = self._format_id(rule.sid)
path = self.path
description = standard.description
description = rule.description
if isinstance(err, StandardBase.Error):
if isinstance(err, RuleBase.Error):
err_labels.update(err.to_dict())
if not standard.version:
LOG.warning(
f"{sid}Best practice '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels),
)
elif Version(standard.version) > Version(self.version):
LOG.warning(
f"{sid}Future standard '{description}' not met:\n{path}:{err}",
extra=flag_extra(err_labels),
)
else:
msg = f"{sid}Standard '{description}' not met:\n{path}:{err}"
msg = f"{sid}rule '{description}' not met:\n{path}:{err}"
if standard.sid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1
else:
LOG.warning(msg, extra=flag_extra(err_labels))
if rule.sid not in self.config["rules"]["warning_filter"]:
LOG.error(msg, extra=flag_extra(err_labels))
errors = errors + 1
else:
LOG.warning(msg, extra=flag_extra(err_labels))
return errors
@staticmethod
def classify(filename, settings={}, standards=[]): # noqa
def classify(filename, settings={}, rules=[]): # noqa
parentdir = os.path.basename(os.path.dirname(filename))
basename = os.path.basename(filename)
ext = os.path.splitext(filename)[1][1:]
if parentdir in ["tasks"]:
return Task(filename, settings, standards)
return Task(filename, settings, rules)
if parentdir in ["handlers"]:
return Handler(filename, settings, standards)
return Handler(filename, settings, rules)
if parentdir in ["vars", "defaults"]:
return RoleVars(filename, settings, standards)
return RoleVars(filename, settings, rules)
if "group_vars" in filename.split(os.sep):
return GroupVars(filename, settings, standards)
return GroupVars(filename, settings, rules)
if "host_vars" in filename.split(os.sep):
return HostVars(filename, settings, standards)
return HostVars(filename, settings, rules)
if parentdir in ["meta"] and "main" in basename:
return Meta(filename, settings, standards)
return Meta(filename, settings, rules)
if parentdir in ["meta"] and "argument_specs" in basename:
return ArgumentSpecs(filename, settings, standards)
return ArgumentSpecs(filename, settings, rules)
if parentdir in [
"library",
"lookup_plugins",
"callback_plugins",
"filter_plugins",
] or filename.endswith(".py"):
return Code(filename, settings, standards)
return Code(filename, settings, rules)
if basename == "inventory" or basename == "hosts" or parentdir in ["inventories"]:
return Inventory(filename, settings, standards)
return Inventory(filename, settings, rules)
if "rolesfile" in basename or ("requirements" in basename and ext in ["yaml", "yml"]):
return Rolesfile(filename, settings, standards)
return Rolesfile(filename, settings, rules)
if "Makefile" in basename:
return Makefile(filename, settings, standards)
return Makefile(filename, settings, rules)
if "templates" in filename.split(os.sep) or basename.endswith(".j2"):
return Template(filename, settings, standards)
return Template(filename, settings, rules)
if "files" in filename.split(os.sep):
return File(filename, settings, standards)
return File(filename, settings, rules)
if basename.endswith(".yml") or basename.endswith(".yaml"):
return Playbook(filename, settings, standards)
return Playbook(filename, settings, rules)
if "README" in basename:
return Doc(filename, settings, standards)
return Doc(filename, settings, rules)
return None
def _format_id(self, standard_id):
sid = standard_id.strip()
def _format_id(self, rule_id):
sid = rule_id.strip()
if sid:
standard_id = f"[{sid}] "
rule_id = f"[{sid}] "
return standard_id
return rule_id
def __repr__(self):
return f"{type(self).__name__} ({self.path})"
@ -205,8 +154,8 @@ class Candidate:
class RoleFile(Candidate):
"""Object classified as Ansible role file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
parentdir = os.path.dirname(os.path.abspath(filename))
while parentdir != os.path.dirname(parentdir):
@ -226,16 +175,16 @@ class Playbook(Candidate):
class Task(RoleFile):
"""Object classified as Ansible task file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filetype = "tasks"
class Handler(RoleFile):
"""Object classified as Ansible handler file."""
def __init__(self, filename, settings={}, standards=[]): # noqa
super().__init__(filename, settings, standards)
def __init__(self, filename, settings={}, rules=[]): # noqa
super().__init__(filename, settings, rules)
self.filetype = "handlers"

View File

@ -1,4 +1,4 @@
"""Standard definition."""
"""Rule definition."""
import copy
import importlib
@ -27,22 +27,21 @@ from ansiblelater.utils.yamlhelper import (
)
class StandardMeta(type):
class RuleMeta(type):
def __call__(cls, *args):
mcls = type.__call__(cls, *args)
mcls.sid = cls.sid
mcls.description = getattr(cls, "description", "__unknown__")
mcls.helptext = getattr(cls, "helptext", "")
mcls.version = getattr(cls, "version", None)
mcls.types = getattr(cls, "types", [])
return mcls
class StandardExtendedMeta(StandardMeta, ABCMeta):
class RuleExtendedMeta(RuleMeta, ABCMeta):
pass
class StandardBase(metaclass=StandardExtendedMeta):
class RuleBase(metaclass=RuleExtendedMeta):
SHELL_PIPE_CHARS = "&|<>;$\n*[]{}?"
@property
@ -55,7 +54,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
pass
def __repr__(self):
return f"Standard: {self.description} (version: {self.version}, types: {self.types})"
return f"Rule: {self.description} (types: {self.types})"
@staticmethod
def get_tasks(candidate, settings): # noqa
@ -69,11 +68,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -93,11 +92,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return tasks, errors
@ -115,11 +114,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -159,11 +158,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return normalized, errors
@ -184,11 +183,11 @@ class StandardBase(metaclass=StandardExtendedMeta):
except LaterError as ex:
e = ex.original
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except LaterAnsibleError as e:
errors.append(StandardBase.Error(e.line, f"syntax error: {e.message}"))
errors.append(RuleBase.Error(e.line, f"syntax error: {e.message}"))
candidate.faulty = True
return yamllines, errors
@ -210,7 +209,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
content = yaml.safe_load(f)
except yaml.YAMLError as e:
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
@ -224,14 +223,14 @@ class StandardBase(metaclass=StandardExtendedMeta):
try:
with open(candidate.path, encoding="utf-8") as f:
for problem in linter.run(f, YamlLintConfig(options)):
errors.append(StandardBase.Error(problem.line, problem.desc))
errors.append(RuleBase.Error(problem.line, problem.desc))
except yaml.YAMLError as e:
errors.append(
StandardBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
RuleBase.Error(e.problem_mark.line + 1, f"syntax error: {e.problem}")
)
candidate.faulty = True
except (TypeError, ValueError) as e:
errors.append(StandardBase.Error(None, f"yamllint error: {e}"))
errors.append(RuleBase.Error(None, f"yamllint error: {e}"))
candidate.faulty = True
return errors
@ -302,7 +301,7 @@ class StandardBase(metaclass=StandardExtendedMeta):
return "\n".join([f"{self.candidate}:{error}" for error in self.errors])
class StandardLoader:
class RulesLoader:
def __init__(self, source):
self.rules = []
@ -331,10 +330,7 @@ class StandardLoader:
def _is_plugin(self, obj):
return (
inspect.isclass(obj)
and issubclass(obj, StandardBase)
and obj is not StandardBase
and not None
inspect.isclass(obj) and issubclass(obj, RuleBase) and obj is not RuleBase and not None
)
def validate(self):
@ -343,11 +339,11 @@ class StandardLoader:
all_std = len(normalized_std)
if all_std != unique_std:
sysexit_with_message(
"Detect duplicate ID's in standards definition. Please use unique ID's only."
"Found duplicate tags in rules definition. Please use unique tags only."
)
class SingleStandards(StandardLoader, metaclass=Singleton):
class SingleRules(RulesLoader, metaclass=Singleton):
"""Singleton config class."""
pass

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckBecomeUser(StandardBase):
class CheckBecomeUser(RuleBase):
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):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
from ansiblelater.utils import count_spaces
class CheckBracesSpaces(StandardBase):
class CheckBracesSpaces(RuleBase):
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):

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckChangedInWhen(StandardBase):
class CheckChangedInWhen(RuleBase):
sid = "ANSIBLE0026"
description = "Use handlers instead of `when: changed`"
helptext = "tasks using `when: result.changed` setting are effectively acting as a handler"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandHasChanges(StandardBase):
class CheckCommandHasChanges(RuleBase):
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):

View File

@ -20,14 +20,13 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandInsteadOfArgument(StandardBase):
class CheckCommandInsteadOfArgument(RuleBase):
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):

View File

@ -1,13 +1,12 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCommandInsteadOfModule(StandardBase):
class CheckCommandInsteadOfModule(RuleBase):
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):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCompareToEmptyString(StandardBase):
class CheckCompareToEmptyString(RuleBase):
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):

View File

@ -1,14 +1,13 @@
import re
from ansiblelater.candidate import Template
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckCompareToLiteralBool(StandardBase):
class CheckCompareToLiteralBool(RuleBase):
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):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckDeprecated(StandardBase):
class CheckDeprecated(RuleBase):
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):

View File

@ -20,18 +20,17 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
from ansiblelater.utils import has_glob, has_jinja
class CheckDeprecatedBareVars(StandardBase):
class CheckDeprecatedBareVars(RuleBase):
sid = "ANSIBLE0027"
description = "Deprecated bare variables in loops must not be used"
helptext = (
"bare var '{barevar}' in '{loop_type}' must use full var syntax '{{{{ {barevar} }}}}' "
"or be converted to a list"
)
version = "0.3"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -19,17 +19,16 @@
# THE SOFTWARE.
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilePermissionMissing(StandardBase):
class CheckFilePermissionMissing(RuleBase):
sid = "ANSIBLE0018"
description = "File permissions unset or incorrect"
helptext = (
"`mode` parameter should set permissions explicitly (e.g. `mode: 0644`) "
"to avoid unexpected file permissions"
)
version = "0.2"
types = ["playbook", "task", "handler"]
_modules = {

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilePermissionOctal(StandardBase):
class CheckFilePermissionOctal(RuleBase):
sid = "ANSIBLE0019"
description = "Octal file permissions must contain leading zero or be a string"
helptext = "numeric file permissions without leading zero can behave in unexpected ways"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckFilterSeparation(StandardBase):
class CheckFilterSeparation(RuleBase):
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):

View File

@ -18,14 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckGitHasVersion(StandardBase):
class CheckGitHasVersion(RuleBase):
sid = "ANSIBLE0020"
description = "Git checkouts should use explicit version"
helptext = "git checkouts should point to an explicit commit or tag, not `latest`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckInstallUseLatest(StandardBase):
class CheckInstallUseLatest(RuleBase):
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):

View File

@ -1,13 +1,12 @@
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckLiteralBoolFormat(StandardBase):
class CheckLiteralBoolFormat(RuleBase):
sid = "ANSIBLE0014"
description = "Literal bools should be consistent"
helptext = "literal bools should be written as `{bools}`"
version = "0.1"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckLocalAction(StandardBase):
class CheckLocalAction(RuleBase):
sid = "ANSIBLE0024"
description = "Don't use local_action"
helptext = "`delegate_to: localhost` should be used instead of `local_action`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,14 +1,13 @@
# Copyright (c) 2018, Ansible Project
from nested_lookup import nested_lookup
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckMetaChangeFromDefault(StandardBase):
class CheckMetaChangeFromDefault(RuleBase):
sid = "ANSIBLE0021"
description = "Roles meta/main.yml default values should be changed"
helptext = "meta/main.yml default values should be changed for: `{field}`"
version = "0.2"
types = ["meta"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from nested_lookup import nested_lookup
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckMetaMain(StandardBase):
class CheckMetaMain(RuleBase):
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):

View File

@ -1,13 +1,12 @@
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNameFormat(StandardBase):
class CheckNameFormat(RuleBase):
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):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNamedTask(StandardBase):
class CheckNamedTask(RuleBase):
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):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNativeYaml(StandardBase):
class CheckNativeYaml(RuleBase):
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):

View File

@ -21,17 +21,16 @@
# THE SOFTWARE.
import re
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckNestedJinja(StandardBase):
class CheckNestedJinja(RuleBase):
sid = "ANSIBLE0023"
description = "Don't use nested Jinja2 pattern"
helptext = (
"there should not be any nested jinja pattern "
"like `{{ list_one + {{ list_two | max }} }}`"
)
version = "0.2"
types = ["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
# Copyright (c) 2018, Ansible Project
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckRelativeRolePaths(StandardBase):
class CheckRelativeRolePaths(RuleBase):
sid = "ANSIBLE0025"
description = "Don't use a relative path in a role"
helptext = "`copy` and `template` modules don't need relative path for `src`"
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,13 +1,12 @@
from ansible.parsing.yaml.objects import AnsibleMapping
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckScmInSrc(StandardBase):
class CheckScmInSrc(RuleBase):
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):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckShellInsteadCommand(StandardBase):
class CheckShellInsteadCommand(RuleBase):
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):

View File

@ -1,14 +1,13 @@
import re
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckTaskSeparation(StandardBase):
class CheckTaskSeparation(RuleBase):
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):

View File

@ -1,13 +1,12 @@
from collections import defaultdict
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckUniqueNamedTask(StandardBase):
class CheckUniqueNamedTask(RuleBase):
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):

View File

@ -1,16 +0,0 @@
from ansiblelater.standard import StandardBase
class CheckVersion(StandardBase):
sid = "ANSIBLE9998"
description = "Standards version should be pinned"
helptext = "Standards version not set. Using latest standards version {version}"
types = ["task", "handler", "rolevars", "meta", "template", "file", "playbook"]
def check(self, candidate, settings): # noqa
errors = []
if not candidate.version_config:
errors.append(self.Error(None, self.helptext.format(version=candidate.version)))
return self.Result(candidate.path, errors)

View File

@ -1,13 +1,12 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckWhenFormat(StandardBase):
class CheckWhenFormat(RuleBase):
sid = "ANSIBLE0022"
description = "Don't use Jinja2 in when"
helptext = (
"`when` is a raw Jinja2 expression, redundant {{ }} " "should be removed from variable(s)"
)
version = "0.2"
types = ["playbook", "task", "handler"]
def check(self, candidate, settings):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlColons(StandardBase):
class CheckYamlColons(RuleBase):
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):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlDocumentEnd(StandardBase):
class CheckYamlDocumentEnd(RuleBase):
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):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlDocumentStart(StandardBase):
class CheckYamlDocumentStart(RuleBase):
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):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlEmptyLines(StandardBase):
class CheckYamlEmptyLines(RuleBase):
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):

View File

@ -1,13 +1,12 @@
import os
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlFile(StandardBase):
class CheckYamlFile(RuleBase):
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):

View File

@ -1,11 +1,10 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlHasContent(StandardBase):
class CheckYamlHasContent(RuleBase):
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):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlHyphens(StandardBase):
class CheckYamlHyphens(RuleBase):
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):

View File

@ -1,10 +1,9 @@
from ansiblelater.standard import StandardBase
from ansiblelater.rule import RuleBase
class CheckYamlIndent(StandardBase):
class CheckYamlIndent(RuleBase):
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):

View File

@ -104,13 +104,13 @@ class Settings:
if f not in defaults["ansible"]["custom_modules"]:
defaults["ansible"]["custom_modules"].append(f)
if defaults["rules"]["buildin"]:
defaults["rules"]["standards"].append(
if defaults["rules"]["builtin"]:
defaults["rules"]["dir"].append(
os.path.join(resource_filename("ansiblelater", "rules"))
)
defaults["rules"]["standards"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["standards"]
defaults["rules"]["dir"] = [
os.path.relpath(os.path.normpath(p)) for p in defaults["rules"]["dir"]
]
return defaults
@ -118,9 +118,9 @@ class Settings:
def _get_defaults(self):
defaults = {
"rules": {
"buildin": True,
"standards": [],
"filter": [],
"builtin": True,
"dir": [],
"include_filter": [],
"exclude_filter": [],
"warning_filter": [
"ANSIBLE9999",
@ -128,7 +128,6 @@ class Settings:
],
"ignore_dotfiles": True,
"exclude_files": [],
"version": "",
},
"logging": {
"level": "WARNING",

View File

@ -6,7 +6,6 @@ import sys
from contextlib import suppress
import yaml
from packaging.version import Version
from ansiblelater import logger
@ -35,12 +34,6 @@ def count_spaces(c_string):
return (leading_spaces, trailing_spaces)
def standards_latest(standards):
return max(
[standard.version for standard in standards if standard.version] or ["0.1"], key=Version
)
def lines_ranges(lines_spec):
if not lines_spec:
return None

View File

@ -1,18 +1,17 @@
---
title: Minimal standard checks
title: Write a rule
---
A typical standards check will look like:
A typical rule check will look like:
<!-- prettier-ignore-start -->
<!-- spellchecker-disable -->
{{< highlight Python "linenos=table" >}}
class CheckBecomeUser(StandardBase):
class CheckBecomeUser(RuleBase):
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):

View File

@ -8,28 +8,27 @@ You can get all available CLI options by running `ansible-later --help`:
<!-- spellchecker-disable -->
{{< highlight Shell "linenos=table" >}}
$ ansible-later --help
usage: ansible-later [-h] [-c CONFIG_FILE] [-r RULES.STANDARDS]
[-s RULES.FILTER] [-v] [-q] [--version]
[rules.files [rules.files ...]]
usage: ansible-later [-h] [-c CONFIG] [-r DIR] [-B] [-i TAGS] [-x TAGS] [-v] [-q] [-V] [rules.files ...]
Validate Ansible files against best practice guideline
positional arguments:
rules.files
optional arguments:
options:
-h, --help show this help message and exit
-c CONFIG_FILE, --config CONFIG_FILE
location of configuration file
-r RULES.STANDARDS, --rules RULES.STANDARDS
location of standards rules
-s RULES.FILTER, --standards RULES.FILTER
limit standards to given ID's
-x RULES.EXCLUDE_FILTER, --exclude-standards RULES.EXCLUDE_FILTER
exclude standards by given ID's
-c CONFIG, --config CONFIG
path to configuration file
-r DIR, --rules-dir DIR
directory of rules
-B, --no-builtin disables built-in rules
-i TAGS, --include-rules TAGS
limit rules to given id/tags
-x TAGS, --exclude-rules TAGS
exclude rules by given it/tags
-v increase log level
-q decrease log level
--version show program's version number and exit
-V, --version show program's version number and exit
{{< /highlight >}}
<!-- spellchecker-enable -->
<!-- prettier-ignore-end -->

View File

@ -58,8 +58,8 @@ logging:
# Global settings for all defined rules
rules:
# Disable build-in rules if required
buildin: True
# Disable built-in rules if required
builtin: True
# List of files to exclude
exclude_files: []
@ -75,8 +75,7 @@ rules:
exclude_filter: []
# List of rule ID's that should be displayed as a warning instead of an error. By default,
# only rules whose version is higher than the current default version are marked as warnings.
# This list allows to degrade errors to warnings for each rule.
# no rules are marked as warnings. This list allows to degrade errors to warnings for each rule.
warning_filter:
- "ANSIBLE9999"
- "ANSIBLE9998"
@ -85,12 +84,8 @@ rules:
# You can disable this setting and handle dotfiles by yourself with `exclude_files`.
ignore_dotfiles: True
# List of directories to load standard rules from (defaults to build-in)
standards: []
# Standard version to use. Standard version set in a roles meta file
# or playbook will takes precedence.
version:
# List of directories to load rules from (defaults to built-in)
dir: []
# Block to control included yamllint rules.
# See https://yamllint.readthedocs.io/en/stable/rules.html

View File

@ -2,7 +2,7 @@
title: Included rules
---
Reviews are useless without some rules or standards to check against. ansible-later comes with a set of built-in checks, which are explained in the following table.
Reviews are useless without some rules to check against. `ansible-later` comes with a set of built-in checks, which are explained in the following table.
| Rule | ID | Description | Parameter |
| ----------------------------- | ----------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------- |
@ -42,5 +42,4 @@ Reviews are useless without some rules or standards to check against. ansible-la
| CheckRelativeRolePaths | ANSIBLE0025 | Don't use a relative path in a role. | |
| CheckChangedInWhen | ANSIBLE0026 | Use handlers instead of `when: changed`. | |
| CheckChangedInWhen | ANSIBLE0027 | Deprecated bare variables in loops must not be used. | |
| CheckVersion | ANSIBLE9998 | Standards version should be pinned. | |
| CheckDeprecated | ANSIBLE9999 | Deprecated features of `ansible-later` should not be used. | |

View File

@ -23,5 +23,5 @@ main:
sub:
- name: Candidates
ref: "/build_rules/candidates"
- name: Standards checks
ref: "/build_rules/standards_check"
- name: Rules
ref: "/build_rules/rule"