summaryrefslogtreecommitdiffstats
path: root/lib/ansiblelint/rules
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansiblelint/rules')
-rw-r--r--lib/ansiblelint/rules/AlwaysRunRule.py33
-rw-r--r--lib/ansiblelint/rules/BecomeUserWithoutBecomeRule.py80
-rw-r--r--lib/ansiblelint/rules/CommandHasChangesCheckRule.py45
-rw-r--r--lib/ansiblelint/rules/CommandsInsteadOfArgumentsRule.py65
-rw-r--r--lib/ansiblelint/rules/CommandsInsteadOfModulesRule.py86
-rw-r--r--lib/ansiblelint/rules/ComparisonToEmptyStringRule.py23
-rw-r--r--lib/ansiblelint/rules/ComparisonToLiteralBoolRule.py23
-rw-r--r--lib/ansiblelint/rules/DeprecatedModuleRule.py37
-rw-r--r--lib/ansiblelint/rules/EnvVarsInCommandRule.py48
-rw-r--r--lib/ansiblelint/rules/GitHasVersionRule.py37
-rw-r--r--lib/ansiblelint/rules/IncludeMissingFileRule.py67
-rw-r--r--lib/ansiblelint/rules/LineTooLongRule.py19
-rw-r--r--lib/ansiblelint/rules/LoadingFailureRule.py14
-rw-r--r--lib/ansiblelint/rules/MercurialHasRevisionRule.py37
-rw-r--r--lib/ansiblelint/rules/MetaChangeFromDefaultRule.py40
-rw-r--r--lib/ansiblelint/rules/MetaMainHasInfoRule.py66
-rw-r--r--lib/ansiblelint/rules/MetaTagValidRule.py81
-rw-r--r--lib/ansiblelint/rules/MetaVideoLinksRule.py65
-rw-r--r--lib/ansiblelint/rules/MissingFilePermissionsRule.py95
-rw-r--r--lib/ansiblelint/rules/NestedJinjaRule.py53
-rw-r--r--lib/ansiblelint/rules/NoFormattingInWhenRule.py34
-rw-r--r--lib/ansiblelint/rules/NoTabsRule.py16
-rw-r--r--lib/ansiblelint/rules/OctalPermissionsRule.py73
-rw-r--r--lib/ansiblelint/rules/PackageIsNotLatestRule.py67
-rw-r--r--lib/ansiblelint/rules/PlaybookExtension.py28
-rw-r--r--lib/ansiblelint/rules/RoleNames.py74
-rw-r--r--lib/ansiblelint/rules/RoleRelativePath.py32
-rw-r--r--lib/ansiblelint/rules/ShellWithoutPipefail.py38
-rw-r--r--lib/ansiblelint/rules/SudoRule.py36
-rw-r--r--lib/ansiblelint/rules/TaskHasNameRule.py40
-rw-r--r--lib/ansiblelint/rules/TaskNoLocalAction.py18
-rw-r--r--lib/ansiblelint/rules/TrailingWhitespaceRule.py34
-rw-r--r--lib/ansiblelint/rules/UseCommandInsteadOfShellRule.py45
-rw-r--r--lib/ansiblelint/rules/UseHandlerRatherThanWhenChangedRule.py52
-rw-r--r--lib/ansiblelint/rules/UsingBareVariablesIsDeprecatedRule.py75
-rw-r--r--lib/ansiblelint/rules/VariableHasSpacesRule.py24
-rw-r--r--lib/ansiblelint/rules/__init__.py254
-rw-r--r--lib/ansiblelint/rules/custom/__init__.py1
38 files changed, 1955 insertions, 0 deletions
diff --git a/lib/ansiblelint/rules/AlwaysRunRule.py b/lib/ansiblelint/rules/AlwaysRunRule.py
new file mode 100644
index 0000000..8d811ff
--- /dev/null
+++ b/lib/ansiblelint/rules/AlwaysRunRule.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2017 Anth Courtney <anthcourtney@gmail.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class AlwaysRunRule(AnsibleLintRule):
+ id = '101'
+ shortdesc = 'Deprecated always_run'
+ description = 'Instead of ``always_run``, use ``check_mode``'
+ severity = 'MEDIUM'
+ tags = ['deprecated', 'ANSIBLE0018']
+ version_added = 'historic'
+
+ def matchtask(self, file, task):
+ return 'always_run' in task
diff --git a/lib/ansiblelint/rules/BecomeUserWithoutBecomeRule.py b/lib/ansiblelint/rules/BecomeUserWithoutBecomeRule.py
new file mode 100644
index 0000000..e6f3259
--- /dev/null
+++ b/lib/ansiblelint/rules/BecomeUserWithoutBecomeRule.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from functools import reduce
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+def _get_subtasks(data):
+ result = []
+ block_names = [
+ 'tasks',
+ 'pre_tasks',
+ 'post_tasks',
+ 'handlers',
+ 'block',
+ 'always',
+ 'rescue']
+ for name in block_names:
+ if data and name in data:
+ result += (data[name] or [])
+ return result
+
+
+def _nested_search(term, data):
+ if data and term in data:
+ return True
+ return reduce((lambda x, y: x or _nested_search(term, y)), _get_subtasks(data), False)
+
+
+def _become_user_without_become(becomeuserabove, data):
+ if 'become' in data:
+ # If become is in lineage of tree then correct
+ return False
+ if ('become_user' in data and _nested_search('become', data)):
+ # If 'become_user' on tree and become somewhere below
+ # we must check for a case of a second 'become_user' without a
+ # 'become' in its lineage
+ subtasks = _get_subtasks(data)
+ return reduce((lambda x, y: x or _become_user_without_become(False, y)), subtasks, False)
+ if _nested_search('become_user', data):
+ # Keep searching down if 'become_user' exists in the tree below current task
+ subtasks = _get_subtasks(data)
+ return (len(subtasks) == 0 or
+ reduce((lambda x, y: x or
+ _become_user_without_become(
+ becomeuserabove or 'become_user' in data, y)), subtasks, False))
+ # If at bottom of tree, flag up if 'become_user' existed in the lineage of the tree and
+ # 'become' was not. This is an error if any lineage has a 'become_user' but no become
+ return becomeuserabove
+
+
+class BecomeUserWithoutBecomeRule(AnsibleLintRule):
+ id = '501'
+ shortdesc = 'become_user requires become to work as expected'
+ description = '``become_user`` without ``become`` will not actually change user'
+ severity = 'VERY_HIGH'
+ tags = ['task', 'oddity', 'ANSIBLE0017']
+ version_added = 'historic'
+
+ def matchplay(self, file, data):
+ if file['type'] == 'playbook' and _become_user_without_become(False, data):
+ return ({'become_user': data}, self.shortdesc)
diff --git a/lib/ansiblelint/rules/CommandHasChangesCheckRule.py b/lib/ansiblelint/rules/CommandHasChangesCheckRule.py
new file mode 100644
index 0000000..26087b8
--- /dev/null
+++ b/lib/ansiblelint/rules/CommandHasChangesCheckRule.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class CommandHasChangesCheckRule(AnsibleLintRule):
+ id = '301'
+ shortdesc = 'Commands should not change things if nothing needs doing'
+ description = (
+ 'Commands should either read information (and thus set '
+ '``changed_when``) or not do something if it has already been '
+ 'done (using creates/removes) or only do it if another '
+ 'check has a particular result (``when``)'
+ )
+ severity = 'HIGH'
+ tags = ['command-shell', 'idempotency', 'ANSIBLE0012']
+ version_added = 'historic'
+
+ _commands = ['command', 'shell', 'raw']
+
+ def matchtask(self, file, task):
+ if task["__ansible_action_type__"] == 'task':
+ if task["action"]["__ansible_module__"] in self._commands:
+ return 'changed_when' not in task and \
+ 'when' not in task and \
+ 'creates' not in task['action'] and \
+ 'removes' not in task['action']
diff --git a/lib/ansiblelint/rules/CommandsInsteadOfArgumentsRule.py b/lib/ansiblelint/rules/CommandsInsteadOfArgumentsRule.py
new file mode 100644
index 0000000..f1adffa
--- /dev/null
+++ b/lib/ansiblelint/rules/CommandsInsteadOfArgumentsRule.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import os
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.utils import get_first_cmd_arg
+
+try:
+ 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:
+ from ansible import constants
+ boolean = constants.mk_boolean
+
+
+class CommandsInsteadOfArgumentsRule(AnsibleLintRule):
+ id = '302'
+ shortdesc = 'Using command rather than an argument to e.g. file'
+ description = (
+ 'Executing a command when there are arguments to modules '
+ 'is generally a bad idea'
+ )
+ severity = 'VERY_HIGH'
+ tags = ['command-shell', 'resources', 'ANSIBLE0007']
+ version_added = 'historic'
+
+ _commands = ['command', 'shell', 'raw']
+ _arguments = {'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group',
+ 'ln': 'state=link', 'mkdir': 'state=directory',
+ 'rmdir': 'state=absent', 'rm': 'state=absent'}
+
+ def matchtask(self, file, task):
+ if task["action"]["__ansible_module__"] in self._commands:
+ first_cmd_arg = get_first_cmd_arg(task)
+ 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"
+ return message.format(executable, self._arguments[executable])
diff --git a/lib/ansiblelint/rules/CommandsInsteadOfModulesRule.py b/lib/ansiblelint/rules/CommandsInsteadOfModulesRule.py
new file mode 100644
index 0000000..b19c5c2
--- /dev/null
+++ b/lib/ansiblelint/rules/CommandsInsteadOfModulesRule.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import os
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.utils import get_first_cmd_arg
+
+try:
+ 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:
+ from ansible import constants
+ boolean = constants.mk_boolean
+
+
+class CommandsInsteadOfModulesRule(AnsibleLintRule):
+ id = '303'
+ shortdesc = 'Using command rather than module'
+ description = (
+ 'Executing a command when there is an Ansible module '
+ 'is generally a bad idea'
+ )
+ severity = 'HIGH'
+ tags = ['command-shell', 'resources', 'ANSIBLE0006']
+ version_added = 'historic'
+
+ _commands = ['command', 'shell']
+ _modules = {
+ 'apt-get': 'apt-get',
+ 'chkconfig': 'service',
+ 'curl': 'get_url or uri',
+ 'git': 'git',
+ 'hg': 'hg',
+ 'letsencrypt': 'acme_certificate',
+ 'mktemp': 'tempfile',
+ 'mount': 'mount',
+ 'patch': 'patch',
+ 'rpm': 'yum or rpm_key',
+ 'rsync': 'synchronize',
+ 'sed': 'template, replace or lineinfile',
+ 'service': 'service',
+ 'supervisorctl': 'supervisorctl',
+ 'svn': 'subversion',
+ 'systemctl': 'systemd',
+ 'tar': 'unarchive',
+ 'unzip': 'unarchive',
+ 'wget': 'get_url or uri',
+ 'yum': 'yum',
+ }
+
+ def matchtask(self, file, task):
+ if task['action']['__ansible_module__'] not in self._commands:
+ return
+
+ first_cmd_arg = get_first_cmd_arg(task)
+ 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'
+ return message.format(executable, self._modules[executable])
diff --git a/lib/ansiblelint/rules/ComparisonToEmptyStringRule.py b/lib/ansiblelint/rules/ComparisonToEmptyStringRule.py
new file mode 100644
index 0000000..a43c4f7
--- /dev/null
+++ b/lib/ansiblelint/rules/ComparisonToEmptyStringRule.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class ComparisonToEmptyStringRule(AnsibleLintRule):
+ id = '602'
+ shortdesc = "Don't compare to empty string"
+ description = (
+ 'Use ``when: var|length > 0`` rather than ``when: var != ""`` (or '
+ 'conversely ``when: var|length == 0`` rather than ``when: var == ""``)'
+ )
+ severity = 'HIGH'
+ tags = ['idiom']
+ version_added = 'v4.0.0'
+
+ empty_string_compare = re.compile("[=!]= ?(\"{2}|'{2})")
+
+ def match(self, file, line):
+ return self.empty_string_compare.search(line)
diff --git a/lib/ansiblelint/rules/ComparisonToLiteralBoolRule.py b/lib/ansiblelint/rules/ComparisonToLiteralBoolRule.py
new file mode 100644
index 0000000..46668d1
--- /dev/null
+++ b/lib/ansiblelint/rules/ComparisonToLiteralBoolRule.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class ComparisonToLiteralBoolRule(AnsibleLintRule):
+ id = '601'
+ shortdesc = "Don't compare to literal True/False"
+ description = (
+ 'Use ``when: var`` rather than ``when: var == True`` '
+ '(or conversely ``when: not var``)'
+ )
+ severity = 'HIGH'
+ tags = ['idiom']
+ version_added = 'v4.0.0'
+
+ literal_bool_compare = re.compile("[=!]= ?(True|true|False|false)")
+
+ def match(self, file, line):
+ return self.literal_bool_compare.search(line)
diff --git a/lib/ansiblelint/rules/DeprecatedModuleRule.py b/lib/ansiblelint/rules/DeprecatedModuleRule.py
new file mode 100644
index 0000000..dc019ed
--- /dev/null
+++ b/lib/ansiblelint/rules/DeprecatedModuleRule.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class DeprecatedModuleRule(AnsibleLintRule):
+ id = '105'
+ shortdesc = 'Deprecated module'
+ description = (
+ 'These are deprecated modules, some modules are kept '
+ 'temporarily for backwards compatibility but usage is discouraged. '
+ 'For more details see: '
+ 'https://docs.ansible.com/ansible/latest/modules/list_of_all_modules.html'
+ )
+ severity = 'HIGH'
+ tags = ['deprecated']
+ version_added = 'v4.0.0'
+
+ _modules = [
+ 'accelerate', 'aos_asn_pool', 'aos_blueprint', 'aos_blueprint_param',
+ 'aos_blueprint_virtnet', 'aos_device', 'aos_external_router',
+ 'aos_ip_pool', 'aos_logical_device', 'aos_logical_device_map',
+ 'aos_login', 'aos_rack_type', 'aos_template', 'azure', 'cl_bond',
+ 'cl_bridge', 'cl_img_install', 'cl_interface', 'cl_interface_policy',
+ 'cl_license', 'cl_ports', 'cs_nic', 'docker', 'ec2_ami_find',
+ 'ec2_ami_search', 'ec2_remote_facts', 'ec2_vpc', 'kubernetes',
+ 'netscaler', 'nxos_ip_interface', 'nxos_mtu', 'nxos_portchannel',
+ 'nxos_switchport', 'oc', 'panos_nat_policy', 'panos_security_policy',
+ 'vsphere_guest', 'win_msi', 'include'
+ ]
+
+ def matchtask(self, file, task):
+ module = task["action"]["__ansible_module__"]
+ if module in self._modules:
+ message = '{0} {1}'
+ return message.format(self.shortdesc, module)
+ return False
diff --git a/lib/ansiblelint/rules/EnvVarsInCommandRule.py b/lib/ansiblelint/rules/EnvVarsInCommandRule.py
new file mode 100644
index 0000000..58dba90
--- /dev/null
+++ b/lib/ansiblelint/rules/EnvVarsInCommandRule.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.utils import FILENAME_KEY, LINE_NUMBER_KEY, get_first_cmd_arg
+
+
+class EnvVarsInCommandRule(AnsibleLintRule):
+ id = '304'
+ shortdesc = "Environment variables don't work as part of command"
+ description = (
+ 'Environment variables should be passed to ``shell`` or ``command`` '
+ 'through environment argument'
+ )
+ severity = 'VERY_HIGH'
+ tags = ['command-shell', 'bug', 'ANSIBLE0014']
+ version_added = 'historic'
+
+ expected_args = ['chdir', 'creates', 'executable', 'removes', 'stdin', 'warn',
+ 'stdin_add_newline', 'strip_empty_ends',
+ 'cmd', '__ansible_module__', '__ansible_arguments__',
+ LINE_NUMBER_KEY, FILENAME_KEY]
+
+ def matchtask(self, file, task):
+ if task["action"]["__ansible_module__"] in ['command']:
+ first_cmd_arg = get_first_cmd_arg(task)
+ if not first_cmd_arg:
+ return
+
+ return any([arg not in self.expected_args for arg in task['action']] +
+ ["=" in first_cmd_arg])
diff --git a/lib/ansiblelint/rules/GitHasVersionRule.py b/lib/ansiblelint/rules/GitHasVersionRule.py
new file mode 100644
index 0000000..f0f3680
--- /dev/null
+++ b/lib/ansiblelint/rules/GitHasVersionRule.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class GitHasVersionRule(AnsibleLintRule):
+ id = '401'
+ shortdesc = 'Git checkouts must contain explicit version'
+ description = (
+ 'All version control checkouts must point to '
+ 'an explicit commit or tag, not just ``latest``'
+ )
+ severity = 'MEDIUM'
+ tags = ['module', 'repeatability', 'ANSIBLE0004']
+ version_added = 'historic'
+
+ def matchtask(self, file, task):
+ return (task['action']['__ansible_module__'] == 'git' and
+ task['action'].get('version', 'HEAD') == 'HEAD')
diff --git a/lib/ansiblelint/rules/IncludeMissingFileRule.py b/lib/ansiblelint/rules/IncludeMissingFileRule.py
new file mode 100644
index 0000000..57508fa
--- /dev/null
+++ b/lib/ansiblelint/rules/IncludeMissingFileRule.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2020, Joachim Lusiardi
+# Copyright (c) 2020, Ansible Project
+
+import os.path
+
+import ansible.parsing.yaml.objects
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class IncludeMissingFileRule(AnsibleLintRule):
+ id = '505'
+ shortdesc = 'referenced files must exist'
+ description = (
+ 'All files referenced by by include / import tasks '
+ 'must exist. The check excludes files with jinja2 '
+ 'templates in the filename.'
+ )
+ severity = 'MEDIUM'
+ tags = ['task', 'bug']
+ version_added = 'v4.3.0'
+
+ def matchplay(self, file, data):
+ absolute_directory = file.get('absolute_directory', None)
+ results = []
+
+ # avoid failing with a playbook having tasks: null
+ for task in (data.get('tasks', []) or []):
+
+ # ignore None tasks or
+ # if the id of the current rule is not in list of skipped rules for this play
+ if not task or self.id in task.get('skipped_rules', ()):
+ continue
+
+ # collect information which file was referenced for include / import
+ referenced_file = None
+ for key, val in task.items():
+ if not (key.startswith('include_') or
+ key.startswith('import_') or
+ key == 'include'):
+ continue
+ if isinstance(val, ansible.parsing.yaml.objects.AnsibleMapping):
+ referenced_file = val.get('file', None)
+ else:
+ referenced_file = val
+ # take the file and skip the remaining keys
+ if referenced_file:
+ break
+
+ if referenced_file is None or absolute_directory is None:
+ continue
+
+ # make sure we have a absolute path here and check if it is a file
+ referenced_file = os.path.join(absolute_directory, referenced_file)
+
+ # skip if this is a jinja2 templated reference
+ if '{{' in referenced_file:
+ continue
+
+ # existing files do not produce any error
+ if os.path.isfile(referenced_file):
+ continue
+
+ results.append(({'referenced_file': referenced_file},
+ 'referenced missing file in %s:%i'
+ % (task['__file__'], task['__line__'])))
+ return results
diff --git a/lib/ansiblelint/rules/LineTooLongRule.py b/lib/ansiblelint/rules/LineTooLongRule.py
new file mode 100644
index 0000000..007857e
--- /dev/null
+++ b/lib/ansiblelint/rules/LineTooLongRule.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class LineTooLongRule(AnsibleLintRule):
+ id = '204'
+ shortdesc = 'Lines should be no longer than 160 chars'
+ description = (
+ 'Long lines make code harder to read and '
+ 'code review more difficult'
+ )
+ severity = 'VERY_LOW'
+ tags = ['formatting']
+ version_added = 'v4.0.0'
+
+ def match(self, file, line):
+ return len(line) > 160
diff --git a/lib/ansiblelint/rules/LoadingFailureRule.py b/lib/ansiblelint/rules/LoadingFailureRule.py
new file mode 100644
index 0000000..7c37498
--- /dev/null
+++ b/lib/ansiblelint/rules/LoadingFailureRule.py
@@ -0,0 +1,14 @@
+"""Rule definition for a failure to load a file."""
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class LoadingFailureRule(AnsibleLintRule):
+ """File loading failure."""
+
+ id = '901'
+ shortdesc = 'Failed to load or parse file'
+ description = 'Linter failed to process a YAML file, possible not an Ansible file.'
+ severity = 'VERY_HIGH'
+ tags = ['core']
+ version_added = 'v4.3.0'
diff --git a/lib/ansiblelint/rules/MercurialHasRevisionRule.py b/lib/ansiblelint/rules/MercurialHasRevisionRule.py
new file mode 100644
index 0000000..fcfe0a8
--- /dev/null
+++ b/lib/ansiblelint/rules/MercurialHasRevisionRule.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class MercurialHasRevisionRule(AnsibleLintRule):
+ id = '402'
+ shortdesc = 'Mercurial checkouts must contain explicit revision'
+ description = (
+ 'All version control checkouts must point to '
+ 'an explicit commit or tag, not just ``latest``'
+ )
+ severity = 'MEDIUM'
+ tags = ['module', 'repeatability', 'ANSIBLE0005']
+ version_added = 'historic'
+
+ def matchtask(self, file, task):
+ return (task['action']['__ansible_module__'] == 'hg' and
+ task['action'].get('revision', 'default') == 'default')
diff --git a/lib/ansiblelint/rules/MetaChangeFromDefaultRule.py b/lib/ansiblelint/rules/MetaChangeFromDefaultRule.py
new file mode 100644
index 0000000..db52db3
--- /dev/null
+++ b/lib/ansiblelint/rules/MetaChangeFromDefaultRule.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class MetaChangeFromDefaultRule(AnsibleLintRule):
+ id = '703'
+ shortdesc = 'meta/main.yml default values should be changed'
+ field_defaults = [
+ ('author', 'your name'),
+ ('description', 'your description'),
+ ('company', 'your company (optional)'),
+ ('license', 'license (GPLv2, CC-BY, etc)'),
+ ('license', 'license (GPL-2.0-or-later, MIT, etc)'),
+ ]
+ description = (
+ 'meta/main.yml default values should be changed for: ``{}``'.format(
+ ', '.join(f[0] for f in field_defaults)
+ )
+ )
+ severity = 'HIGH'
+ tags = ['metadata']
+ version_added = 'v4.0.0'
+
+ def matchplay(self, file, data):
+ if file['type'] != 'meta':
+ return False
+
+ galaxy_info = data.get('galaxy_info', None)
+ if not galaxy_info:
+ return False
+
+ results = []
+ for field, default in self.field_defaults:
+ value = galaxy_info.get(field, None)
+ if value and value == default:
+ results.append(({'meta/main.yml': data},
+ 'Should change default metadata: %s' % field))
+
+ return results
diff --git a/lib/ansiblelint/rules/MetaMainHasInfoRule.py b/lib/ansiblelint/rules/MetaMainHasInfoRule.py
new file mode 100644
index 0000000..f05f240
--- /dev/null
+++ b/lib/ansiblelint/rules/MetaMainHasInfoRule.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+META_STR_INFO = (
+ 'author',
+ 'description'
+)
+META_INFO = tuple(list(META_STR_INFO) + [
+ 'license',
+ 'min_ansible_version',
+ 'platforms',
+])
+
+
+def _platform_info_errors_itr(platforms):
+ if not isinstance(platforms, list):
+ yield 'Platforms should be a list of dictionaries'
+ return
+
+ for platform in platforms:
+ if not isinstance(platform, dict):
+ yield 'Platforms should be a list of dictionaries'
+ elif 'name' not in platform:
+ yield 'Platform should contain name'
+
+
+def _galaxy_info_errors_itr(galaxy_info,
+ info_list=META_INFO,
+ str_info_list=META_STR_INFO):
+ for info in info_list:
+ ginfo = galaxy_info.get(info, False)
+ if ginfo:
+ if info in str_info_list and not isinstance(ginfo, str):
+ yield '{info} should be a string'.format(info=info)
+ elif info == 'platforms':
+ for err in _platform_info_errors_itr(ginfo):
+ yield err
+ else:
+ yield 'Role info should contain {info}'.format(info=info)
+
+
+class MetaMainHasInfoRule(AnsibleLintRule):
+ id = '701'
+ shortdesc = 'meta/main.yml should contain relevant info'
+ str_info = META_STR_INFO
+ info = META_INFO
+ description = (
+ 'meta/main.yml should contain: ``{}``'.format(', '.join(info))
+ )
+ severity = 'HIGH'
+ tags = ['metadata']
+ version_added = 'v4.0.0'
+
+ def matchplay(self, file, data):
+ if file['type'] != 'meta':
+ return False
+
+ meta = {'meta/main.yml': data}
+ galaxy_info = data.get('galaxy_info', False)
+ if galaxy_info:
+ return [(meta, err) for err
+ in _galaxy_info_errors_itr(galaxy_info)]
+
+ return [(meta, "No 'galaxy_info' found")]
diff --git a/lib/ansiblelint/rules/MetaTagValidRule.py b/lib/ansiblelint/rules/MetaTagValidRule.py
new file mode 100644
index 0000000..0739ca3
--- /dev/null
+++ b/lib/ansiblelint/rules/MetaTagValidRule.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2018, Ansible Project
+
+import re
+import sys
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class MetaTagValidRule(AnsibleLintRule):
+ id = '702'
+ shortdesc = 'Tags must contain lowercase letters and digits only'
+ description = (
+ 'Tags must contain lowercase letters and digits only, '
+ 'and ``galaxy_tags`` is expected to be a list'
+ )
+ severity = 'HIGH'
+ tags = ['metadata']
+ version_added = 'v4.0.0'
+
+ TAG_REGEXP = re.compile('^[a-z0-9]+$')
+
+ def matchplay(self, file, data):
+ if file['type'] != 'meta':
+ return False
+
+ galaxy_info = data.get('galaxy_info', None)
+ if not galaxy_info:
+ return False
+
+ tags = []
+ results = []
+
+ if 'galaxy_tags' in galaxy_info:
+ if isinstance(galaxy_info['galaxy_tags'], list):
+ tags += galaxy_info['galaxy_tags']
+ else:
+ results.append(({'meta/main.yml': data},
+ "Expected 'galaxy_tags' to be a list"))
+
+ if 'categories' in galaxy_info:
+ results.append(({'meta/main.yml': data},
+ "Use 'galaxy_tags' rather than 'categories'"))
+ if isinstance(galaxy_info['categories'], list):
+ tags += galaxy_info['categories']
+ else:
+ results.append(({'meta/main.yml': data},
+ "Expected 'categories' to be a list"))
+
+ for tag in tags:
+ msg = self.shortdesc
+ if not isinstance(tag, str):
+ results.append((
+ {'meta/main.yml': data},
+ "Tags must be strings: '{}'".format(tag)))
+ continue
+ if not re.match(self.TAG_REGEXP, tag):
+ results.append(({'meta/main.yml': data},
+ "{}, invalid: '{}'".format(msg, tag)))
+
+ return results
+
+
+META_TAG_VALID = '''
+galaxy_info:
+ galaxy_tags: ['database', 'my s q l', 'MYTAG']
+ categories: 'my_category_not_in_a_list'
+'''
+
+# testing code to be loaded only with pytest or when executed the rule file
+if "pytest" in sys.modules:
+
+ import pytest
+
+ @pytest.mark.parametrize('rule_runner', (MetaTagValidRule, ), indirect=['rule_runner'])
+ def test_valid_tag_rule(rule_runner):
+ """Test rule matches."""
+ results = rule_runner.run_role_meta_main(META_TAG_VALID)
+ assert "Use 'galaxy_tags' rather than 'categories'" in str(results)
+ assert "Expected 'categories' to be a list" in str(results)
+ assert "invalid: 'my s q l'" in str(results)
+ assert "invalid: 'MYTAG'" in str(results)
diff --git a/lib/ansiblelint/rules/MetaVideoLinksRule.py b/lib/ansiblelint/rules/MetaVideoLinksRule.py
new file mode 100644
index 0000000..aa34012
--- /dev/null
+++ b/lib/ansiblelint/rules/MetaVideoLinksRule.py
@@ -0,0 +1,65 @@
+# Copyright (c) 2018, Ansible Project
+
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class MetaVideoLinksRule(AnsibleLintRule):
+ id = '704'
+ shortdesc = "meta/main.yml video_links should be formatted correctly"
+ description = (
+ 'Items in ``video_links`` in meta/main.yml should be '
+ 'dictionaries, and contain only keys ``url`` and ``title``, '
+ 'and have a shared link from a supported provider'
+ )
+ severity = 'LOW'
+ tags = ['metadata']
+ version_added = 'v4.0.0'
+
+ VIDEO_REGEXP = {
+ 'google': re.compile(
+ r'https://drive\.google\.com.*file/d/([0-9A-Za-z-_]+)/.*'),
+ 'vimeo': re.compile(
+ r'https://vimeo\.com/([0-9]+)'),
+ 'youtube': re.compile(
+ r'https://youtu\.be/([0-9A-Za-z-_]+)'),
+ }
+
+ def matchplay(self, file, data):
+ if file['type'] != 'meta':
+ return False
+
+ galaxy_info = data.get('galaxy_info', None)
+ if not galaxy_info:
+ return False
+
+ video_links = galaxy_info.get('video_links', None)
+ if not video_links:
+ return False
+
+ results = []
+
+ for video in video_links:
+ if not isinstance(video, dict):
+ results.append(({'meta/main.yml': data},
+ "Expected item in 'video_links' to be "
+ "a dictionary"))
+ continue
+
+ if set(video) != {'url', 'title', '__file__', '__line__'}:
+ results.append(({'meta/main.yml': data},
+ "Expected item in 'video_links' to contain "
+ "only keys 'url' and 'title'"))
+ continue
+
+ for name, expr in self.VIDEO_REGEXP.items():
+ if expr.match(video['url']):
+ break
+ else:
+ msg = ("URL format '{0}' is not recognized. "
+ "Expected it be a shared link from Vimeo, YouTube, "
+ "or Google Drive.".format(video['url']))
+ results.append(({'meta/main.yml': data}, msg))
+
+ return results
diff --git a/lib/ansiblelint/rules/MissingFilePermissionsRule.py b/lib/ansiblelint/rules/MissingFilePermissionsRule.py
new file mode 100644
index 0000000..bc11cc7
--- /dev/null
+++ b/lib/ansiblelint/rules/MissingFilePermissionsRule.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2020 Sorin Sbarnea <sorin.sbarnea@gmail.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+from ansiblelint.rules import AnsibleLintRule
+
+# Despite documentation mentioning 'preserve' only these modules support it:
+_modules_with_preserve = (
+ 'copy',
+ 'template',
+)
+
+
+class MissingFilePermissionsRule(AnsibleLintRule):
+ id = "208"
+ shortdesc = 'File permissions unset or incorrect'
+ description = (
+ "Missing or unsupported mode parameter can cause unexpected file "
+ "permissions based "
+ "on version of Ansible being used. Be explicit, like ``mode: 0644`` to "
+ "avoid hitting this rule. Special ``preserve`` value is accepted "
+ f"only by {', '.join(_modules_with_preserve)} modules. "
+ "See https://github.com/ansible/ansible/issues/71200"
+ )
+ severity = 'VERY_HIGH'
+ tags = ['unpredictability', 'experimental']
+ version_added = 'v4.3.0'
+
+ _modules = {
+ 'archive',
+ 'assemble',
+ 'copy', # supports preserve
+ 'file',
+ 'replace', # implicit preserve behavior but mode: preserve is invalid
+ 'template', # supports preserve
+ # 'unarchive', # disabled because .tar.gz files can have permissions inside
+ }
+
+ _modules_with_create = {
+ 'blockinfile': False,
+ 'htpasswd': True,
+ 'ini_file': True,
+ 'lineinfile': False,
+ }
+
+ def matchtask(self, file, task):
+ module = task["action"]["__ansible_module__"]
+ mode = task['action'].get('mode', None)
+
+ if module not in self._modules and \
+ module not in self._modules_with_create:
+ return False
+
+ if mode == 'preserve' and module not in _modules_with_preserve:
+ return True
+
+ if module in self._modules_with_create:
+ create = task["action"].get("create", self._modules_with_create[module])
+ return create and mode is None
+
+ # A file that doesn't exist cannot have a mode
+ if task['action'].get('state', None) == "absent":
+ return False
+
+ # A symlink always has mode 0o777
+ if task['action'].get('state', None) == "link":
+ return False
+
+ # The file module does not create anything when state==file (default)
+ if module == "file" and \
+ task['action'].get('state', 'file') == 'file':
+ return False
+
+ # replace module is the only one that has a valid default preserve
+ # behavior, but we want to trigger rule if user used incorrect
+ # documentation and put 'preserve', which is not supported.
+ if module == 'replace' and mode is None:
+ return False
+
+ return mode is None
diff --git a/lib/ansiblelint/rules/NestedJinjaRule.py b/lib/ansiblelint/rules/NestedJinjaRule.py
new file mode 100644
index 0000000..c10d4ec
--- /dev/null
+++ b/lib/ansiblelint/rules/NestedJinjaRule.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+# Author: Adrián Tóth <adtoth@redhat.com>
+#
+# Copyright (c) 2020, Red Hat, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class NestedJinjaRule(AnsibleLintRule):
+ id = '207'
+ shortdesc = 'Nested jinja pattern'
+ description = (
+ "There should not be any nested jinja pattern. "
+ "Example (bad): ``{{ list_one + {{ list_two | max }} }}``, "
+ "example (good): ``{{ list_one + max(list_two) }}``"
+ )
+ severity = 'VERY_HIGH'
+ tags = ['formatting']
+ version_added = 'v4.3.0'
+
+ pattern = re.compile(r"{{(?:[^{}]*)?{{")
+
+ def matchtask(self, file, task):
+
+ command = "".join(
+ str(value)
+ # task properties are stored in the 'action' key
+ for key, value in task['action'].items()
+ # exclude useless values of '__file__', '__ansible_module__', '__*__', etc.
+ if not key.startswith('__') and not key.endswith('__')
+ )
+
+ return bool(self.pattern.search(command))
diff --git a/lib/ansiblelint/rules/NoFormattingInWhenRule.py b/lib/ansiblelint/rules/NoFormattingInWhenRule.py
new file mode 100644
index 0000000..a665311
--- /dev/null
+++ b/lib/ansiblelint/rules/NoFormattingInWhenRule.py
@@ -0,0 +1,34 @@
+from ansiblelint.rules import AnsibleLintRule
+
+
+class NoFormattingInWhenRule(AnsibleLintRule):
+ id = '102'
+ shortdesc = 'No Jinja2 in when'
+ description = '``when`` lines should not include Jinja2 variables'
+ severity = 'HIGH'
+ tags = ['deprecated', 'ANSIBLE0019']
+ version_added = 'historic'
+
+ def _is_valid(self, when):
+ if not isinstance(when, str):
+ return True
+ return when.find('{{') == -1 and when.find('}}') == -1
+
+ def matchplay(self, file, play):
+ errors = []
+ if isinstance(play, dict):
+ if 'roles' not in play or play['roles'] is None:
+ return errors
+ for role in play['roles']:
+ if self.matchtask(file, role):
+ errors.append(({'when': role},
+ 'role "when" clause has Jinja2 templates'))
+ if isinstance(play, list):
+ for play_item in play:
+ sub_errors = self.matchplay(file, play_item)
+ if sub_errors:
+ errors = errors + sub_errors
+ return errors
+
+ def matchtask(self, file, task):
+ return 'when' in task and not self._is_valid(task['when'])
diff --git a/lib/ansiblelint/rules/NoTabsRule.py b/lib/ansiblelint/rules/NoTabsRule.py
new file mode 100644
index 0000000..78222c8
--- /dev/null
+++ b/lib/ansiblelint/rules/NoTabsRule.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class NoTabsRule(AnsibleLintRule):
+ id = '203'
+ shortdesc = 'Most files should not contain tabs'
+ description = 'Tabs can cause unexpected display issues, use spaces'
+ severity = 'LOW'
+ tags = ['formatting']
+ version_added = 'v4.0.0'
+
+ def match(self, file, line):
+ return '\t' in line
diff --git a/lib/ansiblelint/rules/OctalPermissionsRule.py b/lib/ansiblelint/rules/OctalPermissionsRule.py
new file mode 100644
index 0000000..b95c322
--- /dev/null
+++ b/lib/ansiblelint/rules/OctalPermissionsRule.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class OctalPermissionsRule(AnsibleLintRule):
+ id = '202'
+ shortdesc = 'Octal file permissions must contain leading zero or be a string'
+ description = (
+ 'Numeric file permissions without leading zero can behave '
+ 'in unexpected ways. See '
+ 'http://docs.ansible.com/ansible/file_module.html'
+ )
+ severity = 'VERY_HIGH'
+ tags = ['formatting', 'ANSIBLE0009']
+ version_added = 'historic'
+
+ _modules = ['assemble', 'copy', 'file', 'ini_file', 'lineinfile',
+ 'replace', 'synchronize', 'template', 'unarchive']
+
+ def is_invalid_permission(self, mode):
+ # sensible file permission modes don't
+ # 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
+
+ other_write_without_read = (mode % 8 and mode % 8 < 4 and
+ not (mode % 8 == 1 and (mode >> 6) % 2 == 1))
+ group_write_without_read = ((mode >> 3) % 8 and (mode >> 3) % 8 < 4 and
+ not ((mode >> 3) % 8 == 1 and (mode >> 6) % 2 == 1))
+ user_write_without_read = ((mode >> 6) % 8 and (mode >> 6) % 8 < 4 and
+ not (mode >> 6) % 8 == 1)
+ other_more_generous_than_group = mode % 8 > (mode >> 3) % 8
+ other_more_generous_than_user = mode % 8 > (mode >> 6) % 8
+ group_more_generous_than_user = (mode >> 3) % 8 > (mode >> 6) % 8
+
+ return (other_write_without_read or
+ group_write_without_read or
+ user_write_without_read or
+ other_more_generous_than_group or
+ other_more_generous_than_user or
+ group_more_generous_than_user)
+
+ def matchtask(self, file, task):
+ if task["action"]["__ansible_module__"] in self._modules:
+ mode = task['action'].get('mode', None)
+
+ if isinstance(mode, str):
+ return False
+
+ if isinstance(mode, int):
+ return self.is_invalid_permission(mode)
diff --git a/lib/ansiblelint/rules/PackageIsNotLatestRule.py b/lib/ansiblelint/rules/PackageIsNotLatestRule.py
new file mode 100644
index 0000000..9fddaf4
--- /dev/null
+++ b/lib/ansiblelint/rules/PackageIsNotLatestRule.py
@@ -0,0 +1,67 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class PackageIsNotLatestRule(AnsibleLintRule):
+ id = '403'
+ shortdesc = 'Package installs should not use latest'
+ description = (
+ 'Package installs should use ``state=present`` '
+ 'with or without a version'
+ )
+ severity = 'VERY_LOW'
+ tags = ['module', 'repeatability', 'ANSIBLE0010']
+ version_added = 'historic'
+
+ _package_managers = [
+ 'apk',
+ 'apt',
+ 'bower',
+ 'bundler',
+ 'dnf',
+ 'easy_install',
+ 'gem',
+ 'homebrew',
+ 'jenkins_plugin',
+ 'npm',
+ 'openbsd_package',
+ 'openbsd_pkg',
+ 'package',
+ 'pacman',
+ 'pear',
+ 'pip',
+ 'pkg5',
+ 'pkgutil',
+ 'portage',
+ 'slackpkg',
+ 'sorcery',
+ 'swdepot',
+ 'win_chocolatey',
+ 'yarn',
+ 'yum',
+ 'zypper',
+ ]
+
+ def matchtask(self, file, task):
+ return (task['action']['__ansible_module__'] in self._package_managers and
+ not task['action'].get('version') and
+ task['action'].get('state') == 'latest')
diff --git a/lib/ansiblelint/rules/PlaybookExtension.py b/lib/ansiblelint/rules/PlaybookExtension.py
new file mode 100644
index 0000000..593e5ae
--- /dev/null
+++ b/lib/ansiblelint/rules/PlaybookExtension.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
+# Copyright (c) 2018, Ansible Project
+
+import os
+from typing import List
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class PlaybookExtension(AnsibleLintRule):
+ id = '205'
+ shortdesc = 'Use ".yml" or ".yaml" playbook extension'
+ description = 'Playbooks should have the ".yml" or ".yaml" extension'
+ severity = 'MEDIUM'
+ tags = ['formatting']
+ done = [] # type: List # already noticed path list
+ version_added = 'v4.0.0'
+
+ def match(self, file, text):
+ if file['type'] != 'playbook':
+ return False
+
+ path = file['path']
+ ext = os.path.splitext(path)
+ if ext[1] not in ['.yml', '.yaml'] and path not in self.done:
+ self.done.append(path)
+ return True
+ return False
diff --git a/lib/ansiblelint/rules/RoleNames.py b/lib/ansiblelint/rules/RoleNames.py
new file mode 100644
index 0000000..3d790b3
--- /dev/null
+++ b/lib/ansiblelint/rules/RoleNames.py
@@ -0,0 +1,74 @@
+# Copyright (c) 2020 Gael Chamoulaud <gchamoul@redhat.com>
+# Copyright (c) 2020 Sorin Sbarnea <ssbarnea@redhat.com>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import re
+from pathlib import Path
+from typing import List
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.utils import parse_yaml_from_file
+
+ROLE_NAME_REGEX = '^[a-z][a-z0-9_]+$'
+
+
+def _remove_prefix(text, prefix):
+ return re.sub(r'^{0}'.format(re.escape(prefix)), '', text)
+
+
+class RoleNames(AnsibleLintRule):
+ id = '106'
+ shortdesc = (
+ "Role name {} does not match ``%s`` pattern" % ROLE_NAME_REGEX
+ )
+ description = (
+ "Role names are now limited to contain only lowercase alphanumeric "
+ "characters, plus '_' and start with an alpha character. See "
+ "`developing collections <https://docs.ansible.com/ansible/devel/dev_guide/developing_"
+ "collections.html#roles-directory>`_"
+ )
+ severity = 'HIGH'
+ done: List[str] = [] # already noticed roles list
+ tags = ['deprecated']
+ version_added = 'v4.3.0'
+
+ ROLE_NAME_REGEXP = re.compile(ROLE_NAME_REGEX)
+
+ def match(self, file, text):
+ path = file['path'].split("/")
+ if "tasks" in path:
+ role_name = _remove_prefix(path[path.index("tasks") - 1], "ansible-role-")
+ role_root = path[:path.index("tasks")]
+ meta = Path("/".join(role_root)) / "meta" / "main.yml"
+
+ if meta.is_file():
+ meta_data = parse_yaml_from_file(str(meta))
+ if meta_data:
+ try:
+ role_name = meta_data['galaxy_info']['role_name']
+ except KeyError:
+ pass
+
+ if role_name in self.done:
+ return False
+ self.done.append(role_name)
+ if not re.match(self.ROLE_NAME_REGEXP, role_name):
+ return self.shortdesc.format(role_name)
+ return False
diff --git a/lib/ansiblelint/rules/RoleRelativePath.py b/lib/ansiblelint/rules/RoleRelativePath.py
new file mode 100644
index 0000000..87d7ac8
--- /dev/null
+++ b/lib/ansiblelint/rules/RoleRelativePath.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class RoleRelativePath(AnsibleLintRule):
+ id = '404'
+ shortdesc = "Doesn't need a relative path in role"
+ description = '``copy`` and ``template`` do not need to use relative path for ``src``'
+ severity = 'HIGH'
+ tags = ['module']
+ version_added = 'v4.0.0'
+
+ _module_to_path_folder = {
+ 'copy': 'files',
+ 'win_copy': 'files',
+ 'template': 'templates',
+ 'win_template': 'win_templates',
+ }
+
+ def matchtask(self, file, task):
+ module = task['action']['__ansible_module__']
+ if module not in self._module_to_path_folder:
+ return False
+
+ if 'src' not in task['action']:
+ return False
+
+ path_to_check = '../{}'.format(self._module_to_path_folder[module])
+ if path_to_check in task['action']['src']:
+ return True
diff --git a/lib/ansiblelint/rules/ShellWithoutPipefail.py b/lib/ansiblelint/rules/ShellWithoutPipefail.py
new file mode 100644
index 0000000..678e5a2
--- /dev/null
+++ b/lib/ansiblelint/rules/ShellWithoutPipefail.py
@@ -0,0 +1,38 @@
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class ShellWithoutPipefail(AnsibleLintRule):
+ id = '306'
+ shortdesc = 'Shells that use pipes should set the pipefail option'
+ description = (
+ 'Without the pipefail option set, a shell command that '
+ 'implements a pipeline can fail and still return 0. If '
+ 'any part of the pipeline other than the terminal command '
+ 'fails, the whole pipeline will still return 0, which may '
+ 'be considered a success by Ansible. '
+ 'Pipefail is available in the bash shell.'
+ )
+ severity = 'MEDIUM'
+ tags = ['command-shell']
+ version_added = 'v4.1.0'
+
+ _pipefail_re = re.compile(r"^\s*set.*[+-][A-z]*o\s*pipefail")
+ _pipe_re = re.compile(r"(?<!\|)\|(?!\|)")
+
+ def matchtask(self, file, task):
+ if task["__ansible_action_type__"] != "task":
+ return False
+
+ if task["action"]["__ansible_module__"] != "shell":
+ return False
+
+ if task.get("ignore_errors"):
+ return False
+
+ unjinjad_cmd = self.unjinja(
+ ' '.join(task["action"].get("__ansible_arguments__", [])))
+
+ return (self._pipe_re.search(unjinjad_cmd) and
+ not self._pipefail_re.match(unjinjad_cmd))
diff --git a/lib/ansiblelint/rules/SudoRule.py b/lib/ansiblelint/rules/SudoRule.py
new file mode 100644
index 0000000..8ea554e
--- /dev/null
+++ b/lib/ansiblelint/rules/SudoRule.py
@@ -0,0 +1,36 @@
+from ansiblelint.rules import AnsibleLintRule
+
+
+class SudoRule(AnsibleLintRule):
+ id = '103'
+ shortdesc = 'Deprecated sudo'
+ description = 'Instead of ``sudo``/``sudo_user``, use ``become``/``become_user``.'
+ severity = 'VERY_HIGH'
+ tags = ['deprecated', 'ANSIBLE0008']
+ version_added = 'historic'
+
+ def _check_value(self, play_frag):
+ results = []
+
+ if isinstance(play_frag, dict):
+ if 'sudo' in play_frag:
+ results.append(({'sudo': play_frag['sudo']},
+ 'Deprecated sudo feature', play_frag['__line__']))
+ if 'sudo_user' in play_frag:
+ results.append(({'sudo_user': play_frag['sudo_user']},
+ 'Deprecated sudo_user feature', play_frag['__line__']))
+ if 'tasks' in play_frag:
+ output = self._check_value(play_frag['tasks'])
+ if output:
+ results += output
+
+ if isinstance(play_frag, list):
+ for item in play_frag:
+ output = self._check_value(item)
+ if output:
+ results += output
+
+ return results
+
+ def matchplay(self, file, play):
+ return self._check_value(play)
diff --git a/lib/ansiblelint/rules/TaskHasNameRule.py b/lib/ansiblelint/rules/TaskHasNameRule.py
new file mode 100644
index 0000000..8757b03
--- /dev/null
+++ b/lib/ansiblelint/rules/TaskHasNameRule.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class TaskHasNameRule(AnsibleLintRule):
+ id = '502'
+ shortdesc = 'All tasks should be named'
+ description = (
+ 'All tasks should have a distinct name for readability '
+ 'and for ``--start-at-task`` to work'
+ )
+ severity = 'MEDIUM'
+ tags = ['task', 'readability', 'ANSIBLE0011']
+ version_added = 'historic'
+
+ _nameless_tasks = ['meta', 'debug', 'include_role', 'import_role',
+ 'include_tasks', 'import_tasks']
+
+ def matchtask(self, file, task):
+ return (not task.get('name') and
+ task["action"]["__ansible_module__"] not in self._nameless_tasks)
diff --git a/lib/ansiblelint/rules/TaskNoLocalAction.py b/lib/ansiblelint/rules/TaskNoLocalAction.py
new file mode 100644
index 0000000..294bb9d
--- /dev/null
+++ b/lib/ansiblelint/rules/TaskNoLocalAction.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2016, Tsukinowa Inc. <info@tsukinowa.jp>
+# Copyright (c) 2018, Ansible Project
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class TaskNoLocalAction(AnsibleLintRule):
+ id = '504'
+ shortdesc = "Do not use 'local_action', use 'delegate_to: localhost'"
+ description = 'Do not use ``local_action``, use ``delegate_to: localhost``'
+ severity = 'MEDIUM'
+ tags = ['task']
+ version_added = 'v4.0.0'
+
+ def match(self, file, text):
+ if 'local_action' in text:
+ return True
+ return False
diff --git a/lib/ansiblelint/rules/TrailingWhitespaceRule.py b/lib/ansiblelint/rules/TrailingWhitespaceRule.py
new file mode 100644
index 0000000..ac0f1c2
--- /dev/null
+++ b/lib/ansiblelint/rules/TrailingWhitespaceRule.py
@@ -0,0 +1,34 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class TrailingWhitespaceRule(AnsibleLintRule):
+ id = '201'
+ shortdesc = 'Trailing whitespace'
+ description = 'There should not be any trailing whitespace'
+ severity = 'INFO'
+ tags = ['formatting', 'ANSIBLE0002']
+ version_added = 'historic'
+
+ def match(self, file, line):
+ line = line.replace("\r", "")
+ return line.rstrip() != line
diff --git a/lib/ansiblelint/rules/UseCommandInsteadOfShellRule.py b/lib/ansiblelint/rules/UseCommandInsteadOfShellRule.py
new file mode 100644
index 0000000..48babcf
--- /dev/null
+++ b/lib/ansiblelint/rules/UseCommandInsteadOfShellRule.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class UseCommandInsteadOfShellRule(AnsibleLintRule):
+ id = '305'
+ shortdesc = 'Use shell only when shell functionality is required'
+ description = (
+ 'Shell should only be used when piping, redirecting '
+ 'or chaining commands (and Ansible would be preferred '
+ 'for some of those!)'
+ )
+ severity = 'HIGH'
+ tags = ['command-shell', 'safety', 'ANSIBLE0013']
+ version_added = 'historic'
+
+ def matchtask(self, file, task):
+ # 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 = self.unjinja(task["action"].get("cmd", []))
+ else:
+ unjinjad_cmd = self.unjinja(
+ ' '.join(task["action"].get("__ansible_arguments__", [])))
+ return not any([ch in unjinjad_cmd for ch in '&|<>;$\n*[]{}?`'])
diff --git a/lib/ansiblelint/rules/UseHandlerRatherThanWhenChangedRule.py b/lib/ansiblelint/rules/UseHandlerRatherThanWhenChangedRule.py
new file mode 100644
index 0000000..53b389d
--- /dev/null
+++ b/lib/ansiblelint/rules/UseHandlerRatherThanWhenChangedRule.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2016 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+def _changed_in_when(item):
+ if not isinstance(item, str):
+ return False
+ return any(changed in item for changed in
+ ['.changed', '|changed', '["changed"]', "['changed']"])
+
+
+class UseHandlerRatherThanWhenChangedRule(AnsibleLintRule):
+ id = '503'
+ shortdesc = 'Tasks that run when changed should likely be handlers'
+ description = (
+ 'If a task has a ``when: result.changed`` setting, it is effectively '
+ 'acting as a handler'
+ )
+ severity = 'MEDIUM'
+ tags = ['task', 'behaviour', 'ANSIBLE0016']
+ version_added = 'historic'
+
+ def matchtask(self, file, task):
+ if task["__ansible_action_type__"] != 'task':
+ return False
+
+ when = task.get('when')
+
+ if isinstance(when, list):
+ for item in when:
+ return _changed_in_when(item)
+ else:
+ return _changed_in_when(when)
diff --git a/lib/ansiblelint/rules/UsingBareVariablesIsDeprecatedRule.py b/lib/ansiblelint/rules/UsingBareVariablesIsDeprecatedRule.py
new file mode 100644
index 0000000..a0721ac
--- /dev/null
+++ b/lib/ansiblelint/rules/UsingBareVariablesIsDeprecatedRule.py
@@ -0,0 +1,75 @@
+# Copyright (c) 2013-2014 Will Thames <will@thames.id.au>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import os
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class UsingBareVariablesIsDeprecatedRule(AnsibleLintRule):
+ id = '104'
+ shortdesc = 'Using bare variables is deprecated'
+ description = (
+ 'Using bare variables is deprecated. Update your '
+ 'playbooks so that the environment value uses the full variable '
+ 'syntax ``{{ your_variable }}``'
+ )
+ severity = 'VERY_HIGH'
+ tags = ['deprecated', 'formatting', 'ANSIBLE0015']
+ version_added = 'historic'
+
+ _jinja = re.compile(r"{{.*}}", re.DOTALL)
+ _glob = re.compile('[][*?]')
+
+ def matchtask(self, file, task):
+ loop_type = next((key for key in task
+ if key.startswith("with_")), None)
+ if loop_type:
+ if loop_type in ["with_nested", "with_together", "with_flattened", "with_filetree"]:
+ # These loops can either take a list defined directly in the task
+ # or a variable that is a list itself. When a single variable is used
+ # we just need to check that one variable, and not iterate over it like
+ # it's a list. Otherwise, loop through and check all items.
+ items = task[loop_type]
+ if not isinstance(items, (list, tuple)):
+ items = [items]
+ for var in items:
+ return self._matchvar(var, task, loop_type)
+ elif loop_type == "with_subelements":
+ return self._matchvar(task[loop_type][0], task, loop_type)
+ elif loop_type in ["with_sequence", "with_ini",
+ "with_inventory_hostnames"]:
+ pass
+ else:
+ return self._matchvar(task[loop_type], task, loop_type)
+
+ def _matchvar(self, varstring, task, loop_type):
+ if (isinstance(varstring, str) and
+ not self._jinja.match(varstring)):
+ valid = loop_type == 'with_fileglob' and bool(self._jinja.search(varstring) or
+ self._glob.search(varstring))
+
+ valid |= loop_type == 'with_filetree' and bool(self._jinja.search(varstring) or
+ varstring.endswith(os.sep))
+ if not valid:
+ message = "Found a bare variable '{0}' used in a '{1}' loop." + \
+ " You should use the full variable syntax ('{{{{ {0} }}}}')"
+ return message.format(task[loop_type], loop_type)
diff --git a/lib/ansiblelint/rules/VariableHasSpacesRule.py b/lib/ansiblelint/rules/VariableHasSpacesRule.py
new file mode 100644
index 0000000..dd4f441
--- /dev/null
+++ b/lib/ansiblelint/rules/VariableHasSpacesRule.py
@@ -0,0 +1,24 @@
+# Copyright (c) 2016, Will Thames and contributors
+# Copyright (c) 2018, Ansible Project
+
+import re
+
+from ansiblelint.rules import AnsibleLintRule
+
+
+class VariableHasSpacesRule(AnsibleLintRule):
+ id = '206'
+ shortdesc = 'Variables should have spaces before and after: {{ var_name }}'
+ description = 'Variables should have spaces before and after: ``{{ var_name }}``'
+ severity = 'LOW'
+ tags = ['formatting']
+ version_added = 'v4.0.0'
+
+ variable_syntax = re.compile(r"{{.*}}")
+ bracket_regex = re.compile(r"{{[^{' -]|[^ '}-]}}")
+
+ def match(self, file, line):
+ if not self.variable_syntax.search(line):
+ return
+ line_exclude_json = re.sub(r"[^{]{'\w+': ?[^{]{.*?}}", "", line)
+ return self.bracket_regex.search(line_exclude_json)
diff --git a/lib/ansiblelint/rules/__init__.py b/lib/ansiblelint/rules/__init__.py
new file mode 100644
index 0000000..fd3e92d
--- /dev/null
+++ b/lib/ansiblelint/rules/__init__.py
@@ -0,0 +1,254 @@
+"""All internal ansible-lint rules."""
+import glob
+import importlib.util
+import logging
+import os
+import re
+from collections import defaultdict
+from importlib.abc import Loader
+from time import sleep
+from typing import List
+
+import ansiblelint.utils
+from ansiblelint.errors import MatchError
+from ansiblelint.skip_utils import append_skipped_rules, get_rule_skips_from_line
+
+_logger = logging.getLogger(__name__)
+
+
+class AnsibleLintRule(object):
+
+ def __repr__(self) -> str:
+ """Return a AnsibleLintRule instance representation."""
+ return self.id + ": " + self.shortdesc
+
+ def verbose(self) -> str:
+ return self.id + ": " + self.shortdesc + "\n " + self.description
+
+ id: str = ""
+ tags: List[str] = []
+ shortdesc: str = ""
+ description: str = ""
+ version_added: str = ""
+ severity: str = ""
+ match = None
+ matchtask = None
+ matchplay = None
+
+ @staticmethod
+ def unjinja(text):
+ text = re.sub(r"{{.+?}}", "JINJA_EXPRESSION", text)
+ text = re.sub(r"{%.+?%}", "JINJA_STATEMENT", text)
+ text = re.sub(r"{#.+?#}", "JINJA_COMMENT", text)
+ return text
+
+ def matchlines(self, file, text) -> List[MatchError]:
+ matches: List[MatchError] = []
+ if not self.match:
+ return matches
+ # arrays are 0-based, line numbers are 1-based
+ # so use prev_line_no as the counter
+ for (prev_line_no, line) in enumerate(text.split("\n")):
+ if line.lstrip().startswith('#'):
+ continue
+
+ rule_id_list = get_rule_skips_from_line(line)
+ if self.id in rule_id_list:
+ continue
+
+ result = self.match(file, line)
+ if not result:
+ continue
+ message = None
+ if isinstance(result, str):
+ message = result
+ m = MatchError(
+ message=message,
+ linenumber=prev_line_no + 1,
+ details=line,
+ filename=file['path'],
+ rule=self)
+ matches.append(m)
+ return matches
+
+ # TODO(ssbarnea): Reduce mccabe complexity
+ # https://github.com/ansible/ansible-lint/issues/744
+ def matchtasks(self, file: str, text: str) -> List[MatchError]: # noqa: C901
+ matches: List[MatchError] = []
+ if not self.matchtask:
+ return matches
+
+ if file['type'] == 'meta':
+ return matches
+
+ yaml = ansiblelint.utils.parse_yaml_linenumbers(text, file['path'])
+ if not yaml:
+ return matches
+
+ yaml = append_skipped_rules(yaml, text, file['type'])
+
+ try:
+ tasks = ansiblelint.utils.get_normalized_tasks(yaml, file)
+ except MatchError as e:
+ return [e]
+
+ for task in tasks:
+ if self.id in task.get('skipped_rules', ()):
+ continue
+
+ if 'action' not in task:
+ continue
+ result = self.matchtask(file, task)
+ if not result:
+ continue
+
+ message = None
+ if isinstance(result, str):
+ message = result
+ task_msg = "Task/Handler: " + ansiblelint.utils.task_to_str(task)
+ m = MatchError(
+ message=message,
+ linenumber=task[ansiblelint.utils.LINE_NUMBER_KEY],
+ details=task_msg,
+ filename=file['path'],
+ rule=self)
+ matches.append(m)
+ return matches
+
+ @staticmethod
+ def _matchplay_linenumber(play, optional_linenumber):
+ try:
+ linenumber, = optional_linenumber
+ except ValueError:
+ linenumber = play[ansiblelint.utils.LINE_NUMBER_KEY]
+ return linenumber
+
+ def matchyaml(self, file: str, text: str) -> List[MatchError]:
+ matches: List[MatchError] = []
+ if not self.matchplay:
+ return matches
+
+ yaml = ansiblelint.utils.parse_yaml_linenumbers(text, file['path'])
+ if not yaml:
+ return matches
+
+ if isinstance(yaml, dict):
+ yaml = [yaml]
+
+ yaml = ansiblelint.skip_utils.append_skipped_rules(yaml, text, file['type'])
+
+ for play in yaml:
+ if self.id in play.get('skipped_rules', ()):
+ continue
+
+ result = self.matchplay(file, play)
+ if not result:
+ continue
+
+ if isinstance(result, tuple):
+ result = [result]
+
+ if not isinstance(result, list):
+ raise TypeError("{} is not a list".format(result))
+
+ for section, message, *optional_linenumber in result:
+ linenumber = self._matchplay_linenumber(play, optional_linenumber)
+ m = MatchError(
+ message=message,
+ linenumber=linenumber,
+ details=str(section),
+ filename=file['path'],
+ rule=self)
+ matches.append(m)
+ return matches
+
+
+def load_plugins(directory: str) -> List[AnsibleLintRule]:
+ """Return a list of rule classes."""
+ result = []
+
+ for pluginfile in glob.glob(os.path.join(directory, '[A-Za-z]*.py')):
+
+ pluginname = os.path.basename(pluginfile.replace('.py', ''))
+ spec = importlib.util.spec_from_file_location(pluginname, pluginfile)
+ # https://github.com/python/typeshed/issues/2793
+ if spec and isinstance(spec.loader, Loader):
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ obj = getattr(module, pluginname)()
+ result.append(obj)
+ return result
+
+
+class RulesCollection(object):
+
+ def __init__(self, rulesdirs=None) -> None:
+ """Initialize a RulesCollection instance."""
+ if rulesdirs is None:
+ rulesdirs = []
+ self.rulesdirs = ansiblelint.utils.expand_paths_vars(rulesdirs)
+ self.rules: List[AnsibleLintRule] = []
+ for rulesdir in self.rulesdirs:
+ _logger.debug("Loading rules from %s", rulesdir)
+ self.extend(load_plugins(rulesdir))
+ self.rules = sorted(self.rules, key=lambda r: r.id)
+
+ def register(self, obj: AnsibleLintRule):
+ self.rules.append(obj)
+
+ def __iter__(self):
+ """Return the iterator over the rules in the RulesCollection."""
+ return iter(self.rules)
+
+ def __len__(self):
+ """Return the length of the RulesCollection data."""
+ return len(self.rules)
+
+ def extend(self, more: List[AnsibleLintRule]) -> None:
+ self.rules.extend(more)
+
+ def run(self, playbookfile, tags=set(), skip_list=frozenset()) -> List:
+ text = ""
+ matches: List = list()
+
+ for i in range(3):
+ try:
+ with open(playbookfile['path'], mode='r', encoding='utf-8') as f:
+ text = f.read()
+ break
+ except IOError as e:
+ _logger.warning(
+ "Couldn't open %s - %s [try:%s]",
+ playbookfile['path'],
+ e.strerror,
+ i)
+ sleep(1)
+ continue
+ if i and not text:
+ return matches
+
+ for rule in self.rules:
+ if not tags or not set(rule.tags).union([rule.id]).isdisjoint(tags):
+ rule_definition = set(rule.tags)
+ rule_definition.add(rule.id)
+ if set(rule_definition).isdisjoint(skip_list):
+ matches.extend(rule.matchlines(playbookfile, text))
+ matches.extend(rule.matchtasks(playbookfile, text))
+ matches.extend(rule.matchyaml(playbookfile, text))
+
+ return matches
+
+ def __repr__(self) -> str:
+ """Return a RulesCollection instance representation."""
+ return "\n".join([rule.verbose()
+ for rule in sorted(self.rules, key=lambda x: x.id)])
+
+ def listtags(self) -> str:
+ tags = defaultdict(list)
+ for rule in self.rules:
+ for tag in rule.tags:
+ tags[tag].append("[{0}]".format(rule.id))
+ results = []
+ for tag in sorted(tags):
+ results.append("{0} {1}".format(tag, tags[tag]))
+ return "\n".join(results)
diff --git a/lib/ansiblelint/rules/custom/__init__.py b/lib/ansiblelint/rules/custom/__init__.py
new file mode 100644
index 0000000..8c3e048
--- /dev/null
+++ b/lib/ansiblelint/rules/custom/__init__.py
@@ -0,0 +1 @@
+"""A placeholder package for putting custom rules under this dir."""