Commit 14073d9c authored by Gregory Colpart's avatar Gregory Colpart

Import upstream from Git (commit 51c7087d87cedd22b554828cb220ff6ef61efab9)

parent 078e6d46
......@@ -51,6 +51,8 @@ Options:
repeatable.
--force-color Try force colored output (relying on ansible's code)
--nocolor disable colored output
-c /path/to/file Specify configuration file to use. Defaults to
".ansible-lint"
```
False positives
......@@ -167,6 +169,7 @@ In ansible-lint 3.0.0 `task['action']['module']` was renamed
`task['action']['__ansible_module__']` to avoid a clash when a module take
`module` as an argument. As a precaution, `task['action']['module_arguments']`
was renamed `task['action']['__ansible_arguments__']`
Examples
--------
......@@ -222,6 +225,34 @@ As of version 2.4.0, ansible-lint now works just on roles (this is useful
for CI of roles)
Configuration File
==================
Ansible-lint supports local configuration via a `.ansible-lint` configuration file. Ansible-lint checks the working directory for the presence of this file and applies any configuration found there. The configuration file location can also be overridden via the `-c path/to/file` CLI flag.
The following values are supported and function identically to their CLI counterparts.
If a value is provided on both the command line and via a config file, the values will be merged (if a list like `exclude_paths`), or the "True" value will be preferred, in the case of something like `quiet`.
```yaml
exclude_paths:
- ./my/excluded/directory/
- ./my/other/excluded/directory/
- ./last/excluded/directory/
parseable: true
quiet: true
rulesdir:
- ./rule/directory/
skip_list:
- skip_this_tag
- and_this_one_too
tags:
- run_this_tag
use_default_rules: true
verbosity: 1
```
Pre-commit
==========
......
#!/usr/bin/env python
import sys
import ansiblelint.__main__
sys.exit(ansiblelint.__main__.main())
......@@ -20,6 +20,12 @@ class TaskHasTag(AnsibleLintRule):
if not set(task.keys()).isdisjoint(['include', 'fail']):
return False
if not set(task.keys()).isdisjoint(['include_tasks', 'fail']):
return False
if not set(task.keys()).isdisjoint(['import_tasks', 'fail']):
return False
# Task should have tags
if 'tags' not in task:
return True
......
......@@ -31,21 +31,39 @@ import ansiblelint.formatters as formatters
import six
from ansiblelint import RulesCollection
from ansiblelint.version import __version__
import yaml
import os
def load_config(config_file):
config_path = config_file if config_file else ".ansible-lint"
if os.path.exists(config_path):
with open(config_path, "r") as stream:
try:
return yaml.load(stream)
except yaml.YAMLError:
pass
return None
def main():
formatter = formatters.Formatter()
parser = optparse.OptionParser("%prog playbook.yml",
parser = optparse.OptionParser("%prog [options] playbook.yml [playbook2 ...]",
version="%prog " + __version__)
parser.add_option('-L', dest='listrules', default=False,
action='store_true', help="list all the rules")
parser.add_option('-q', dest='quiet', default=False, action='store_true',
parser.add_option('-q', dest='quiet',
default=False,
action='store_true',
help="quieter, although not silent output")
parser.add_option('-p', dest='parseable',
default=False, action='store_true',
default=False,
action='store_true',
help="parseable output in the format of pep8")
parser.add_option('-r', action='append', dest='rulesdir',
default=[], type='str',
......@@ -53,18 +71,22 @@ def main():
"one or more -r arguments. Any -r flags override "
"the default rules in %s, unless -R is also used."
% ansiblelint.default_rulesdir)
parser.add_option('-R', action='store_true', default=False,
parser.add_option('-R', action='store_true',
default=False,
dest='use_default_rules',
help="Use default rules in %s in addition to any extra "
"rules directories specified with -r. There is "
"no need to specify this if no -r flags are used"
% ansiblelint.default_rulesdir)
parser.add_option('-t', dest='tags', default=[],
parser.add_option('-t', dest='tags',
action='append',
default=[],
help="only check rules whose id/tags match these values")
parser.add_option('-T', dest='listtags', action='store_true',
help="list all the tags")
parser.add_option('-v', dest='verbosity', action='count',
help="Increase verbosity level", default=0)
help="Increase verbosity level",
default=0)
parser.add_option('-x', dest='skip_list', default=[],
help="only check rules whose id/tags do not " +
"match these values")
......@@ -77,9 +99,38 @@ def main():
help="Try force colored output (relying on ansible's code)")
parser.add_option('--exclude', dest='exclude_paths', action='append',
help='path to directories or files to skip. This option'
' is repeatable.')
' is repeatable.',
default=[])
parser.add_option('-c', help='Specify configuration file to use. Defaults to ".ansible-lint"')
options, args = parser.parse_args(sys.argv[1:])
config = load_config(options.c)
if config:
if 'quiet' in config:
options.quiet = options.quiet or config['quiet']
if 'parseable' in config:
options.parseable = options.parseable or config['parseable']
if 'use_default_rules' in config:
options.use_default_rules = options.use_default_rules or config['use_default_rules']
if 'verbosity' in config:
options.verbosity = options.verbosity + config['verbosity']
if 'exclude_paths' in config:
options.exclude_paths = options.exclude_paths + config['exclude_paths']
if 'rulesdir' in config:
options.rulesdir = options.rulesdir + config['rulesdir']
if 'skip_list' in config:
options.skip_list = options.skip_list + config['skip_list']
if 'tags' in config:
options.tags = options.tags + config['tags']
if options.quiet:
formatter = formatters.QuietFormatter()
......
......@@ -22,8 +22,11 @@ import os
from ansiblelint import AnsibleLintRule
try:
from ansible.utils.boolean import boolean
from ansible.module_utils.parsing.convert_bool import boolean
except ImportError:
try:
from ansible.utils.boolean import boolean
except ImportError:
try:
from ansible.utils import boolean
except ImportError:
......@@ -44,9 +47,14 @@ class CommandsInsteadOfArgumentsRule(AnsibleLintRule):
'rmdir': 'state=absent', 'rm': 'state=absent'}
def matchtask(self, file, task):
if task["action"]["__ansible_module__"] in self._commands and \
task["action"]["__ansible_arguments__"]:
executable = os.path.basename(task["action"]["__ansible_arguments__"][0])
if task["action"]["__ansible_module__"] in self._commands:
if 'cmd' in task['action']:
first_cmd_arg = task['action']['cmd'].split()[0]
else:
first_cmd_arg = task["action"]["__ansible_arguments__"][0]
if not first_cmd_arg:
return
executable = os.path.basename(first_cmd_arg)
if executable in self._arguments and \
boolean(task['action'].get('warn', True)):
message = "{0} used in place of argument {1} to file module"
......
......@@ -22,8 +22,11 @@ import os
from ansiblelint import AnsibleLintRule
try:
from ansible.utils.boolean import boolean
from ansible.module_utils.parsing.convert_bool import boolean
except ImportError:
try:
from ansible.utils.boolean import boolean
except ImportError:
try:
from ansible.utils import boolean
except ImportError:
......@@ -43,12 +46,18 @@ class CommandsInsteadOfModulesRule(AnsibleLintRule):
'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'}
'rsync': 'synchronize', 'supervisorctl': 'supervisorctl', 'systemctl': 'systemd',
'sed': 'template or lineinfile'}
def matchtask(self, file, task):
if task["action"]["__ansible_module__"] in self._commands and \
task["action"]["__ansible_arguments__"]:
executable = os.path.basename(task["action"]["__ansible_arguments__"][0])
if task["action"]["__ansible_module__"] in self._commands:
if 'cmd' in task['action']:
first_cmd_arg = task['action']['cmd'].split()[0]
else:
first_cmd_arg = task["action"]["__ansible_arguments__"][0]
if not first_cmd_arg:
return
executable = os.path.basename(first_cmd_arg)
if executable in self._modules and \
boolean(task['action'].get('warn', True)):
message = "{0} used in place of {1} module"
......
......@@ -29,11 +29,15 @@ class EnvVarsInCommandRule(AnsibleLintRule):
'command through environment argument'
tags = ['bug']
expected_args = ['chdir', 'creates', 'executable', 'removes', 'warn',
'__ansible_module__', '__ansible_arguments__',
expected_args = ['chdir', 'creates', 'executable', 'removes', 'stdin', 'warn',
'cmd', '__ansible_module__', '__ansible_arguments__',
LINE_NUMBER_KEY, FILENAME_KEY]
def matchtask(self, file, task):
if task["action"]["__ansible_module__"] in ['shell', 'command']:
if 'cmd' in task['action']:
first_cmd_arg = task['action']['cmd'].split()[0]
else:
first_cmd_arg = task['action']['__ansible_arguments__'][0]
return any([arg not in self.expected_args for arg in task['action']] +
["=" in task['action']['__ansible_arguments__'][0]])
["=" in first_cmd_arg])
......@@ -26,7 +26,7 @@ import six
class OctalPermissionsRule(AnsibleLintRule):
id = 'ANSIBLE0009'
shortdesc = 'Octal file permissions must contain leading zero'
description = 'Numeric file permissions without leading zero can behave' + \
description = 'Numeric file permissions without leading zero can behave ' + \
'in unexpected ways. See ' + \
'http://docs.ansible.com/ansible/file_module.html'
tags = ['formatting']
......@@ -44,15 +44,16 @@ class OctalPermissionsRule(AnsibleLintRule):
return not self.valid_mode_regex.match(mode)
if isinstance(mode, int):
# sensible file permission modes don't
# have write or execute bit set when read bit is
# not set
# have write bit set when read bit is
# not set and don't have execute bit set
# when user execute bit is not set.
# also, user permissions are more generous than
# group permissions and user and group permissions
# are more generous than world permissions
result = (mode % 8 and mode % 8 < 4 or
(mode >> 3) % 8 and (mode >> 3) % 8 < 4 or
(mode >> 6) % 8 and (mode >> 6) % 8 < 4 or
result = (mode % 8 and mode % 8 < 4 and not (mode % 8 == 1 and (mode >> 6) % 8 == 1) or
(mode >> 3) % 8 and (mode >> 3) % 8 < 4 and not ((mode >> 3) % 8 == 1 and (mode >> 6) % 8 == 1) or
(mode >> 6) % 8 and (mode >> 6) % 8 < 4 and not (mode >> 6) % 8 == 1 or
mode & 8 < (mode << 3) & 8 or
mode & 8 < (mode << 6) & 8 or
(mode << 3) & 8 < (mode << 6) & 8)
......
......@@ -38,5 +38,8 @@ class UseCommandInsteadOfShellRule(AnsibleLintRule):
# Use unjinja so that we don't match on jinja filters
# rather than pipes
if task["action"]["__ansible_module__"] == 'shell':
if 'cmd' in task['action']:
unjinjad_cmd = unjinja(task["action"].get("cmd", []))
else:
unjinjad_cmd = unjinja(' '.join(task["action"].get("__ansible_arguments__", [])))
return not any([ch in unjinjad_cmd for ch in '&|<>;$\n*[]{}?'])
......@@ -26,8 +26,8 @@ from ansiblelint import AnsibleLintRule
class UsingBareVariablesIsDeprecatedRule(AnsibleLintRule):
id = 'ANSIBLE0015'
shortdesc = 'Using bare variables is deprecated'
description = 'Using bare variables is deprecated. Update your' + \
'playbooks so that the environment value uses the full variable' + \
description = 'Using bare variables is deprecated. Update your ' + \
'playbooks so that the environment value uses the full variable ' + \
'syntax ("{{your_variable}}").'
tags = ['formatting', 'deprecated']
......
......@@ -23,7 +23,6 @@ import imp
import os
import six
from ansible import constants
from ansible.errors import AnsibleError
......@@ -50,7 +49,6 @@ except ImportError:
from ansible.parsing.dataloader import DataLoader
from ansible.template import Templar
from ansible.parsing.mod_args import ModuleArgsParser
from ansible.plugins import module_loader
from ansible.errors import AnsibleParserError
ANSIBLE_VERSION = 2
......@@ -68,15 +66,19 @@ except ImportError:
dl.set_basedir(basedir)
templar = Templar(dl, variables=templatevars)
return templar.template(varname, **kwargs)
try:
from ansible.plugins import module_loader
except ImportError:
from ansible.plugins.loader import module_loader
LINE_NUMBER_KEY = '__line__'
FILENAME_KEY = '__file__'
VALID_KEYS = [
'name', 'action', 'when', 'async', 'poll', 'notify',
'first_available_file', 'include', 'tags', 'register', 'ignore_errors',
'delegate_to', 'local_action', 'transport', 'remote_user', 'sudo', 'sudo_user',
'sudo_pass', 'when', 'connection', 'environment', 'args', 'always_run',
'first_available_file', 'include', 'include_tasks', 'import_tasks', '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,
......@@ -190,6 +192,8 @@ def play_children(basedir, item, parent_type, playbook_dir):
'post_tasks': _taskshandlers_children,
'block': _taskshandlers_children,
'include': _include_children,
'include_tasks': _include_children,
'import_tasks': _include_children,
'roles': _roles_children,
'dependencies': _roles_children,
'handlers': _taskshandlers_children,
......@@ -222,6 +226,21 @@ def _taskshandlers_children(basedir, k, v, parent_type):
results = []
for th in v:
if 'include' in th:
append_children(th['include'], basedir, k, parent_type, results)
elif 'include_tasks' in th:
append_children(th['include_tasks'], basedir, k, parent_type, results)
elif 'import_tasks' in th:
append_children(th['import_tasks'], basedir, k, parent_type, results)
elif 'block' in th:
results.extend(_taskshandlers_children(basedir, k, th['block'], parent_type))
if 'rescue' in th:
results.extend(_taskshandlers_children(basedir, k, th['rescue'], parent_type))
if 'always' in th:
results.extend(_taskshandlers_children(basedir, k, th['always'], parent_type))
return results
def append_children(taskhandler, basedir, k, parent_type, results):
# when taskshandlers_children is called for playbooks, the
# actual type of the included tasks is the section containing the
# include, e.g. tasks, pre_tasks, or handlers.
......@@ -230,17 +249,9 @@ def _taskshandlers_children(basedir, k, v, parent_type):
else:
playbook_section = parent_type
results.append({
'path': path_dwim(basedir, th['include']),
'path': path_dwim(basedir, taskhandler),
'type': playbook_section
})
elif 'block' in th:
results.extend(_taskshandlers_children(basedir, k, th['block'], parent_type))
if 'rescue' in th:
results.extend(_taskshandlers_children(basedir, k, th['rescue'], parent_type))
if 'always' in th:
results.extend(_taskshandlers_children(basedir, k, th['always'], parent_type))
return results
def _roles_children(basedir, k, v, parent_type):
......@@ -365,7 +376,7 @@ def normalize_task_v2(task):
result['action'] = dict(__ansible_module__=action)
if '_raw_params' in arguments:
result['action']['__ansible_arguments__'] = arguments['_raw_params'].split()
result['action']['__ansible_arguments__'] = arguments['_raw_params'].split(' ')
del(arguments['_raw_params'])
else:
result['action']['__ansible_arguments__'] = list()
......@@ -439,7 +450,7 @@ def task_to_str(task):
if name:
return name
action = task.get("action")
args = " " .join([u"{0}={1}".format(k, v) for (k, v) in action.items()
args = " ".join([u"{0}={1}".format(k, v) 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)
......@@ -480,7 +491,8 @@ def get_action_tasks(yaml, file):
block_rescue_always = ('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 'include' not in task.keys()]
return [task for task in tasks if
set(['include', 'include_tasks', 'import_tasks']).isdisjoint(task.keys())]
def get_normalized_tasks(yaml, file):
......
__version__ = '3.4.13'
__version__ = '3.4.20'
......@@ -22,7 +22,7 @@ setup(
install_requires=['ansible', 'pyyaml', 'six'],
entry_points={
'console_scripts': [
'ansible-lint = ansiblelint.main:main'
'ansible-lint = ansiblelint.__main__:main'
]
},
license='MIT',
......
import unittest
from subprocess import Popen, PIPE
import os
import shutil
import yaml
class TestCommandLineInvocationSameAsConfig(unittest.TestCase):
def setUp(self):
if os.path.exists(".sandbox"):
shutil.rmtree(".sandbox")
os.makedirs(".sandbox/subdir")
def run_ansible_lint(self, args=False, config=None):
command = "cd .sandbox; ../bin/ansible-lint ../test/skiptasks.yml"
if args:
command += " " + args
if config:
with open(".sandbox/.ansible-lint", "w") as outfile:
yaml.dump(config, outfile, default_flow_style=False)
result, err = Popen(
[command],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
shell=True
).communicate()
self.assertFalse(err, "Expected no error but was " + str(err))
return result
def assert_config_for(self, cli_arg, config):
with_arg = self.run_ansible_lint(args=cli_arg)
with_config = self.run_ansible_lint(config=config)
self.assertEqual(with_arg, with_config)
def test_parseable_as_config(self):
self.assert_config_for("-p", dict(parseable=True))
def test_quiet_as_config(self):
self.assert_config_for("-q", dict(quiet=True))
def test_rulesdir_as_config(self):
self.assert_config_for("-r ../test/rules/", dict(rulesdir=["../test/rules/"]))
def test_use_default_rules(self):
self.assert_config_for("-R -r ../test/rules/", dict(rulesdir=["../test/rules"],
use_default_rules=True))
def test_tags(self):
self.assert_config_for("-t skip_ansible_lint", dict(tags=["skip_ansible_lint"]))
def test_verbosity(self):
self.assert_config_for("-v", dict(verbosity=1))
def test_skip_list(self):
self.assert_config_for("-x bad_tag", dict(skip_list=["bad_tag"]))
def test_exclude(self):
self.assert_config_for("--exclude ../test/", dict(exclude_paths=["../test/"]))
def test_config_can_be_overridden(self):
no_override = self.run_ansible_lint(args="-t bad_tag")
overridden = self.run_ansible_lint(args="-t bad_tag", config=dict(tags=["skip_ansible_lint"]))
self.assertEqual(no_override, overridden)
def test_different_config_file(self):
with open(".sandbox/subdir/ansible-config.yml", "w") as outfile:
yaml.dump(dict(verbosity=1), outfile, default_flow_style=False)
diff_config = self.run_ansible_lint(args="-c ./subdir/ansible-config.yml")
no_config = self.run_ansible_lint(args="-v")
self.assertEqual(diff_config, no_config)
import unittest
import ansible
from ansiblelint import Runner, RulesCollection
from ansiblelint.rules.EnvVarsInCommandRule import EnvVarsInCommandRule
from pkg_resources import parse_version
class TestEnvVarsInCommand(unittest.TestCase):
......@@ -14,6 +17,12 @@ class TestEnvVarsInCommand(unittest.TestCase):
good_runner = Runner(self.collection, success, [], [], [])
self.assertEqual([], good_runner.run())
@unittest.skipIf(parse_version(ansible.__version__) < parse_version('2.4'), "not supported with ansible < 2.4")
def test_file_positive_2_4(self):
success = 'test/env-vars-in-command-success_2_4_style.yml'
good_runner = Runner(self.collection, success, [], [], [])
self.assertEqual([], good_runner.run())
def test_file_negative(self):
failure = 'test/env-vars-in-command-failure.yml'
bad_runner = Runner(self.collection, failure, [], [], [])
......
......@@ -18,4 +18,4 @@ class TestOctalPermissionsRuleWithFile(unittest.TestCase):
failure = 'test/octalpermissions-failure.yml'
bad_runner = Runner(self.collection, failure, [], [], [])
errs = bad_runner.run()
self.assertEqual(4, len(errs))
self.assertEqual(5, len(errs))
import os
import unittest
import ansible
from ansiblelint import Runner, RulesCollection
from pkg_resources import parse_version
class TestTaskIncludes(unittest.TestCase):
def setUp(self):
rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
self.rules = RulesCollection.create_from_directory(rulesdir)
......@@ -27,6 +30,20 @@ class TestTaskIncludes(unittest.TestCase):
runner.run()
self.assertEqual(len(runner.playbooks), 4)
@unittest.skipIf(parse_version(ansible.__version__) < parse_version('2.4'), "not supported with ansible < 2.4")
def test_include_tasks_2_4_style(self):
filename = 'test/taskincludes_2_4_style.yml'
runner = Runner(self.rules, filename, [], [], [])
runner.run()
self.assertEqual(len(runner.playbooks), 4)
@unittest.skipIf(parse_version(ansible.__version__) < parse_version('2.4'), "not supported with ansible < 2.4")
def test_import_tasks_2_4_style(self):
filename = 'test/taskimports.yml'
runner = Runner(self.rules, filename, [], [], [])
runner.run()
self.assertEqual(len(runner.playbooks), 4)
def test_include_tasks_with_block_include(self):
filename = 'test/include-in-block.yml'
runner = Runner(self.rules, filename, [], [], [])
......
......@@ -19,6 +19,13 @@
- name: command with inline removes
command: removes=Z echo blah
- name: command with cmd
command:
cmd:
echo blah
args:
creates: Z
- name: shell with creates check
shell: echo blah
args:
......@@ -39,6 +46,13 @@
- name: shell with inline removes
shell: removes=Z echo blah
- name: shell with cmd
shell:
cmd:
echo blah
args:
creates: Z
- handlers:
- name: restart something
command: do something
......
......@@ -25,3 +25,9 @@
- name: use shell generator
shell: ls foo{.txt,.xml}
- name: use shell with cmd
shell:
cmd: |
set -x
ls foo?
......@@ -14,3 +14,8 @@
- name: commands can have equals in them
command: echo "==========="
- name: commands with cmd
command:
cmd:
echo "-------"
- hosts: localhost
tasks:
- name: command with stdin (ansible > 2.4)
command: /bin/cat
args:
stdin: "Hello, world!"
\ No newline at end of file
......@@ -8,6 +8,11 @@
path: foo
mode: 600
- name: octal permissions test fail (710)
file:
path: foo
mode: 710
- name: octal permissions test fail (123)
file:
path: foo
......
......@@ -21,5 +21,11 @@
- name: octal permissions test success (0777)
file: path=baz mode=0777
- name: octal permissions test success (0711)
file: path=baz mode=0711
- name: octal permissions test success (0710)
file: path=baz mode=0710
- name: permissions test success (0777)
file: path=baz mode=u+rwx
---
- hosts: webservers
vars:
varset: varset
tasks:
- import_tasks: nestedincludes.yml tags=nested
- import_tasks: "{{ varnotset }}.yml"
- import_tasks: "{{ varset }}.yml"
- import_tasks: "directory with spaces/main.yml"
---
- hosts: webservers
vars:
varset: varset
tasks:
- include_tasks: nestedincludes.yml tags=nested
- include_tasks: "{{ varnotset }}.yml"
- include_tasks: "{{ varset }}.yml"
- include_tasks: "directory with spaces/main.yml"
[tox]
minversion = 1.6
envlist = py27-ansible{19,20,21,22,devel},py34-ansible{22,devel},flake8
envlist = py27-ansible{19,20,21,22,23,24,devel},py36-ansible{22,23,24,devel},py27-flake8,py36-flake8
[testenv]
deps =
......@@ -8,10 +8,13 @@ deps =
ansible20: ansible>=2.0.0.2,<2.1
ansible21: ansible>=2.1,<2.2
ansible22: ansible>=2.2,<2.3
ansible23: ansible>=2.3,<2.4
ansible24: ansible>=2.4,<2.5