Merge pull request #32 from xoxys/refactor-formatting

add yapf as formatter
This commit is contained in:
Robert Kaussow 2020-04-05 15:56:44 +02:00 committed by GitHub
commit 209d5b3d2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 781 additions and 547 deletions

View File

@ -59,9 +59,9 @@ local PipelineTest = {
CODECOV_TOKEN: { from_secret: 'codecov_token' }, CODECOV_TOKEN: { from_secret: 'codecov_token' },
}, },
commands: [ commands: [
'pip install codecov', 'pip install codecov -qq',
'coverage combine .tox/py*/.coverage', 'coverage combine .tox/py*/.coverage',
'codecov --required', 'codecov --required -X gcov',
], ],
depends_on: [ depends_on: [
'python35-ansible', 'python35-ansible',

View File

@ -74,9 +74,9 @@ steps:
- name: codecov - name: codecov
image: python:3.7 image: python:3.7
commands: commands:
- pip install codecov - pip install codecov -qq
- coverage combine .tox/py*/.coverage - coverage combine .tox/py*/.coverage
- codecov --required - codecov --required -X gcov
environment: environment:
CODECOV_TOKEN: CODECOV_TOKEN:
from_secret: codecov_token from_secret: codecov_token
@ -470,6 +470,6 @@ depends_on:
--- ---
kind: signature kind: signature
hmac: 4b5f6077a3352113853e936f62396caf68d9a7a2014245f6b24cca6ae0fa8b3b hmac: 03c91a03dab38f62ce4e792238c87e34ac583fa570ff806bf4208df7ed579cd8
... ...

18
.flake8
View File

@ -1,8 +1,18 @@
[flake8] [flake8]
# Temp disable Docstring checks D101, D102, D103, D107 ignore = D102, D103, D107, D202, W503
ignore = E501, W503, F401, N813, D101, D102, D103, D107 max-line-length = 99
max-line-length = 110
inline-quotes = double inline-quotes = double
exclude = .git,.tox,__pycache__,build,dist,tests,*.pyc,*.egg-info,.cache,.eggs exclude =
.git
.tox
__pycache__
build
dist
tests
*.pyc
*.egg-info
.cache
.eggs
env*
application-import-names = ansiblelater application-import-names = ansiblelater
format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s

View File

@ -1,3 +1,3 @@
* BUGFIX * ENHANCEMENT
* replace removed dependency `ansible.module_utils.parsing.convert_bool` * improve ANSIBLE0001 logic to avoid false positive results
* decode task actions to prevent errors on multiline strings * improve log output readability

View File

@ -2,11 +2,11 @@
__author__ = "Robert Kaussow" __author__ = "Robert Kaussow"
__project__ = "ansible-later" __project__ = "ansible-later"
__version__ = "0.3.1"
__license__ = "MIT" __license__ = "MIT"
__maintainer__ = "Robert Kaussow" __maintainer__ = "Robert Kaussow"
__email__ = "mail@geeklabor.de" __email__ = "mail@geeklabor.de"
__status__ = "Production" __url__ = "https://github.com/xoxys/ansible-later"
__version__ = "0.3.1"
from ansiblelater import logger from ansiblelater import logger

View File

@ -15,19 +15,34 @@ from ansiblelater.command import candidates
def main(): def main():
"""Run main program.""" """Run main program."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Validate ansible files against best pratice guideline") description="Validate ansible files against best pratice guideline"
parser.add_argument("-c", "--config", dest="config_file", )
help="location of configuration file") parser.add_argument(
parser.add_argument("-r", "--rules", dest="rules.standards", "-c", "--config", dest="config_file", help="location of configuration file"
help="location of standards rules") )
parser.add_argument("-s", "--standards", dest="rules.filter", action="append", parser.add_argument(
help="limit standards to given ID's") "-r", "--rules", dest="rules.standards", help="location of standards rules"
parser.add_argument("-x", "--exclude-standards", dest="rules.exclude_filter", action="append", )
help="exclude standards by given ID's") parser.add_argument(
parser.add_argument("-v", dest="logging.level", action="append_const", const=-1, "-s",
help="increase log level") "--standards",
parser.add_argument("-q", dest="logging.level", action="append_const", dest="rules.filter",
const=1, help="decrease log level") action="append",
help="limit standards to given ID's"
)
parser.add_argument(
"-x",
"--exclude-standards",
dest="rules.exclude_filter",
action="append",
help="exclude standards by given ID's"
)
parser.add_argument(
"-v", dest="logging.level", action="append_const", const=-1, help="increase log level"
)
parser.add_argument(
"-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("--version", action="version", version="%(prog)s {}".format(__version__))

View File

@ -20,9 +20,7 @@ def get_settings(args):
:returns: Settings object :returns: Settings object
""" """
config = settings.Settings( config = settings.Settings(args=args)
args=args,
)
return config return config
@ -33,13 +31,15 @@ def get_standards(filepath):
standards = importlib.import_module("standards") standards = importlib.import_module("standards")
except ImportError as e: except ImportError as e:
utils.sysexit_with_message( utils.sysexit_with_message(
"Could not import standards from directory %s: %s" % (filepath, str(e))) "Could not import standards from directory %s: %s" % (filepath, str(e))
)
if getattr(standards, "ansible_min_version", None) and \ if getattr(standards, "ansible_min_version", None) and \
LooseVersion(standards.ansible_min_version) > LooseVersion(ansible.__version__): LooseVersion(standards.ansible_min_version) > LooseVersion(ansible.__version__):
utils.sysexit_with_message("Standards require ansible version %s (current version %s). " utils.sysexit_with_message(
"Please upgrade ansible." % "Standards require ansible version %s (current version %s). "
(standards.ansible_min_version, ansible.__version__)) "Please upgrade ansible." % (standards.ansible_min_version, ansible.__version__)
)
if getattr(standards, "ansible_later_min_version", None) and \ if getattr(standards, "ansible_later_min_version", None) and \
LooseVersion(standards.ansible_later_min_version) > LooseVersion( LooseVersion(standards.ansible_later_min_version) > LooseVersion(
@ -47,13 +47,15 @@ def get_standards(filepath):
utils.sysexit_with_message( utils.sysexit_with_message(
"Standards require ansible-later version %s (current version %s). " "Standards require ansible-later version %s (current version %s). "
"Please upgrade ansible-later." % "Please upgrade ansible-later." %
(standards.ansible_later_min_version, utils.get_property("__version__"))) (standards.ansible_later_min_version, utils.get_property("__version__"))
)
normalized_std = (list(toolz.remove(lambda x: x.id == "", standards.standards))) normalized_std = (list(toolz.remove(lambda x: x.id == "", standards.standards)))
unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.id))) unique_std = len(list(toolz.unique(normalized_std, key=lambda x: x.id)))
all_std = len(normalized_std) all_std = len(normalized_std)
if not all_std == unique_std: if not all_std == unique_std:
utils.sysexit_with_message( utils.sysexit_with_message(
"Detect duplicate ID's in standards definition. Please use unique ID's only.") "Detect duplicate ID's in standards definition. Please use unique ID's only."
)
return standards.standards return standards.standards

View File

@ -77,15 +77,18 @@ class Candidate(object):
"%s %s is in a role that contains a meta/main.yml without a declared " "%s %s is in a role that contains a meta/main.yml without a declared "
"standards version. " "standards version. "
"Using latest standards version %s" % "Using latest standards version %s" %
(type(self).__name__, self.path, version)) (type(self).__name__, self.path, version)
)
else: else:
LOG.warning( LOG.warning(
"%s %s does not present standards version. " "%s %s does not present standards version. "
"Using latest standards version %s" % "Using latest standards version %s" %
(type(self).__name__, self.path, version)) (type(self).__name__, self.path, version)
)
else: else:
LOG.info("%s %s declares standards version %s" % LOG.info(
(type(self).__name__, self.path, version)) "%s %s declares standards version %s" % (type(self).__name__, self.path, version)
)
return version return version
@ -113,39 +116,61 @@ class Candidate(object):
result = standard.check(self, settings.config) result = standard.check(self, settings.config)
if not result: if not result:
utils.sysexit_with_message("Standard '{}' returns an empty result object.".format( utils.sysexit_with_message(
standard.check.__name__)) "Standard '{}' returns an empty result object.".format(
standard.check.__name__
)
)
labels = {"tag": "review", "standard": standard.name, "file": self.path, "passed": True} labels = {
"tag": "review",
"standard": standard.name,
"file": self.path,
"passed": True
}
if standard.id and standard.id.strip(): if standard.id and standard.id.strip():
labels["id"] = standard.id labels["id"] = standard.id
for err in [err for err in result.errors for err in [
if not err.lineno or utils.is_line_in_ranges(err.lineno, utils.lines_ranges(lines))]: # noqa 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, Error):
err_labels.update(err.to_dict()) err_labels.update(err.to_dict())
if not standard.version: if not standard.version:
LOG.warning("{id}Best practice '{name}' not met:\n{path}:{error}".format( LOG.warning(
id=self._format_id(standard.id), "{id}Best practice '{name}' not met:\n{path}:{error}".format(
name=standard.name, id=self._format_id(standard.id),
path=self.path, name=standard.name,
error=err), extra=flag_extra(err_labels)) path=self.path,
error=err
),
extra=flag_extra(err_labels)
)
elif LooseVersion(standard.version) > LooseVersion(self.version): elif LooseVersion(standard.version) > LooseVersion(self.version):
LOG.warning("{id}Future standard '{name}' not met:\n{path}:{error}".format( LOG.warning(
id=self._format_id(standard.id), "{id}Future standard '{name}' not met:\n{path}:{error}".format(
name=standard.name, id=self._format_id(standard.id),
path=self.path, name=standard.name,
error=err), extra=flag_extra(err_labels)) path=self.path,
error=err
),
extra=flag_extra(err_labels)
)
else: else:
LOG.error("{id}Standard '{name}' not met:\n{path}:{error}".format( LOG.error(
id=self._format_id(standard.id), "{id}Standard '{name}' not met:\n{path}:{error}".format(
name=standard.name, id=self._format_id(standard.id),
path=self.path, name=standard.name,
error=err), extra=flag_extra(err_labels)) path=self.path,
error=err
),
extra=flag_extra(err_labels)
)
errors = errors + 1 errors = errors + 1
return errors return errors
@ -156,14 +181,16 @@ class Candidate(object):
return standard_id return standard_id
def __repr__(self): # noqa def __repr__(self): # noqa
return "%s (%s)" % (type(self).__name__, self.path) return "%s (%s)" % (type(self).__name__, self.path)
def __getitem__(self, item): # noqa def __getitem__(self, item): # noqa
return self.__dict__.get(item) return self.__dict__.get(item)
class RoleFile(Candidate): class RoleFile(Candidate):
"""Object classified as Ansible role file."""
def __init__(self, filename, settings={}, standards=[]): def __init__(self, filename, settings={}, standards=[]):
super(RoleFile, self).__init__(filename, settings, standards) super(RoleFile, self).__init__(filename, settings, standards)
@ -177,76 +204,110 @@ class RoleFile(Candidate):
class Playbook(Candidate): class Playbook(Candidate):
"""Object classified as Ansible playbook."""
pass pass
class Task(RoleFile): class Task(RoleFile):
"""Object classified as Ansible task file."""
def __init__(self, filename, settings={}, standards=[]): def __init__(self, filename, settings={}, standards=[]):
super(Task, self).__init__(filename, settings, standards) super(Task, self).__init__(filename, settings, standards)
self.filetype = "tasks" self.filetype = "tasks"
class Handler(RoleFile): class Handler(RoleFile):
"""Object classified as Ansible handler file."""
def __init__(self, filename, settings={}, standards=[]): def __init__(self, filename, settings={}, standards=[]):
super(Handler, self).__init__(filename, settings, standards) super(Handler, self).__init__(filename, settings, standards)
self.filetype = "handlers" self.filetype = "handlers"
class Vars(Candidate): class Vars(Candidate):
"""Object classified as Ansible vars file."""
pass pass
class Unversioned(Candidate): class Unversioned(Candidate):
"""Object classified as unversioned file."""
def __init__(self, filename, settings={}, standards=[]): def __init__(self, filename, settings={}, standards=[]):
super(Unversioned, self).__init__(filename, settings, standards) super(Unversioned, self).__init__(filename, settings, standards)
self.expected_version = False self.expected_version = False
class InventoryVars(Unversioned): class InventoryVars(Unversioned):
"""Object classified as Ansible inventory vars."""
pass pass
class HostVars(InventoryVars): class HostVars(InventoryVars):
"""Object classified as Ansible host vars."""
pass pass
class GroupVars(InventoryVars): class GroupVars(InventoryVars):
"""Object classified as Ansible group vars."""
pass pass
class RoleVars(RoleFile): class RoleVars(RoleFile):
"""Object classified as Ansible role vars."""
pass pass
class Meta(RoleFile): class Meta(RoleFile):
"""Object classified as Ansible meta file."""
pass pass
class Inventory(Unversioned): class Inventory(Unversioned):
"""Object classified as Ansible inventory file."""
pass pass
class Code(Unversioned): class Code(Unversioned):
"""Object classified as code file."""
pass pass
class Template(RoleFile): class Template(RoleFile):
"""Object classified as Ansible template file."""
pass pass
class Doc(Unversioned): class Doc(Unversioned):
"""Object classified as documentation file."""
pass pass
class Makefile(Unversioned): class Makefile(Unversioned):
"""Object classified as makefile."""
pass pass
class File(RoleFile): class File(RoleFile):
"""Object classified as generic file."""
pass pass
class Rolesfile(Unversioned): class Rolesfile(Unversioned):
"""Object classified as Ansible roles file."""
pass pass
@ -267,7 +328,7 @@ class Error(object):
for (key, value) in iteritems(kwargs): for (key, value) in iteritems(kwargs):
setattr(self, key, value) setattr(self, key, value)
def __repr__(self): # noqa def __repr__(self): # noqa
if self.lineno: if self.lineno:
return "%s: %s" % (self.lineno, self.message) return "%s: %s" % (self.lineno, self.message)
else: else:
@ -281,13 +342,14 @@ class Error(object):
class Result(object): class Result(object):
"""Generic result object."""
def __init__(self, candidate, errors=None): def __init__(self, candidate, errors=None):
self.candidate = candidate self.candidate = candidate
self.errors = errors or [] self.errors = errors or []
def message(self): def message(self):
return "\n".join(["{0}:{1}".format(self.candidate, error) return "\n".join(["{0}:{1}".format(self.candidate, error) for error in self.errors])
for error in self.errors])
def classify(filename, settings={}, standards=[]): def classify(filename, settings={}, standards=[]):
@ -306,8 +368,10 @@ def classify(filename, settings={}, standards=[]):
return HostVars(filename, settings, standards) return HostVars(filename, settings, standards)
if parentdir in ["meta"]: if parentdir in ["meta"]:
return Meta(filename, settings, standards) return Meta(filename, settings, standards)
if parentdir in ["library", "lookup_plugins", "callback_plugins", if (
"filter_plugins"] or filename.endswith(".py"): parentdir in ["library", "lookup_plugins", "callback_plugins", "filter_plugins"]
or filename.endswith(".py")
):
return Code(filename, settings, standards) return Code(filename, settings, standards)
if "inventory" == basename or "hosts" == basename or parentdir in ["inventories"]: if "inventory" == basename or "hosts" == basename or parentdir in ["inventories"]:
return Inventory(filename, settings, standards) return Inventory(filename, settings, standards)

View File

@ -27,217 +27,257 @@ from ansiblelater.rules.yamlfiles import check_yaml_hyphens
from ansiblelater.rules.yamlfiles import check_yaml_indent from ansiblelater.rules.yamlfiles import check_yaml_indent
from ansiblelater.standard import Standard from ansiblelater.standard import Standard
tasks_should_be_separated = Standard(dict( tasks_should_be_separated = Standard(
id="ANSIBLE0001", dict(
name="Single tasks should be separated by empty line", id="ANSIBLE0001",
check=check_line_between_tasks, name="Single tasks should be separated by empty line",
version="0.1", check=check_line_between_tasks,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
role_must_contain_meta_main = Standard(dict( role_must_contain_meta_main = Standard(
id="ANSIBLE0002", dict(
name="Roles must contain suitable meta/main.yml", id="ANSIBLE0002",
check=check_meta_main, name="Roles must contain suitable meta/main.yml",
version="0.1", check=check_meta_main,
types=["meta"] version="0.1",
)) types=["meta"]
)
)
tasks_are_uniquely_named = Standard(dict( tasks_are_uniquely_named = Standard(
id="ANSIBLE0003", dict(
name="Tasks and handlers must be uniquely named within a single file", id="ANSIBLE0003",
check=check_unique_named_task, name="Tasks and handlers must be uniquely named within a single file",
version="0.1", check=check_unique_named_task,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
use_spaces_between_variable_braces = Standard(dict( use_spaces_between_variable_braces = Standard(
id="ANSIBLE0004", dict(
name="YAML should use consistent number of spaces around variables", id="ANSIBLE0004",
check=check_braces_spaces, name="YAML should use consistent number of spaces around variables",
version="0.1", check=check_braces_spaces,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
roles_scm_not_in_src = Standard(dict( roles_scm_not_in_src = Standard(
id="ANSIBLE0005", dict(
name="Use scm key rather than src: scm+url", id="ANSIBLE0005",
check=check_scm_in_src, name="Use scm key rather than src: scm+url",
version="0.1", check=check_scm_in_src,
types=["rolesfile"] version="0.1",
)) types=["rolesfile"]
)
)
tasks_are_named = Standard(dict( tasks_are_named = Standard(
id="ANSIBLE0006", dict(
name="Tasks and handlers must be named", id="ANSIBLE0006",
check=check_named_task, name="Tasks and handlers must be named",
version="0.1", check=check_named_task,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
tasks_names_are_formatted = Standard(dict( tasks_names_are_formatted = Standard(
id="ANSIBLE0007", dict(
name="Name of tasks and handlers must be formatted", id="ANSIBLE0007",
check=check_name_format, name="Name of tasks and handlers must be formatted",
version="0.1", check=check_name_format,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
commands_should_not_be_used_in_place_of_modules = Standard(dict( commands_should_not_be_used_in_place_of_modules = Standard(
id="ANSIBLE0008", dict(
name="Commands should not be used in place of modules", id="ANSIBLE0008",
check=check_command_instead_of_module, name="Commands should not be used in place of modules",
version="0.1", check=check_command_instead_of_module,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
package_installs_should_not_use_latest = Standard(dict( package_installs_should_not_use_latest = Standard(
id="ANSIBLE0009", dict(
name="Package installs should use present, not latest", id="ANSIBLE0009",
check=check_install_use_latest, name="Package installs should use present, not latest",
types=["playbook", "task", "handler"] check=check_install_use_latest,
)) types=["playbook", "task", "handler"]
)
)
use_shell_only_when_necessary = Standard(dict( use_shell_only_when_necessary = Standard(
id="ANSIBLE0010", dict(
name="Shell should only be used when essential", id="ANSIBLE0010",
check=check_shell_instead_command, name="Shell should only be used when essential",
types=["playbook", "task", "handler"] check=check_shell_instead_command,
)) types=["playbook", "task", "handler"]
)
)
commands_should_be_idempotent = Standard(dict( commands_should_be_idempotent = Standard(
id="ANSIBLE0011", dict(
name="Commands should be idempotent", id="ANSIBLE0011",
check=check_command_has_changes, name="Commands should be idempotent",
version="0.1", check=check_command_has_changes,
types=["playbook", "task"] version="0.1",
)) types=["playbook", "task"]
)
)
dont_compare_to_empty_string = Standard(dict( dont_compare_to_empty_string = Standard(
id="ANSIBLE0012", dict(
name="Don't compare to \"\" - use `when: var` or `when: not var`", id="ANSIBLE0012",
check=check_empty_string_compare, name="Don't compare to \"\" - use `when: var` or `when: not var`",
version="0.1", check=check_empty_string_compare,
types=["playbook", "task", "handler", "template"] version="0.1",
)) types=["playbook", "task", "handler", "template"]
)
)
dont_compare_to_literal_bool = Standard(dict( dont_compare_to_literal_bool = Standard(
id="ANSIBLE0013", dict(
name="Don't compare to True or False - use `when: var` or `when: not var`", id="ANSIBLE0013",
check=check_compare_to_literal_bool, name="Don't compare to True or False - use `when: var` or `when: not var`",
version="0.1", check=check_compare_to_literal_bool,
types=["playbook", "task", "handler", "template"] version="0.1",
)) types=["playbook", "task", "handler", "template"]
)
)
literal_bool_should_be_formatted = Standard(dict( literal_bool_should_be_formatted = Standard(
id="ANSIBLE0014", dict(
name="Literal bools should start with a capital letter", id="ANSIBLE0014",
check=check_literal_bool_format, name="Literal bools should start with a capital letter",
version="0.1", check=check_literal_bool_format,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
)) )
)
use_become_with_become_user = Standard(dict( use_become_with_become_user = Standard(
id="ANSIBLE0015", dict(
name="become should be combined with become_user", id="ANSIBLE0015",
check=check_become_user, name="become should be combined with become_user",
version="0.1", check=check_become_user,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
use_spaces_around_filters = Standard(dict( use_spaces_around_filters = Standard(
id="ANSIBLE0016", dict(
name="jinja2 filters should be separated with spaces", id="ANSIBLE0016",
check=check_filter_separation, name="jinja2 filters should be separated with spaces",
version="0.1", check=check_filter_separation,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars"]
)) )
)
files_should_not_contain_unnecessarily_empty_lines = Standard(dict( files_should_not_contain_unnecessarily_empty_lines = Standard(
id="LINT0001", dict(
name="YAML should not contain unnecessarily empty lines", id="LINT0001",
check=check_yaml_empty_lines, name="YAML should not contain unnecessarily empty lines",
version="0.1", check=check_yaml_empty_lines,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_be_indented = Standard(dict( files_should_be_indented = Standard(
id="LINT0002", dict(
name="YAML should be correctly indented", id="LINT0002",
check=check_yaml_indent, name="YAML should be correctly indented",
version="0.1", check=check_yaml_indent,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_use_consistent_spaces_after_hyphens = Standard(dict( files_should_use_consistent_spaces_after_hyphens = Standard(
id="LINT0003", dict(
name="YAML should use consistent number of spaces after hyphens", id="LINT0003",
check=check_yaml_hyphens, name="YAML should use consistent number of spaces after hyphens",
version="0.1", check=check_yaml_hyphens,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_contain_document_start_marker = Standard(dict( files_should_contain_document_start_marker = Standard(
id="LINT0004", dict(
name="YAML should contain document start marker", id="LINT0004",
check=check_yaml_document_start, name="YAML should contain document start marker",
version="0.1", check=check_yaml_document_start,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
spaces_around_colons = Standard(dict( spaces_around_colons = Standard(
id="LINT0005", dict(
name="YAML should use consistent number of spaces around colons", id="LINT0005",
check=check_yaml_colons, name="YAML should use consistent number of spaces around colons",
version="0.1", check=check_yaml_colons,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
rolesfile_should_be_in_yaml = Standard(dict( rolesfile_should_be_in_yaml = Standard(
id="LINT0006", dict(
name="Roles file should be in yaml format", id="LINT0006",
check=check_yaml_file, name="Roles file should be in yaml format",
version="0.1", check=check_yaml_file,
types=["rolesfile"] version="0.1",
)) types=["rolesfile"]
)
)
files_should_not_be_purposeless = Standard(dict( files_should_not_be_purposeless = Standard(
id="LINT0007", dict(
name="Files should contain useful content", id="LINT0007",
check=check_yaml_has_content, name="Files should contain useful content",
version="0.1", check=check_yaml_has_content,
types=["playbook", "task", "handler", "rolevars", "defaults", "meta"] version="0.1",
)) types=["playbook", "task", "handler", "rolevars", "defaults", "meta"]
)
)
use_yaml_rather_than_key_value = Standard(dict( use_yaml_rather_than_key_value = Standard(
id="LINT0008", dict(
name="Use YAML format for tasks and handlers rather than key=value", id="LINT0008",
check=check_native_yaml, name="Use YAML format for tasks and handlers rather than key=value",
version="0.1", check=check_native_yaml,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
files_should_contain_document_end_marker = Standard(dict( files_should_contain_document_end_marker = Standard(
id="LINT0009", dict(
name="YAML should contain document end marker", id="LINT0009",
check=check_yaml_document_end, name="YAML should contain document end marker",
version="0.1", check=check_yaml_document_end,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
ansible_min_version = "2.5" ansible_min_version = "2.5"
ansible_later_min_version = "0.3.0" ansible_later_min_version = "0.3.0"
standards = [ standards = [
# Ansible # Ansible
tasks_should_be_separated, tasks_should_be_separated,

View File

@ -9,7 +9,7 @@ import colorama
from pythonjsonlogger import jsonlogger from pythonjsonlogger import jsonlogger
from six import iteritems from six import iteritems
CONSOLE_FORMAT = "%(levelname)s: %(message)s" CONSOLE_FORMAT = "%(levelname)s: {}%(message)s"
JSON_FORMAT = "(asctime) (levelname) (message)" JSON_FORMAT = "(asctime) (levelname) (message)"
@ -61,15 +61,16 @@ class LogFilter(object):
class MultilineFormatter(logging.Formatter): class MultilineFormatter(logging.Formatter):
"""Logging Formatter to reset color after newline characters.""" """Logging Formatter to reset color after newline characters."""
def format(self, record): # noqa def format(self, record): # noqa
record.msg = record.msg.replace("\n", "\n{}... ".format(colorama.Style.RESET_ALL)) record.msg = record.msg.replace("\n", "\n{}... ".format(colorama.Style.RESET_ALL))
record.msg = record.msg + "\n"
return logging.Formatter.format(self, record) return logging.Formatter.format(self, record)
class MultilineJsonFormatter(jsonlogger.JsonFormatter): class MultilineJsonFormatter(jsonlogger.JsonFormatter):
"""Logging Formatter to remove newline characters.""" """Logging Formatter to remove newline characters."""
def format(self, record): # noqa def format(self, record): # noqa
record.msg = record.msg.replace("\n", " ") record.msg = record.msg.replace("\n", " ")
return jsonlogger.JsonFormatter.format(self, record) return jsonlogger.JsonFormatter.format(self, record)
@ -157,22 +158,22 @@ def _get_critical_handler(json=False):
def critical(message): def critical(message):
"""Format critical messages and return string.""" """Format critical messages and return string."""
return color_text(colorama.Fore.RED, "{}".format(message)) return color_text(colorama.Fore.RED, message)
def error(message): def error(message):
"""Format error messages and return string.""" """Format error messages and return string."""
return color_text(colorama.Fore.RED, "{}".format(message)) return color_text(colorama.Fore.RED, message)
def warn(message): def warn(message):
"""Format warn messages and return string.""" """Format warn messages and return string."""
return color_text(colorama.Fore.YELLOW, "{}".format(message)) return color_text(colorama.Fore.YELLOW, message)
def info(message): def info(message):
"""Format info messages and return string.""" """Format info messages and return string."""
return color_text(colorama.Fore.BLUE, "{}".format(message)) return color_text(colorama.Fore.BLUE, message)
def color_text(color, msg): def color_text(color, msg):
@ -184,4 +185,5 @@ def color_text(color, msg):
:returns: string :returns: string
""" """
msg = msg.format(colorama.Style.BRIGHT)
return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL) return "{}{}{}".format(color, msg, colorama.Style.RESET_ALL)

View File

@ -16,7 +16,8 @@ def check_braces_spaces(candidate, settings):
yamllines, errors = get_normalized_yaml(candidate, settings) yamllines, errors = get_normalized_yaml(candidate, settings)
conf = settings["ansible"]["double-braces"] conf = settings["ansible"]["double-braces"]
description = "no suitable numbers of spaces (min: {min} max: {max})".format( description = "no suitable numbers of spaces (min: {min} max: {max})".format(
min=conf["min-spaces-inside"], max=conf["max-spaces-inside"]) min=conf["min-spaces-inside"], max=conf["max-spaces-inside"]
)
matches = [] matches = []
braces = re.compile("{{(.*?)}}") braces = re.compile("{{(.*?)}}")
@ -33,8 +34,8 @@ def check_braces_spaces(candidate, settings):
sum_spaces = leading + trailing sum_spaces = leading + trailing
if ( if (
(sum_spaces < conf["min-spaces-inside"] * 2) sum_spaces < conf["min-spaces-inside"] * 2
or (sum_spaces > conf["min-spaces-inside"] * 2) or sum_spaces > conf["min-spaces-inside"] * 2
): ):
errors.append(Error(i, description)) errors.append(Error(i, description))
return Result(candidate.path, errors) return Result(candidate.path, errors)
@ -42,9 +43,10 @@ def check_braces_spaces(candidate, settings):
def check_named_task(candidate, settings): def check_named_task(candidate, settings):
tasks, errors = get_normalized_tasks(candidate, settings) tasks, errors = get_normalized_tasks(candidate, settings)
nameless_tasks = ["meta", "debug", "include_role", "import_role", nameless_tasks = [
"include_tasks", "import_tasks", "include_vars", "meta", "debug", "include_role", "import_role", "include_tasks", "import_tasks",
"block"] "include_vars", "block"
]
description = "module '%s' used without or empty name attribute" description = "module '%s' used without or empty name attribute"
if not errors: if not errors:
@ -93,11 +95,22 @@ def check_command_instead_of_module(candidate, settings):
tasks, errors = get_normalized_tasks(candidate, settings) tasks, errors = get_normalized_tasks(candidate, settings)
commands = ["command", "shell", "raw"] commands = ["command", "shell", "raw"]
modules = { modules = {
"git": "git", "hg": "hg", "curl": "get_url or uri", "wget": "get_url or uri", "git": "git",
"svn": "subversion", "service": "service", "mount": "mount", "hg": "hg",
"rpm": "yum or rpm_key", "yum": "yum", "apt-get": "apt-get", "curl": "get_url or uri",
"unzip": "unarchive", "tar": "unarchive", "chkconfig": "service", "wget": "get_url or uri",
"rsync": "synchronize", "supervisorctl": "supervisorctl", "systemctl": "systemd", "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" "sed": "template or lineinfile"
} }
description = "%s command used in place of %s module" description = "%s command used in place of %s module"
@ -111,26 +124,32 @@ def check_command_instead_of_module(candidate, settings):
first_cmd_arg = task["action"]["__ansible_arguments__"][0] first_cmd_arg = task["action"]["__ansible_arguments__"][0]
executable = os.path.basename(first_cmd_arg) executable = os.path.basename(first_cmd_arg)
if (first_cmd_arg and executable in modules if (
and task["action"].get("warn", True) and "register" not in task): first_cmd_arg and executable in modules and task["action"].get("warn", True)
and "register" not in task
):
errors.append( errors.append(
Error(task["__line__"], description % (executable, modules[executable]))) Error(task["__line__"], description % (executable, modules[executable]))
)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_install_use_latest(candidate, settings): def check_install_use_latest(candidate, settings):
tasks, errors = get_normalized_tasks(candidate, settings) tasks, errors = get_normalized_tasks(candidate, settings)
package_managers = ["yum", "apt", "dnf", "homebrew", "pacman", "openbsd_package", "pkg5", package_managers = [
"portage", "pkgutil", "slackpkg", "swdepot", "zypper", "bundler", "pip", "yum", "apt", "dnf", "homebrew", "pacman", "openbsd_package", "pkg5", "portage", "pkgutil",
"pear", "npm", "yarn", "gem", "easy_install", "bower", "package", "apk", "slackpkg", "swdepot", "zypper", "bundler", "pip", "pear", "npm", "yarn", "gem",
"openbsd_pkg", "pkgng", "sorcery", "xbps"] "easy_install", "bower", "package", "apk", "openbsd_pkg", "pkgng", "sorcery", "xbps"
]
description = "package installs should use state=present with or without a version" description = "package installs should use state=present with or without a version"
if not errors: if not errors:
for task in tasks: for task in tasks:
if (task["action"]["__ansible_module__"] in package_managers if (
and task["action"].get("state") == "latest"): task["action"]["__ansible_module__"] in package_managers
and task["action"].get("state") == "latest"
):
errors.append(Error(task["__line__"], description)) errors.append(Error(task["__line__"], description))
return Result(candidate.path, errors) return Result(candidate.path, errors)
@ -165,10 +184,11 @@ def check_command_has_changes(candidate, settings):
if not errors: if not errors:
for task in tasks: for task in tasks:
if task["action"]["__ansible_module__"] in commands: if task["action"]["__ansible_module__"] in commands:
if ("changed_when" not in task and "when" not in task if (
and "when" not in task["__ansible_action_meta__"] "changed_when" not in task and "when" not in task
and "creates" not in task["action"] and "when" not in task["__ansible_action_meta__"]
and "removes" not in task["action"]): and "creates" not in task["action"] and "removes" not in task["action"]
):
errors.append(Error(task["__line__"], description)) errors.append(Error(task["__line__"], description))
return Result(candidate.path, errors) return Result(candidate.path, errors)

View File

@ -49,43 +49,39 @@ def check_native_yaml(candidate, settings):
def check_yaml_empty_lines(candidate, settings): def check_yaml_empty_lines(candidate, settings):
options = "rules: {{empty-lines: {conf}}}".format( options = "rules: {{empty-lines: {conf}}}".format(conf=settings["yamllint"]["empty-lines"])
conf=settings["yamllint"]["empty-lines"])
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_yaml_indent(candidate, settings): def check_yaml_indent(candidate, settings):
options = "rules: {{indentation: {conf}}}".format( options = "rules: {{indentation: {conf}}}".format(conf=settings["yamllint"]["indentation"])
conf=settings["yamllint"]["indentation"])
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_yaml_hyphens(candidate, settings): def check_yaml_hyphens(candidate, settings):
options = "rules: {{hyphens: {conf}}}".format( options = "rules: {{hyphens: {conf}}}".format(conf=settings["yamllint"]["hyphens"])
conf=settings["yamllint"]["hyphens"])
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_yaml_document_start(candidate, settings): def check_yaml_document_start(candidate, settings):
options = "rules: {{document-start: {conf}}}".format( options = "rules: {{document-start: {conf}}}".format(
conf=settings["yamllint"]["document-start"]) conf=settings["yamllint"]["document-start"]
)
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_yaml_document_end(candidate, settings): def check_yaml_document_end(candidate, settings):
options = "rules: {{document-end: {conf}}}".format( options = "rules: {{document-end: {conf}}}".format(conf=settings["yamllint"]["document-end"])
conf=settings["yamllint"]["document-end"])
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
def check_yaml_colons(candidate, settings): def check_yaml_colons(candidate, settings):
options = "rules: {{colons: {conf}}}".format( options = "rules: {{colons: {conf}}}".format(conf=settings["yamllint"]["colons"])
conf=settings["yamllint"]["colons"])
errors = run_yamllint(candidate.path, options) errors = run_yamllint(candidate.path, options)
return Result(candidate.path, errors) return Result(candidate.path, errors)
@ -95,14 +91,12 @@ def check_yaml_file(candidate, settings):
filename = candidate.path filename = candidate.path
if os.path.isfile(filename) and os.path.splitext(filename)[1][1:] != "yml": if os.path.isfile(filename) and os.path.splitext(filename)[1][1:] != "yml":
errors.append( errors.append(Error(None, "file does not have a .yml extension"))
Error(None, "file does not have a .yml extension"))
elif os.path.isfile(filename) and os.path.splitext(filename)[1][1:] == "yml": elif os.path.isfile(filename) and os.path.splitext(filename)[1][1:] == "yml":
with codecs.open(filename, mode="rb", encoding="utf-8") as f: with codecs.open(filename, mode="rb", encoding="utf-8") as f:
try: try:
yaml.safe_load(f) yaml.safe_load(f)
except Exception as e: except Exception as e:
errors.append( errors.append(Error(e.problem_mark.line + 1, "syntax error: %s" % (e.problem)))
Error(e.problem_mark.line + 1, "syntax error: %s" % (e.problem)))
return Result(candidate.path, errors) return Result(candidate.path, errors)

View File

@ -71,8 +71,9 @@ class Settings(object):
defaults = self._get_defaults() defaults = self._get_defaults()
source_files = [] source_files = []
source_files.append(self.config_file) source_files.append(self.config_file)
source_files.append(os.path.relpath( source_files.append(
os.path.normpath(os.path.join(os.getcwd(), ".later.yml")))) os.path.relpath(os.path.normpath(os.path.join(os.getcwd(), ".later.yml")))
)
cli_options = self.args cli_options = self.args
for config in source_files: for config in source_files:
@ -147,10 +148,11 @@ class Settings(object):
return True return True
except Exception as e: except Exception as e:
schema_error = "Failed validating '{validator}' in schema{schema}".format( schema_error = "Failed validating '{validator}' in schema{schema}".format(
validator=e.validator, validator=e.validator, schema=format_as_index(list(e.relative_schema_path)[:-1])
schema=format_as_index(list(e.relative_schema_path)[:-1]) )
utils.sysexit_with_message(
"{schema}: {msg}".format(schema=schema_error, msg=e.message)
) )
utils.sysexit_with_message("{schema}: {msg}".format(schema=schema_error, msg=e.message))
def _update_filelist(self): def _update_filelist(self):
includes = self.config["rules"]["files"] includes = self.config["rules"]["files"]
@ -165,8 +167,7 @@ class Settings(object):
filelist = [] filelist = []
for root, dirs, files in os.walk("."): for root, dirs, files in os.walk("."):
for filename in files: for filename in files:
filelist.append( filelist.append(os.path.relpath(os.path.normpath(os.path.join(root, filename))))
os.path.relpath(os.path.normpath(os.path.join(root, filename))))
valid = [] valid = []
includespec = pathspec.PathSpec.from_lines("gitwildmatch", includes) includespec = pathspec.PathSpec.from_lines("gitwildmatch", includes)

View File

@ -22,7 +22,5 @@ class Standard(object):
self.check = standard_dict.get("check") self.check = standard_dict.get("check")
self.types = standard_dict.get("types") self.types = standard_dict.get("types")
def __repr__(self): # noqa
def __repr__(self): # noqa return "Standard: %s (version: %s, types: %s)" % (self.name, self.version, self.types)
return "Standard: %s (version: %s, types: %s)" % (
self.name, self.version, self.types)

View File

@ -21,183 +21,217 @@ from ansiblelater.rules.yamlfiles import check_yaml_has_content
from ansiblelater.rules.yamlfiles import check_yaml_hyphens from ansiblelater.rules.yamlfiles import check_yaml_hyphens
from ansiblelater.rules.yamlfiles import check_yaml_indent from ansiblelater.rules.yamlfiles import check_yaml_indent
tasks_should_be_separated = Standard(dict( tasks_should_be_separated = Standard(
id="ANSIBLE0001", dict(
name="Single tasks should be separated by empty line", id="ANSIBLE0001",
check=check_line_between_tasks, name="Single tasks should be separated by empty line",
version="0.1", check=check_line_between_tasks,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
role_must_contain_meta_main = Standard(dict( role_must_contain_meta_main = Standard(
id="ANSIBLE0002", dict(
name="Roles must contain suitable meta/main.yml", id="ANSIBLE0002",
check=check_meta_main, name="Roles must contain suitable meta/main.yml",
version="0.1", check=check_meta_main,
types=["meta"] version="0.1",
)) types=["meta"]
)
)
tasks_are_uniquely_named = Standard(dict( tasks_are_uniquely_named = Standard(
id="ANSIBLE0003", dict(
name="Tasks and handlers must be uniquely named within a single file", id="ANSIBLE0003",
check=check_unique_named_task, name="Tasks and handlers must be uniquely named within a single file",
version="0.1", check=check_unique_named_task,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
use_spaces_between_variable_braces = Standard(dict( use_spaces_between_variable_braces = Standard(
id="ANSIBLE0004", dict(
name="YAML should use consistent number of spaces around variables", id="ANSIBLE0004",
check=check_braces_spaces, name="YAML should use consistent number of spaces around variables",
version="0.1", check=check_braces_spaces,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
roles_scm_not_in_src = Standard(dict( roles_scm_not_in_src = Standard(
id="ANSIBLE0005", dict(
name="Use scm key rather than src: scm+url", id="ANSIBLE0005",
check=check_scm_in_src, name="Use scm key rather than src: scm+url",
version="0.1", check=check_scm_in_src,
types=["rolesfile"] version="0.1",
)) types=["rolesfile"]
)
)
tasks_are_named = Standard(dict( tasks_are_named = Standard(
id="ANSIBLE0006", dict(
name="Tasks and handlers must be named", id="ANSIBLE0006",
check=check_named_task, name="Tasks and handlers must be named",
version="0.1", check=check_named_task,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
tasks_names_are_formatted = Standard(dict( tasks_names_are_formatted = Standard(
id="ANSIBLE0007", dict(
name="Name of tasks and handlers must be formatted", id="ANSIBLE0007",
check=check_name_format, name="Name of tasks and handlers must be formatted",
version="0.1", check=check_name_format,
types=["playbook", "task", "handler"], version="0.1",
)) types=["playbook", "task", "handler"],
)
)
commands_should_not_be_used_in_place_of_modules = Standard(dict( commands_should_not_be_used_in_place_of_modules = Standard(
id="ANSIBLE0008", dict(
name="Commands should not be used in place of modules", id="ANSIBLE0008",
check=check_command_instead_of_module, name="Commands should not be used in place of modules",
version="0.1", check=check_command_instead_of_module,
types=["playbook", "task", "handler"] version="0.1",
)) types=["playbook", "task", "handler"]
)
)
package_installs_should_not_use_latest = Standard(dict( package_installs_should_not_use_latest = Standard(
id="ANSIBLE0009", dict(
name="Package installs should use present, not latest", id="ANSIBLE0009",
check=check_install_use_latest, name="Package installs should use present, not latest",
types=["playbook", "task", "handler"] check=check_install_use_latest,
)) types=["playbook", "task", "handler"]
)
)
use_shell_only_when_necessary = Standard(dict( use_shell_only_when_necessary = Standard(
id="ANSIBLE0010", dict(
name="Shell should only be used when essential", id="ANSIBLE0010",
check=check_shell_instead_command, name="Shell should only be used when essential",
types=["playbook", "task", "handler"] check=check_shell_instead_command,
)) types=["playbook", "task", "handler"]
)
)
commands_should_be_idempotent = Standard(dict( commands_should_be_idempotent = Standard(
id="ANSIBLE0011", dict(
name="Commands should be idempotent", id="ANSIBLE0011",
check=check_command_has_changes, name="Commands should be idempotent",
version="0.1", check=check_command_has_changes,
types=["playbook", "task"] version="0.1",
)) types=["playbook", "task"]
)
)
dont_compare_to_empty_string = Standard(dict( dont_compare_to_empty_string = Standard(
id="ANSIBLE0012", dict(
name="Don't compare to \"\" - use `when: var` or `when: not var`", id="ANSIBLE0012",
check=check_empty_string_compare, name="Don't compare to \"\" - use `when: var` or `when: not var`",
version="0.1", check=check_empty_string_compare,
types=["playbook", "task", "handler", "template"] version="0.1",
)) types=["playbook", "task", "handler", "template"]
)
)
dont_compare_to_literal_bool = Standard(dict( dont_compare_to_literal_bool = Standard(
id="ANSIBLE0013", dict(
name="Don't compare to True or False - use `when: var` or `when: not var`", id="ANSIBLE0013",
check=check_compare_to_literal_bool, name="Don't compare to True or False - use `when: var` or `when: not var`",
version="0.1", check=check_compare_to_literal_bool,
types=["playbook", "task", "handler", "template"] version="0.1",
)) types=["playbook", "task", "handler", "template"]
)
)
files_should_not_contain_unnecessarily_empty_lines = Standard(dict( files_should_not_contain_unnecessarily_empty_lines = Standard(
id="LINT0001", dict(
name="YAML should not contain unnecessarily empty lines", id="LINT0001",
check=check_yaml_empty_lines, name="YAML should not contain unnecessarily empty lines",
version="0.1", check=check_yaml_empty_lines,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_be_indented = Standard(dict( files_should_be_indented = Standard(
id="LINT0002", dict(
name="YAML should be correctly indented", id="LINT0002",
check=check_yaml_indent, name="YAML should be correctly indented",
version="0.1", check=check_yaml_indent,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_use_consistent_spaces_after_hyphens = Standard(dict( files_should_use_consistent_spaces_after_hyphens = Standard(
id="LINT0003", dict(
name="YAML should use consistent number of spaces after hyphens", id="LINT0003",
check=check_yaml_hyphens, name="YAML should use consistent number of spaces after hyphens",
version="0.1", check=check_yaml_hyphens,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
files_should_contain_document_start_marker = Standard(dict( files_should_contain_document_start_marker = Standard(
id="LINT0004", dict(
name="YAML should contain document start marker", id="LINT0004",
check=check_yaml_document_start, name="YAML should contain document start marker",
version="0.1", check=check_yaml_document_start,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
spaces_around_colons = Standard(dict( spaces_around_colons = Standard(
id="LINT0005", dict(
name="YAML should use consistent number of spaces around colons", id="LINT0005",
check=check_yaml_colons, name="YAML should use consistent number of spaces around colons",
version="0.1", check=check_yaml_colons,
types=["playbook", "task", "handler", "rolevars", version="0.1",
"hostvars", "groupvars", "meta"] types=["playbook", "task", "handler", "rolevars", "hostvars", "groupvars", "meta"]
)) )
)
rolesfile_should_be_in_yaml = Standard(dict( rolesfile_should_be_in_yaml = Standard(
id="LINT0006", dict(
name="Roles file should be in yaml format", id="LINT0006",
check=check_yaml_file, name="Roles file should be in yaml format",
version="0.1", check=check_yaml_file,
types=["rolesfile"] version="0.1",
)) types=["rolesfile"]
)
)
files_should_not_be_purposeless = Standard(dict( files_should_not_be_purposeless = Standard(
id="LINT0007", dict(
name="Files should contain useful content", id="LINT0007",
check=check_yaml_has_content, name="Files should contain useful content",
version="0.1", check=check_yaml_has_content,
types=["playbook", "task", "handler", "rolevars", "defaults", "meta"] 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"]
))
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_min_version = '2.1'
ansible_later_min_version = '0.1.0' ansible_later_min_version = '0.1.0'
standards = [ standards = [
# Ansible # Ansible
tasks_should_be_separated, tasks_should_be_separated,

View File

@ -21,8 +21,11 @@ def test_critical(capsys, mocker):
log.critical("foo") log.critical("foo")
_, stderr = capsys.readouterr() _, stderr = capsys.readouterr()
print("{}{}{}".format(colorama.Fore.RED, "CRITICAL: foo".rstrip(), print(
colorama.Style.RESET_ALL)) "{}CRITICAL: {}foo\n{}".format(
colorama.Fore.RED, colorama.Style.BRIGHT, colorama.Style.RESET_ALL
)
)
x, _ = capsys.readouterr() x, _ = capsys.readouterr()
assert x == stderr assert x == stderr
@ -33,8 +36,11 @@ def test_error(capsys, mocker):
log.error("foo") log.error("foo")
_, stderr = capsys.readouterr() _, stderr = capsys.readouterr()
print("{}{}{}".format(colorama.Fore.RED, "ERROR: foo".rstrip(), print(
colorama.Style.RESET_ALL)) "{}ERROR: {}foo\n{}".format(
colorama.Fore.RED, colorama.Style.BRIGHT, colorama.Style.RESET_ALL
)
)
x, _ = capsys.readouterr() x, _ = capsys.readouterr()
assert x == stderr assert x == stderr
@ -45,8 +51,11 @@ def test_warn(capsys, mocker):
log.warning("foo") log.warning("foo")
stdout, _ = capsys.readouterr() stdout, _ = capsys.readouterr()
print("{}{}{}".format(colorama.Fore.YELLOW, "WARNING: foo".rstrip(), print(
colorama.Style.RESET_ALL)) "{}WARNING: {}foo\n{}".format(
colorama.Fore.YELLOW, colorama.Style.BRIGHT, colorama.Style.RESET_ALL
)
)
x, _ = capsys.readouterr() x, _ = capsys.readouterr()
assert x == stdout assert x == stdout
@ -57,8 +66,11 @@ def test_info(capsys, mocker):
log.info("foo") log.info("foo")
stdout, _ = capsys.readouterr() stdout, _ = capsys.readouterr()
print("{}{}{}".format(colorama.Fore.BLUE, "INFO: foo".rstrip(), print(
colorama.Style.RESET_ALL)) "{}INFO: {}foo\n{}".format(
colorama.Fore.BLUE, colorama.Style.BRIGHT, colorama.Style.RESET_ALL
)
)
x, _ = capsys.readouterr() x, _ = capsys.readouterr()
assert x == stdout assert x == stdout

View File

@ -13,10 +13,9 @@ import yaml
from ansiblelater import logger from ansiblelater import logger
try: try:
import ConfigParser as configparser # noqa import ConfigParser as configparser # noqa
except ImportError: except ImportError:
import configparser # noqa import configparser # noqa
LOG = logger.get_logger(__name__) LOG = logger.get_logger(__name__)
@ -35,7 +34,7 @@ def count_spaces(c_string):
break break
trailing_spaces += 1 trailing_spaces += 1
return((leading_spaces, trailing_spaces)) return ((leading_spaces, trailing_spaces))
def get_property(prop): def get_property(prop):
@ -43,7 +42,8 @@ def get_property(prop):
parentdir = os.path.dirname(currentdir) parentdir = os.path.dirname(currentdir)
result = re.search( result = re.search(
r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop), r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop),
open(os.path.join(parentdir, "__init__.py")).read()) open(os.path.join(parentdir, "__init__.py")).read()
)
return result.group(1) return result.group(1)

View File

@ -81,7 +81,8 @@ def get_normalized_tasks(candidate, settings):
# No need to normalize_task if we are skipping it. # No need to normalize_task if we are skipping it.
continue continue
normalized.append( normalized.append(
normalize_task(task, candidate.path, settings["ansible"]["custom_modules"])) normalize_task(task, candidate.path, settings["ansible"]["custom_modules"])
)
except LaterError as ex: except LaterError as ex:
e = ex.original e = ex.original

View File

@ -23,7 +23,6 @@
import codecs import codecs
import glob import glob
import imp import imp
import inspect
import os import os
import ansible.parsing.mod_args import ansible.parsing.mod_args
@ -78,14 +77,46 @@ LINE_NUMBER_KEY = "__line__"
FILENAME_KEY = "__file__" FILENAME_KEY = "__file__"
VALID_KEYS = [ VALID_KEYS = [
"name", "action", "when", "async", "poll", "notify", "name",
"first_available_file", "include", "import_playbook", "action",
"tags", "register", "ignore_errors", "delegate_to", "when",
"local_action", "transport", "remote_user", "sudo", "async",
"sudo_user", "sudo_pass", "when", "connection", "environment", "args", "always_run", "poll",
"any_errors_fatal", "changed_when", "failed_when", "check_mode", "delay", "notify",
"retries", "until", "su", "su_user", "su_pass", "no_log", "run_once", "first_available_file",
"become", "become_user", "become_method", FILENAME_KEY, "include",
"import_playbook",
"tags",
"register",
"ignore_errors",
"delegate_to",
"local_action",
"transport",
"remote_user",
"sudo",
"sudo_user",
"sudo_pass",
"when",
"connection",
"environment",
"args",
"always_run",
"any_errors_fatal",
"changed_when",
"failed_when",
"check_mode",
"delay",
"retries",
"until",
"su",
"su_user",
"su_pass",
"no_log",
"run_once",
"become",
"become_user",
"become_method",
FILENAME_KEY,
] ]
BLOCK_NAME_TO_ACTION_TYPE_MAP = { BLOCK_NAME_TO_ACTION_TYPE_MAP = {
@ -170,17 +201,16 @@ def find_children(playbook, playbook_dir):
break break
valid_tokens.append(token) valid_tokens.append(token)
path = " ".join(valid_tokens) path = " ".join(valid_tokens)
results.append({ results.append({"path": path_dwim(basedir, path), "type": child["type"]})
"path": path_dwim(basedir, path),
"type": child["type"]
})
return results return results
def template(basedir, value, variables, fail_on_undefined=False, **kwargs): def template(basedir, value, variables, fail_on_undefined=False, **kwargs):
try: try:
value = ansible_template(os.path.abspath(basedir), value, variables, value = ansible_template(
**dict(kwargs, fail_on_undefined=fail_on_undefined)) os.path.abspath(basedir), value, variables,
**dict(kwargs, fail_on_undefined=fail_on_undefined)
)
# Hack to skip the following exception when using to_json filter on a variable. # Hack to skip the following exception when using to_json filter on a variable.
# I guess the filter doesn't like empty vars... # I guess the filter doesn't like empty vars...
except (AnsibleError, ValueError): except (AnsibleError, ValueError):
@ -207,10 +237,12 @@ def play_children(basedir, item, parent_type, playbook_dir):
if k in delegate_map: if k in delegate_map:
if v: if v:
v = template(os.path.abspath(basedir), v = template(
v, os.path.abspath(basedir),
dict(playbook_dir=os.path.abspath(basedir)), v,
fail_on_undefined=False) dict(playbook_dir=os.path.abspath(basedir)),
fail_on_undefined=False
)
return delegate_map[k](basedir, k, v, parent_type) return delegate_map[k](basedir, k, v, parent_type)
return [] return []
@ -237,12 +269,23 @@ def _taskshandlers_children(basedir, k, v, parent_type):
elif "import_tasks" in th: elif "import_tasks" in th:
append_children(th["import_tasks"], basedir, k, parent_type, results) append_children(th["import_tasks"], basedir, k, parent_type, results)
elif "import_role" in th: elif "import_role" in th:
results.extend(_roles_children(basedir, k, [th["import_role"].get("name")], parent_type, results.extend(
main=th["import_role"].get("tasks_from", "main"))) _roles_children(
basedir,
k, [th["import_role"].get("name")],
parent_type,
main=th["import_role"].get("tasks_from", "main")
)
)
elif "include_role" in th: elif "include_role" in th:
results.extend(_roles_children(basedir, k, [th["include_role"].get("name")], results.extend(
parent_type, _roles_children(
main=th["include_role"].get("tasks_from", "main"))) basedir,
k, [th["include_role"].get("name")],
parent_type,
main=th["include_role"].get("tasks_from", "main")
)
)
elif "block" in th: elif "block" in th:
results.extend(_taskshandlers_children(basedir, k, th["block"], parent_type)) results.extend(_taskshandlers_children(basedir, k, th["block"], parent_type))
if "rescue" in th: if "rescue" in th:
@ -260,10 +303,7 @@ def append_children(taskhandler, basedir, k, parent_type, results):
playbook_section = k playbook_section = k
else: else:
playbook_section = parent_type playbook_section = parent_type
results.append({ results.append({"path": path_dwim(basedir, taskhandler), "type": playbook_section})
"path": path_dwim(basedir, taskhandler),
"type": playbook_section
})
def _roles_children(basedir, k, v, parent_type, main="main"): def _roles_children(basedir, k, v, parent_type, main="main"):
@ -272,12 +312,16 @@ def _roles_children(basedir, k, v, parent_type, main="main"):
if isinstance(role, dict): if isinstance(role, dict):
if "role" in role or "name" in role: if "role" in role or "name" in role:
if "tags" not in role or "skip_ansible_later" not in role["tags"]: if "tags" not in role or "skip_ansible_later" not in role["tags"]:
results.extend(_look_for_role_files(basedir, results.extend(
role.get("role", role.get("name")), _look_for_role_files(
main=main)) basedir, role.get("role", role.get("name")), main=main
)
)
else: else:
raise SystemExit("role dict {0} does not contain a 'role' " raise SystemExit(
"or 'name' key".format(role)) "role dict {0} does not contain a 'role' "
"or 'name' key".format(role)
)
else: else:
results.extend(_look_for_role_files(basedir, role, main=main)) results.extend(_look_for_role_files(basedir, role, main=main))
return results return results
@ -296,9 +340,7 @@ def _rolepath(basedir, role):
path_dwim(basedir, os.path.join("roles", role)), path_dwim(basedir, os.path.join("roles", role)),
path_dwim(basedir, role), path_dwim(basedir, role),
# if included from roles/[role]/meta/main.yml # if included from roles/[role]/meta/main.yml
path_dwim( path_dwim(basedir, os.path.join("..", "..", "..", "roles", role)),
basedir, os.path.join("..", "..", "..", "roles", role)
),
path_dwim(basedir, os.path.join("..", "..", role)) path_dwim(basedir, os.path.join("..", "..", role))
] ]
@ -356,7 +398,7 @@ def normalize_task(task, filename, custom_modules=[]):
ansible_action_type = task.get("__ansible_action_type__", "task") ansible_action_type = task.get("__ansible_action_type__", "task")
ansible_action_meta = task.get("__ansible_action_meta__", dict()) ansible_action_meta = task.get("__ansible_action_meta__", dict())
if "__ansible_action_type__" in task: if "__ansible_action_type__" in task:
del(task["__ansible_action_type__"]) del (task["__ansible_action_type__"])
normalized = dict() normalized = dict()
# TODO: Workaround for custom modules # TODO: Workaround for custom modules
@ -372,7 +414,7 @@ def normalize_task(task, filename, custom_modules=[]):
# denormalize shell -> command conversion # denormalize shell -> command conversion
if "_uses_shell" in arguments: if "_uses_shell" in arguments:
action = "shell" action = "shell"
del(arguments["_uses_shell"]) del (arguments["_uses_shell"])
for (k, v) in list(task.items()): for (k, v) in list(task.items()):
if k in ("action", "local_action", "args", "delegate_to") or k == action: if k in ("action", "local_action", "args", "delegate_to") or k == action:
@ -386,7 +428,7 @@ def normalize_task(task, filename, custom_modules=[]):
if "_raw_params" in arguments: if "_raw_params" in arguments:
normalized["action"]["__ansible_arguments__"] = arguments["_raw_params"].strip().split() normalized["action"]["__ansible_arguments__"] = arguments["_raw_params"].strip().split()
del(arguments["_raw_params"]) del (arguments["_raw_params"])
else: else:
normalized["action"]["__ansible_arguments__"] = list() normalized["action"]["__ansible_arguments__"] = list()
normalized["action"].update(arguments) normalized["action"].update(arguments)
@ -410,8 +452,9 @@ def action_tasks(yaml, file):
block_rescue_always = ("block", "rescue", "always") block_rescue_always = ("block", "rescue", "always")
tasks[:] = [task for task in tasks if all(k not in task for k in block_rescue_always)] tasks[:] = [task for task in tasks if all(k not in task for k in block_rescue_always)]
return [task for task in tasks if set( allowed = ["include", "include_tasks", "import_playbook", "import_tasks"]
["include", "include_tasks", "import_playbook", "import_tasks"]).isdisjoint(task.keys())]
return [task for task in tasks if set(allowed).isdisjoint(task.keys())]
def task_to_str(task): def task_to_str(task):
@ -419,9 +462,11 @@ def task_to_str(task):
if name: if name:
return name return name
action = task.get("action") action = task.get("action")
args = " ".join([u"{0}={1}".format(k, v) for (k, v) in action.items() args = " ".join([
if k not in ["__ansible_module__", "__ansible_arguments__"] u"{0}={1}".format(k, v)
] + action.get("__ansible_arguments__")) for (k, v) in action.items()
if k not in ["__ansible_module__", "__ansible_arguments__"]
] + action.get("__ansible_arguments__"))
return u"{0} {1}".format(action["__ansible_module__"], args) return u"{0} {1}".format(action["__ansible_module__"], args)
@ -439,7 +484,8 @@ def extract_from_list(blocks, candidates):
elif block[candidate] is not None: elif block[candidate] is not None:
raise RuntimeError( raise RuntimeError(
"Key '%s' defined, but bad value: '%s'" % "Key '%s' defined, but bad value: '%s'" %
(candidate, str(block[candidate]))) (candidate, str(block[candidate]))
)
return results return results
@ -460,6 +506,7 @@ def parse_yaml_linenumbers(data, filename):
The line numbers are stored in each node's LINE_NUMBER_KEY key. The line numbers are stored in each node's LINE_NUMBER_KEY key.
""" """
def compose_node(parent, index): def compose_node(parent, index):
# the line number where the previous token has ended (plus empty lines) # the line number where the previous token has ended (plus empty lines)
line = loader.line line = loader.line

View File

@ -10,11 +10,21 @@ default_section = THIRDPARTY
known_first_party = ansiblelater known_first_party = ansiblelater
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
force_single_line = true force_single_line = true
line_length = 110 line_length = 99
skip_glob = **/.env/*,**/docs/* skip_glob = **/.env*,**/env/*,**/docs/*
[yapf]
based_on_style = google
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true
[tool:pytest] [tool:pytest]
filterwarnings = filterwarnings =
ignore::FutureWarning ignore::FutureWarning
ignore:.*collections.*:DeprecationWarning ignore:.*collections.*:DeprecationWarning
ignore:.*pep8.*:FutureWarning ignore:.*pep8.*:FutureWarning
[coverage:run]
omit = **/tests/*

View File

@ -15,7 +15,8 @@ def get_property(prop, project):
current_dir = os.path.dirname(os.path.realpath(__file__)) current_dir = os.path.dirname(os.path.realpath(__file__))
result = re.search( result = re.search(
r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop), r'{}\s*=\s*[\'"]([^\'"]*)[\'"]'.format(prop),
open(os.path.join(current_dir, project, "__init__.py")).read()) open(os.path.join(current_dir, project, "__init__.py")).read()
)
return result.group(1) return result.group(1)
@ -28,12 +29,12 @@ def get_readme(filename="README.md"):
setup( setup(
name=get_property("__project__", PACKAGE_NAME), name=get_property("__project__", PACKAGE_NAME),
version=get_property("__version__", PACKAGE_NAME),
description=("Reviews ansible playbooks, roles and inventories and suggests improvements."), description=("Reviews ansible playbooks, roles and inventories and suggests improvements."),
keywords="ansible code review", keywords="ansible code review",
version=get_property("__version__", PACKAGE_NAME),
author=get_property("__author__", PACKAGE_NAME), author=get_property("__author__", PACKAGE_NAME),
author_email=get_property("__email__", PACKAGE_NAME), author_email=get_property("__email__", PACKAGE_NAME),
url="https://github.com/xoxys/ansible-later", url=get_property("__url__", PACKAGE_NAME),
license=get_property("__license__", PACKAGE_NAME), license=get_property("__license__", PACKAGE_NAME),
long_description=get_readme(), long_description=get_readme(),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -59,25 +60,9 @@ setup(
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=[ install_requires=[
"ansible", "ansible", "six", "pyyaml", "appdirs", "unidiff", "flake8", "yamllint", "nested-lookup",
"six", "colorama", "anyconfig", "python-json-logger", "jsonschema", "pathspec", "toolz"
"pyyaml",
"appdirs",
"unidiff",
"flake8",
"yamllint",
"nested-lookup",
"colorama",
"anyconfig",
"python-json-logger",
"jsonschema",
"pathspec",
"toolz"
], ],
entry_points={ entry_points={"console_scripts": ["ansible-later = ansiblelater.__main__:main"]},
"console_scripts": [
"ansible-later = ansiblelater.__main__:main"
]
},
test_suite="tests" test_suite="tests"
) )

View File

@ -1,11 +1,8 @@
# open issue
# https://gitlab.com/pycqa/flake8-docstrings/issues/36
pydocstyle<4.0.0 pydocstyle<4.0.0
flake8 flake8
flake8-colors flake8-colors
flake8-blind-except flake8-blind-except
flake8-builtins flake8-builtins
flake8-colors
flake8-docstrings<=3.0.0 flake8-docstrings<=3.0.0
flake8-isort flake8-isort
flake8-logging-format flake8-logging-format
@ -17,3 +14,5 @@ pytest
pytest-mock pytest-mock
pytest-cov pytest-cov
bandit bandit
requests-mock
yapf

View File

@ -15,4 +15,4 @@ deps =
ansibledevel: git+https://github.com/ansible/ansible.git ansibledevel: git+https://github.com/ansible/ansible.git
commands = commands =
ansible-later --help ansible-later --help
pytest ansiblelater/tests/ --cov={toxinidir}/ansiblelater/ --no-cov-on-fail pytest --cov={toxinidir}/ansiblelater --no-cov-on-fail