summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.ansible-lint2
-rw-r--r--.coveragerc20
-rw-r--r--.flake8123
-rw-r--r--.git_archival.txt1
-rw-r--r--.gitattributes7
-rw-r--r--.github/CODE_OF_CONDUCT.md3
-rw-r--r--.github/CONTRIBUTING.rst79
-rw-r--r--.github/ISSUE_TEMPLATE.md32
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md65
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml16
-rw-r--r--.github/ISSUE_TEMPLATE/documentation_report.md24
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md24
-rw-r--r--.github/SECURITY.rst17
-rw-r--r--.github/dependabot.yml22
-rw-r--r--.github/release-drafter.yml21
-rw-r--r--.github/workflows/tox.yml296
-rw-r--r--.gitignore46
-rw-r--r--.isort.cfg9
-rw-r--r--.pre-commit-config.yaml71
-rw-r--r--.pre-commit-hooks.yaml14
-rw-r--r--.pylintrc37
-rw-r--r--.readthedocs.yml41
-rw-r--r--.yamllint4
-rw-r--r--CHANGELOG.rst188
-rw-r--r--DCO_1_1.md45
-rw-r--r--LICENSE21
-rw-r--r--README.rst640
-rw-r--r--bindep.txt13
-rw-r--r--conftest.py30
-rw-r--r--docs/.gitignore16
-rw-r--r--docs/.nojekyll0
-rw-r--r--docs/README.md9
-rw-r--r--docs/_static/ansible-lint.svg15
-rw-r--r--docs/_static/images/logo_invert.pngbin0 -> 4708 bytes
-rw-r--r--docs/_static/theme_overrides.css15
-rw-r--r--docs/conf.py256
-rw-r--r--docs/configuring.rst14
-rw-r--r--docs/contributing.rst50
-rw-r--r--docs/default_rules.rst1
-rw-r--r--docs/index.rst47
-rw-r--r--docs/installing.rst15
-rw-r--r--docs/jinja2-2.9.7.invbin0 -> 3210 bytes
-rw-r--r--docs/python2-2.7.13.invbin0 -> 84880 bytes
-rw-r--r--docs/python3-3.6.2.invbin0 -> 97801 bytes
-rw-r--r--docs/requirements.in5
-rw-r--r--docs/requirements.txt184
-rw-r--r--docs/rules.rst13
-rw-r--r--docs/rules_table_generator_ext.py68
-rw-r--r--docs/usage.rst15
-rw-r--r--examples/example.yml52
-rw-r--r--examples/handlers/y.yml2
-rw-r--r--examples/include.yml19
-rw-r--r--examples/lineno.yml2
-rw-r--r--examples/lots_of_warnings.yml1000
-rw-r--r--examples/nomatches.yml9
-rw-r--r--examples/play.yml6
-rw-r--r--examples/roles/bobbins/tasks/main.yml3
-rw-r--r--examples/roles/hello/meta/main.yml3
-rw-r--r--examples/roles/morecomplex/handlers/main.yml2
-rw-r--r--examples/roles/morecomplex/tasks/main.yml8
-rw-r--r--examples/rules/TaskHasTag.py37
-rw-r--r--examples/tasks/x.yml4
-rw-r--r--examples/unicode.yml5
-rw-r--r--hooks.yaml11
-rw-r--r--lib/ansiblelint/__init__.py28
-rwxr-xr-xlib/ansiblelint/__main__.py270
-rw-r--r--lib/ansiblelint/cli.py219
-rw-r--r--lib/ansiblelint/color.py31
-rw-r--r--lib/ansiblelint/constants.py18
-rw-r--r--lib/ansiblelint/errors.py81
-rw-r--r--lib/ansiblelint/file_utils.py25
-rw-r--r--lib/ansiblelint/formatters/__init__.py167
-rw-r--r--lib/ansiblelint/generate_docs.py66
-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
-rw-r--r--lib/ansiblelint/runner.py111
-rw-r--r--lib/ansiblelint/skip_utils.py189
-rw-r--r--lib/ansiblelint/testing/__init__.py84
-rw-r--r--lib/ansiblelint/utils.py836
-rw-r--r--lib/ansiblelint/version.py12
-rw-r--r--mypy.ini37
-rw-r--r--pyproject.toml13
-rw-r--r--pytest.ini68
-rw-r--r--setup.cfg86
-rwxr-xr-xsetup.py9
-rwxr-xr-xtest-requirements.in9
-rw-r--r--test-requirements.txt24
-rw-r--r--test/TestAlwaysRunRule.py24
-rw-r--r--test/TestAnsibleLintRule.py7
-rw-r--r--test/TestAnsibleSyntax.py16
-rw-r--r--test/TestBaseFormatter.py46
-rw-r--r--test/TestBecomeUserWithoutBecome.py24
-rw-r--r--test/TestCliRolePaths.py134
-rw-r--r--test/TestCommandHasChangesCheck.py24
-rw-r--r--test/TestCommandLineInvocationSameAsConfig.py137
-rw-r--r--test/TestComparisonToEmptyString.py39
-rw-r--r--test/TestComparisonToLiteralBool.py69
-rw-r--r--test/TestDependenciesInMeta.py22
-rw-r--r--test/TestDeprecatedModule.py32
-rw-r--r--test/TestEnvVarsInCommand.py90
-rw-r--r--test/TestExamples.py8
-rw-r--r--test/TestFormatter.py46
-rw-r--r--test/TestImportIncludeRole.py103
-rw-r--r--test/TestImportPlaybook.py17
-rw-r--r--test/TestImportWithMalformed.py65
-rw-r--r--test/TestIncludeMissFileWithRole.py122
-rw-r--r--test/TestIncludeMissingFileRule.py88
-rw-r--r--test/TestLineNumber.py36
-rw-r--r--test/TestLineTooLong.py24
-rw-r--r--test/TestLintRule.py45
-rw-r--r--test/TestLocalContent.py42
-rw-r--r--test/TestMatchError.py178
-rw-r--r--test/TestMetaChangeFromDefault.py33
-rw-r--r--test/TestMetaMainHasInfo.py94
-rw-r--r--test/TestMetaVideoLinks.py35
-rw-r--r--test/TestMissingFilePermissionsRule.py110
-rw-r--r--test/TestNestedJinjaRule.py208
-rw-r--r--test/TestNoFormattingInWhenRule.py24
-rw-r--r--test/TestOctalPermissions.py112
-rw-r--r--test/TestPackageIsNotLatest.py24
-rw-r--r--test/TestRoleHandlers.py20
-rw-r--r--test/TestRoleNames.py82
-rw-r--r--test/TestRoleRelativePath.py52
-rw-r--r--test/TestRuleProperties.py11
-rw-r--r--test/TestRulesCollection.py112
-rw-r--r--test/TestRunner.py86
-rw-r--r--test/TestShellWithoutPipefail.py84
-rw-r--r--test/TestSkipImportPlaybook.py35
-rw-r--r--test/TestSkipInsideYaml.py122
-rw-r--r--test/TestSkipPlaybookItems.py99
-rw-r--r--test/TestSudoRule.py67
-rw-r--r--test/TestTaskHasName.py24
-rw-r--r--test/TestTaskIncludes.py34
-rw-r--r--test/TestTaskNoLocalAction.py24
-rw-r--r--test/TestUseCommandInsteadOfShell.py24
-rw-r--r--test/TestUseHandlerRatherThanWhenChanged.py88
-rw-r--r--test/TestUsingBareVariablesIsDeprecated.py24
-rw-r--r--test/TestUtils.py317
-rw-r--r--test/TestVariableHasSpaces.py54
-rw-r--r--test/TestWithSkipTagId.py39
-rw-r--r--test/__init__.py1
-rw-r--r--test/always-run-failure.yml6
-rw-r--r--test/always-run-success.yml6
-rw-r--r--test/bar.txt1
-rw-r--r--test/become-user-without-become-failure.yml26
-rw-r--r--test/become-user-without-become-success.yml30
-rw-r--r--test/become.yml14
-rw-r--r--test/block.yml26
-rw-r--r--test/blockincludes.yml13
-rw-r--r--test/blockincludes2.yml13
-rw-r--r--test/brackets-do-not-match-test.yml22
-rw-r--r--test/bracketsmatchtest.yml3
-rw-r--r--test/command-check-failure.yml11
-rw-r--r--test/command-check-success.yml61
-rw-r--r--test/command-instead-of-shell-failure.yml8
-rw-r--r--test/command-instead-of-shell-success.yml37
-rw-r--r--test/common-include-1.yml4
-rw-r--r--test/common-include-2.yml4
-rw-r--r--test/contains_secrets.yml14
-rw-r--r--test/custom_rules/__init__.py1
-rw-r--r--test/custom_rules/example_com/ExampleComRule.py28
-rw-r--r--test/custom_rules/example_com/__init__.py1
-rw-r--r--test/custom_rules/example_inc/CustomAlwaysRunRule.py28
-rw-r--r--test/custom_rules/example_inc/__init__.py1
-rw-r--r--test/dependency-in-meta/bitbucket.yml10
-rw-r--r--test/dependency-in-meta/galaxy.yml5
-rw-r--r--test/dependency-in-meta/github.yml10
-rw-r--r--test/dependency-in-meta/gitlab.yml7
-rw-r--r--test/dependency-in-meta/webserver.yml6
-rw-r--r--test/directory with spaces/main.yml1
-rw-r--r--test/ematchtest.yml5
-rw-r--r--test/emptytags.yml7
-rw-r--r--test/fixtures/ansible-config-invalid.yml3
-rw-r--r--test/fixtures/ansible-config.yml4
-rw-r--r--test/fixtures/config-with-relative-path.yml5
-rw-r--r--test/fixtures/exclude-paths-with-expands.yml6
-rw-r--r--test/fixtures/exclude-paths.yml5
-rw-r--r--test/fixtures/parseable.yml4
-rw-r--r--test/fixtures/quiet.yml4
-rw-r--r--test/fixtures/rulesdir-defaults.yml6
-rw-r--r--test/fixtures/rulesdir.yml5
-rw-r--r--test/fixtures/show-abspath.yml4
-rw-r--r--test/fixtures/show-relpath.yml4
-rw-r--r--test/fixtures/skip-tags.yml5
-rw-r--r--test/fixtures/tags.yml5
-rw-r--r--test/fixtures/unknown-type.yml2
-rw-r--r--test/fixtures/verbosity.yml4
-rw-r--r--test/foo.txt1
-rw-r--r--test/include-import-role.yml17
-rw-r--r--test/include-import-tasks-in-role.yml3
-rw-r--r--test/include-in-block-inner.yml5
-rw-r--r--test/include-in-block.yml5
-rw-r--r--test/included-handlers.yml6
-rw-r--r--test/included-with-lint.yml4
-rw-r--r--test/includedoesnotexist.yml3
-rw-r--r--test/jinja2-when-failure.yml10
-rw-r--r--test/jinja2-when-success.yml8
-rw-r--r--test/local-content/README.md6
-rw-r--r--test/local-content/collections/ansible_collections/testns/testcoll/galaxy.yml3
-rw-r--r--test/local-content/collections/ansible_collections/testns/testcoll/plugins/filter/test_filter.py16
-rw-r--r--test/local-content/collections/ansible_collections/testns/testcoll/plugins/modules/test_module_2.py14
-rw-r--r--test/local-content/test-collection.yml10
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py14
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py16
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py14
-rw-r--r--test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed-complete/test.yml5
-rw-r--r--test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py14
-rw-r--r--test/local-content/test-roles-failed/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py16
-rw-r--r--test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py14
-rw-r--r--test/local-content/test-roles-failed/roles/role3/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-failed/test.yml7
-rw-r--r--test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py14
-rw-r--r--test/local-content/test-roles-success/roles/role1/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-success/roles/role2/tasks/main.yml11
-rw-r--r--test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py16
-rw-r--r--test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py14
-rw-r--r--test/local-content/test-roles-success/roles/role3/tasks/main.yml3
-rw-r--r--test/local-content/test-roles-success/test.yml7
-rw-r--r--test/multiline-brackets-do-not-match-test.yml22
-rw-r--r--test/multiline-bracketsmatchtest.yml22
-rw-r--r--test/nestedincludes.yml2
-rw-r--r--test/nomatchestest.yml9
-rw-r--r--test/norole.yml5
-rw-r--r--test/norole2.yml5
-rw-r--r--test/package-check-failure.yml14
-rw-r--r--test/package-check-success.yml15
-rw-r--r--test/playbook-import/playbook_imported.yml9
-rw-r--r--test/playbook-import/playbook_parent.yml3
-rw-r--r--test/role-with-handler/a-role/handlers/main.yml5
-rw-r--r--test/role-with-handler/main.yml4
-rw-r--r--test/role-with-included-imported-tasks/tasks/imported_tasks.yml2
-rw-r--r--test/role-with-included-imported-tasks/tasks/included_tasks.yml2
-rw-r--r--test/role-with-included-imported-tasks/tasks/main.yml6
-rw-r--r--test/roles/ansible-role-foo/tasks/main.yaml0
-rw-r--r--test/roles/invalid-name/tasks/main.yaml4
-rw-r--r--test/roles/invalid_due_to_meta/meta/main.yml8
-rw-r--r--test/roles/invalid_due_to_meta/tasks/main.yaml0
-rw-r--r--test/roles/test-role/molecule/default/include-import-role.yml6
-rw-r--r--test/roles/test-role/tasks/main.yml2
-rw-r--r--test/roles/valid-due-to-meta/meta/main.yml8
-rw-r--r--test/roles/valid-due-to-meta/tasks/debian/main.yml2
-rw-r--r--test/roles/valid-due-to-meta/tasks/main.yaml0
-rw-r--r--test/rules/EMatcherRule.py12
-rw-r--r--test/rules/UnsetVariableMatcherRule.py12
-rw-r--r--test/rules/__init__.py3
-rw-r--r--test/simpletask.yml3
-rw-r--r--test/skiptasks.yml70
-rw-r--r--test/task-has-name-failure.yml7
-rw-r--r--test/task-has-name-success.yml9
-rw-r--r--test/taskimports.yml9
-rw-r--r--test/taskincludes.yml9
-rw-r--r--test/taskincludes_2_4_style.yml9
-rw-r--r--test/test-role/tasks/main.yml2
-rw-r--r--test/test-role/tasks/world.yml1
-rw-r--r--test/test/always-run-success.yml1
-rw-r--r--test/testproject/roles/test-role/tasks/main.yml2
-rw-r--r--test/unicode.yml9
-rw-r--r--test/using-bare-variables-failure.yml108
-rw-r--r--test/using-bare-variables-success.yml200
-rw-r--r--test/varset.yml3
-rw-r--r--test/varunset.yml1
-rw-r--r--test/with-skip-tag-id.yml6
-rwxr-xr-xtools/test-setup.sh17
-rw-r--r--tox.ini134
305 files changed, 13182 insertions, 0 deletions
diff --git a/.ansible-lint b/.ansible-lint
new file mode 100644
index 0000000..8e7ec00
--- /dev/null
+++ b/.ansible-lint
@@ -0,0 +1,2 @@
+exclude_paths:
+- .github/
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..8af2326
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,20 @@
+[run]
+branch = true
+parallel = true
+
+[report]
+skip_covered = True
+show_missing = True
+exclude_lines =
+ \#\s*pragma: no cover
+ ^\s*raise AssertionError\b
+ ^\s*raise NotImplementedError\b
+ ^\s*return NotImplemented\b
+ ^\s*raise$
+ ^if __name__ == ['"]__main__['"]:$
+
+[paths]
+source = lib/ansiblelint
+ */.tox/*/lib/python*/site-packages/ansiblelint
+ */.tox/pypy*/site-packages/ansiblelint
+ */lib/ansiblelint
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..4e6a733
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,123 @@
+[flake8]
+
+# Don't even try to analyze these:
+exclude =
+ # No need to traverse egg files
+ *.egg,
+ # No need to traverse egg info dir
+ *.egg-info,
+ # No need to traverse eggs directory
+ .eggs,
+ # No need to traverse our git directory
+ .git,
+ # GitHub configs
+ .github,
+ # Cache files of MyPy
+ .mypy_cache,
+ # Cache files of pytest
+ .pytest_cache,
+ # Temp dir of pytest-testmon
+ .tmontmp,
+ # Countless third-party libs in venvs
+ .tox,
+ # Occasional virtualenv dir
+ .venv
+ # VS Code
+ .vscode,
+ # There's no value in checking cache directories
+ __pycache__,
+ # Temporary build dir
+ build,
+ # This contains sdists and wheels of ansible-lint that we don't want to check
+ dist,
+ # Occasional virtualenv dir
+ env,
+ # Metadata of `pip wheel` cmd is autogenerated
+ pip-wheel-metadata,
+
+# Let's not overcomplicate the code:
+max-complexity = 10
+
+# Accessibility/large fonts and PEP8 friendly:
+#max-line-length = 79
+# Accessibility/large fonts and PEP8 unfriendly:
+max-line-length = 100
+
+# Allow certain violations in certain files:
+per-file-ignores =
+ # FIXME: D100 Missing docstring in public module
+ # FIXME: D101 Missing docstring in public class
+ # FIXME: D102 Missing docstring in public method
+ # FIXME: D103 Missing docstring in public function
+ # FIXME: drop these once they're made simpler
+ # Ref: https://github.com/ansible/ansible-lint/issues/744
+ # lib/ansiblelint/__main__.py:32:1: C901 'main' is too complex (12)
+ lib/ansiblelint/__main__.py: C901
+ lib/ansiblelint/cli.py: D101 D102 D103
+ lib/ansiblelint/formatters/__init__.py: D101 D102
+ lib/ansiblelint/utils.py: D103
+ lib/ansiblelint/rules/*.py: D100 D101 D102
+
+ # FIXME: drop these once they're fixed
+ # Ref: https://github.com/ansible/ansible-lint/issues/725
+ test/__init__.py: D102
+ test/conftest.py: D100 D103
+ test/rules/EMatcherRule.py: D100 D101 D102
+ test/rules/UnsetVariableMatcherRule.py: D100 D101 D102
+ test/TestAlwaysRunRule.py: PT009 D100 D101 D102
+ test/TestAnsibleLintRule.py: D100 D103
+ test/TestBaseFormatter.py: D100 D103
+ test/TestBecomeUserWithoutBecome.py: PT009 D100 D101 D102
+ test/TestCliRolePaths.py: PT009 D100 D101 D102
+ test/TestCommandLineInvocationSameAsConfig.py: D100 D103
+ test/TestCommandHasChangesCheck.py: PT009 D100 D101 D102
+ test/TestComparisonToEmptyString.py: PT009 D100 D101 D102
+ test/TestComparisonToLiteralBool.py: PT009 D100 D101 D102
+ test/TestDependenciesInMeta.py: D100 D103
+ test/TestDeprecatedModule.py: PT009 D100 D101 D102
+ test/TestEnvVarsInCommand.py: PT009 D100 D101 D102
+ test/TestFormatter.py: D100 D101 D102
+ test/TestImportIncludeRole.py: D100 D103
+ test/TestImportWithMalformed.py: D100 D103
+ test/TestIncludeMissingFileRule.py: D100 D103
+ test/TestIncludeMissFileWithRole.py: D100 D103
+ test/TestLineNumber.py: D100
+ test/TestLineTooLong.py: PT009 D100 D101 D102
+ test/TestLintRule.py: PT009 D100 D101 D102
+ test/TestNestedJinjaRule.py: D100 D103
+ test/TestMatchError.py: D101
+ test/TestMetaChangeFromDefault.py: PT009 D100 D101 D102
+ test/TestMetaMainHasInfo.py: PT009 D100 D101 D102
+ test/TestMetaTagValid.py: PT009 D100 D101 D102
+ test/TestMetaVideoLinks.py: PT009 D100 D101 D102
+ test/TestNoFormattingInWhenRule.py: PT009 D100 D101 D102
+ test/TestOctalPermissions.py: PT009 D100 D101 D102
+ test/TestPackageIsNotLatest.py: PT009 D100 D101 D102
+ test/TestPretaskIncludePlaybook.py: D100 D103
+ test/TestRoleHandlers.py: PT009 D100 D101 D102
+ test/TestRoleRelativePath.py: PT009 D100 D101 D102
+ test/TestRuleProperties.py: D100 D103
+ test/TestRulesCollection.py: D100 D103
+ test/TestRunner.py: D100 D103
+ test/TestShellWithoutPipefail.py: PT009 D100 D101 D102
+ test/TestSkipImportPlaybook.py: D100 D103
+ test/TestSkipInsideYaml.py: D100 D103
+ test/TestSkipPlaybookItems.py: D100 D103
+ test/TestSudoRule.py: PT009 D100 D101 D102
+ test/TestTaskHasName.py: PT009 D100 D101 D102
+ test/TestTaskIncludes.py: D100 D103
+ test/TestTaskNoLocalAction.py: PT009 D100 D101 D102
+ test/TestUseCommandInsteadOfShell.py: PT009 D100 D101 D102
+ test/TestUseHandlerRatherThanWhenChanged.py: PT009 D100 D101 D102
+ test/TestUsingBareVariablesIsDeprecated.py: PT009 D100 D101 D102
+ test/TestVariableHasSpaces.py: PT009 D100 D101 D102
+ test/TestWithSkipTagId.py: PT009 D100 D101 D102
+
+# flake8-pytest-style
+# PT001:
+pytest-fixture-no-parentheses = true
+# PT006:
+pytest-parametrize-names-type = tuple
+# PT007:
+pytest-parametrize-values-type = tuple
+pytest-parametrize-values-row-type = tuple
diff --git a/.git_archival.txt b/.git_archival.txt
new file mode 100644
index 0000000..5a4bffc
--- /dev/null
+++ b/.git_archival.txt
@@ -0,0 +1 @@
+ref-names: HEAD -> master, tag: v4.3.7
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..2e46433
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+# Force LF line endings for text files
+* text=auto eol=lf
+
+*.png binary
+
+# Needed for setuptools-scm-git-archive
+.git_archival.txt export-subst
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..0164155
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Community Code of Conduct
+
+Please see the official [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html).
diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst
new file mode 100644
index 0000000..1319470
--- /dev/null
+++ b/.github/CONTRIBUTING.rst
@@ -0,0 +1,79 @@
+Contributing to Ansible-lint
+============================
+
+To contribute to ansible-lint, please use pull requests on a branch
+of your own fork.
+
+After `creating your fork on GitHub`_, you can do:
+
+.. code-block:: shell-session
+
+ $ git clone git@github.com:yourname/ansible-lint
+ $ cd ansible-lint
+ $ git checkout -b your-branch-name
+ # DO SOME CODING HERE
+ $ git add your new files
+ $ git commit -v
+ $ git push origin your-branch-name
+
+You will then be able to create a pull request from your commit.
+
+All fixes to core functionality (i.e. anything except docs or examples)
+should be accompanied by tests that fail prior to your change and
+succeed afterwards.
+
+Feel free to raise issues in the repo if you feel unable to
+contribute a code fix.
+
+.. _creating your fork on GitHub:
+ https://guides.github.com/activities/forking/
+
+Standards
+---------
+
+ansible-lint is flake8 compliant with ``max-line-length`` set to 100
+(see `.flake8`_).
+
+ansible-lint works only with `supported Ansible versions`_ at the
+time it was released.
+
+Automated tests will be run against all PRs for flake8 compliance
+and Ansible compatibility — to check before pushing commits, just
+use `tox`_.
+
+.. _.flake8: https://github.com/ansible/ansible-lint/blob/master/.flake8
+.. _supported Ansible versions:
+ https://docs.ansible.com/ansible/devel/reference_appendices
+ /release_and_maintenance.html#release-status
+.. _tox: https://tox.readthedocs.io
+
+.. DO-NOT-REMOVE-deps-snippet-PLACEHOLDER
+
+Talk to us
+----------
+
+Discussion around ansible-lint happens in ``#ansible-galaxy`` IRC
+channel on Freenode and the `Ansible Development List`_.
+
+For the full list of Ansible IRC and Mailing list, please see the
+`Ansible Communication`_ page.
+Release announcements will be made to the `Ansible Announce`_ list.
+
+Possible security bugs should be reported via email
+to security@ansible.com.
+
+.. _Ansible Announce:
+ https://groups.google.com/forum/#!forum/ansible-announce
+.. _Ansible Development List:
+ https://groups.google.com/forum/#!forum/ansible-devel
+.. _Ansible Communication:
+ https://docs.ansible.com/ansible/latest/community/communication.html
+
+Code of Conduct
+---------------
+
+As with all Ansible projects, we have a `Code of Conduct`_.
+
+.. _Code of Conduct:
+ https://docs.ansible.com/ansible/latest/community
+ /code_of_conduct.html
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..e07e744
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,32 @@
+# Issue Type
+- Bug report
+- Feature request
+
+# Ansible and Ansible Lint details
+
+```
+ansible --version
+ansible-lint --version
+```
+
+- ansible installation method: one of source, pip, OS package
+- ansible-lint installation method: one of source, pip, OS package
+
+# Desired Behaviour
+
+Please give some details of the feature being requested
+or what should happen if providing a bug report
+
+Possible security bugs should be reported via email to `security@ansible.com`
+
+# Actual Behaviour (Bug report only)
+
+Please give some details of what is actually happening.
+Include a [minimum complete verifiable example] with:
+- playbook
+- output of running ansible-lint
+- if you're getting a stack trace, output of
+ `ansible-playbook --syntax-check playbook`
+
+
+[minimum complete verifiable example]: http://stackoverflow.com/help/mcve
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..3a96005
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,65 @@
+---
+name: 🐛 Bug report
+about: Create a bug report. Please test against the master branch before submitting it.
+labels: priority/medium, status/new, type/bug
+---
+<!--- Verify first that your issue is not already reported on GitHub -->
+<!--- Also test if the latest release and master branch are affected too -->
+
+##### Summary
+<!--- Explain the problem briefly below -->
+
+
+##### Issue Type
+
+- Bug Report
+
+##### Ansible and Ansible Lint details
+<!--- Paste verbatim output between tripple backticks -->
+```console (paste below)
+ansible --version
+
+ansible-lint --version
+
+```
+
+- ansible installation method: one of source, pip, OS package
+- ansible-lint installation method: one of source, pip, OS package
+
+##### OS / ENVIRONMENT
+<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
+
+
+##### STEPS TO REPRODUCE
+<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
+
+<!--- Paste example playbooks or commands between tripple backticks below -->
+```console (paste below)
+
+```
+
+<!--- HINT: You can paste gist.github.com links for larger files -->
+
+##### Desired Behaviour
+<!--- Describe what you expected to happen when running the steps above -->
+
+Possible security bugs should be reported via email to `security@ansible.com`
+
+##### Actual Behaviour
+<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
+
+Please give some details of what is actually happening.
+Include a [minimum complete verifiable example] with:
+- playbook
+- output of running ansible-lint
+- if you're getting a stack trace, output of
+ `ansible-playbook --syntax-check playbook`
+
+
+<!--- Paste verbatim command output between tripple backticks -->
+```paste below
+
+```
+
+
+[minimum complete verifiable example]: http://stackoverflow.com/help/mcve
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3f5190f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,16 @@
+# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
+blank_issues_enabled: false # default is true
+contact_links:
+- name: 🔐 Security bug report 🔥
+ url: https://docs.ansible.com/ansible/latest/community/reporting_bugs_and_features.html
+ about: |
+ Please learn how to report security vulnerabilities here.
+
+ For all security related bugs, email security@ansible.com
+ instead of using this issue tracker and you will receive
+ a prompt response.
+
+ For more information, see https://docs.ansible.com/ansible/latest/community/reporting_bugs_and_features.html
+- name: 📝 Ansible Code of Conduct
+ url: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
+ about: ❤ Be nice to other members of the community. ☮ Behave.
diff --git a/.github/ISSUE_TEMPLATE/documentation_report.md b/.github/ISSUE_TEMPLATE/documentation_report.md
new file mode 100644
index 0000000..1d1cfc1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation_report.md
@@ -0,0 +1,24 @@
+---
+name: 📝 Documentation Report
+about: Ask us about docs
+labels: priority/medium, status/new, documentation
+---
+<!--- Verify first that your improvement is not already reported on GitHub -->
+<!--- Also test if the latest release and master branch are affected too -->
+
+##### SUMMARY
+<!--- Explain the problem briefly below, add suggestions to wording or structure -->
+
+<!--- HINT: Did you know the documentation has an "Edit on GitHub" link on every page ? -->
+
+##### ISSUE TYPE
+
+- Documentation Report
+
+##### OS / ENVIRONMENT
+<!--- Provide all relevant information below, e.g. OS version, browser, etc. -->
+
+##### ADDITIONAL INFORMATION
+<!--- Describe how this improves the documentation, e.g. before/after situation or screenshots -->
+
+<!--- HINT: You can paste gist.github.com links for larger files -->
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..b02df01
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,24 @@
+---
+name: ✨ Feature request
+about: Suggest an idea for this project
+labels: priority/medium, status/new, type/enchancement
+---
+<!--- Verify first that your feature was not already discussed on GitHub -->
+
+##### Summary
+<!--- Describe the new feature/improvement briefly below -->
+
+
+##### Issue Type
+
+- Feature Idea
+
+##### Additional Information
+<!--- Describe how the feature would be used, why it is needed and what it would solve -->
+
+<!--- Paste example playbooks or commands between quotes below -->
+```console
+
+```
+
+<!--- HINT: You can also paste gist.github.com links for larger files -->
diff --git a/.github/SECURITY.rst b/.github/SECURITY.rst
new file mode 100644
index 0000000..b9190d8
--- /dev/null
+++ b/.github/SECURITY.rst
@@ -0,0 +1,17 @@
+Security Policy
+---------------
+
+Supported Versions
+==================
+
+Ansible applies security fixes according to the 3-versions-back support
+policy. Please find more information in `our docs
+<https://docs.ansible.com/ansible/devel/reference_appendices/release_and_maintenance.html#release-status>`_.
+
+Reporting a Vulnerability
+=========================
+
+We encourage responsible disclosure practices for security
+vulnerabilities. Please read our `policies for reporting bugs
+<https://docs.ansible.com/ansible/devel/community/reporting_bugs_and_features.html#reporting-a-bug>`_
+if you want to report a security issue that might affect Ansible.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..892b0dd
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+version: 2
+updates:
+- package-ecosystem: pip
+ directory: /docs
+ schedule:
+ day: sunday
+ interval: weekly
+ labels:
+ - dependabot-deps-updates
+ - skip-changelog
+ versioning-strategy: lockfile-only
+ open-pull-requests-limit: 3
+- package-ecosystem: pip
+ directory: /
+ schedule:
+ day: sunday
+ interval: weekly
+ labels:
+ - dependabot-deps-updates
+ - skip-changelog
+ versioning-strategy: lockfile-only
+ open-pull-requests-limit: 3
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
new file mode 100644
index 0000000..f457c5d
--- /dev/null
+++ b/.github/release-drafter.yml
@@ -0,0 +1,21 @@
+# Format and labels used aim to match those used by Ansible project
+categories:
+ - title: 'Major Changes'
+ labels:
+ - 'major' # c6476b
+ - title: 'Minor Changes'
+ labels:
+ - 'feature' # 006b75
+ - 'enhancement' # ededed
+ - title: 'Bugfixes'
+ labels:
+ - 'bug' # fbca04
+ - title: 'Deprecations'
+ labels:
+ - 'deprecated' # fef2c0
+exclude-labels:
+ - 'skip-changelog'
+template: |
+ ## Changes
+
+ $CHANGES
diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml
new file mode 100644
index 0000000..3966ef1
--- /dev/null
+++ b/.github/workflows/tox.yml
@@ -0,0 +1,296 @@
+name: gh
+
+on:
+ create: # is used for publishing to PyPI and TestPyPI
+ tags: # any tag regardless of its name, no branches
+ push: # only publishes pushes to the main branch to TestPyPI
+ branches: # any branch but not tag
+ - >-
+ **
+ tags-ignore:
+ - >-
+ **
+ pull_request:
+ schedule:
+ - cron: 1 0 * * * # Run daily at 0:01 UTC
+ # Run every Friday at 18:02 UTC
+ # https://crontab.guru/#2_18_*_*_5
+ # - cron: 2 18 * * 5
+
+jobs:
+ linters:
+ name: >-
+ ${{ matrix.env.TOXENV }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version:
+ - 3.8
+ os:
+ - ubuntu-latest
+ env:
+ - TOXENV: lint
+ - TOXENV: docs
+ - TOXENV: build-dists,metadata-validation
+ env:
+ TOX_PARALLEL_NO_SPINNER: 1
+
+ steps:
+ - uses: actions/checkout@master
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v1
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: set PY_SHA256
+ run: echo "::set-env name=PY_SHA256::$(python -VV | sha256sum | cut -d' ' -f1)"
+ - name: Pre-commit cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/pre-commit
+ key: ${{ runner.os }}-pre-commit-${{ env.PY_SHA256 }}-${{ hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('pytest.ini') }}
+ - name: Pip cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ env.PY_SHA256 }}-${{ hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('pytest.ini') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+ - name: Install tox
+ run: |
+ python -m pip install --upgrade tox
+ - name: Log installed dists
+ run: >-
+ python -m pip freeze --all
+ - name: >-
+ Initialize tox envs
+ run: >-
+ python -m
+ tox
+ --parallel auto
+ --parallel-live
+ --notest
+ --skip-missing-interpreters false
+ -vv
+ env: ${{ matrix.env }}
+ - name: Test with tox
+ run: |
+ python -m tox --parallel auto --parallel-live
+ env: ${{ matrix.env }}
+ - name: Archive logs
+ uses: actions/upload-artifact@v2
+ with:
+ name: logs.zip
+ path: .tox/**/log/
+
+ unit:
+ name: >-
+ py${{ matrix.python-version }}@${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ # fail-fast: false
+ # max-parallel: 5
+ # The matrix testing goal is to cover the *most likely* environments
+ # which are expected to be used by users in production. Avoid adding a
+ # combination unless there are good reasons to test it, like having
+ # proof that we failed to catch a bug by not running it. Using
+ # distribution should be prefferred instead of custom builds.
+ matrix:
+ python-version:
+ # keep list sorted as it determines UI order too
+ - 3.6
+ - 3.7
+ - 3.8
+ # NOTE: Installing ansible under 3.10-dev is currently not
+ # NOTE: possible because compiling cffi explodes.
+ os:
+ # https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners
+ - ubuntu-latest # 18.04
+ # - windows-latest
+ # - windows-2016
+ include:
+ - os: ubuntu-20.04
+ python-version: 3.9-dev
+ - os: macOS-latest
+ python-version: 3.6
+ - os: macOS-latest
+ python-version: 3.8
+
+ env:
+ TOX_PARALLEL_NO_SPINNER: 1
+
+ steps:
+ - uses: actions/checkout@master
+ - name: Get history and tags for SCM versioning to work
+ run: |
+ git fetch --prune --unshallow
+ git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+ - name: Set up stock Python ${{ matrix.python-version }} from GitHub
+ if: >-
+ !endsWith(matrix.python-version, '-dev')
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Set up Python ${{ matrix.python-version }} from deadsnakes
+ if: >-
+ endsWith(matrix.python-version, '-dev')
+ uses: deadsnakes/action@v1.0.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: >-
+ Log the currently selected Python
+ version info (${{ matrix.python-version }})
+ run: |
+ python --version --version
+ which python
+ - name: Pip cache
+ uses: actions/cache@v1
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ env.PY_SHA256 }}-${{ hashFiles('setup.cfg') }}-${{ hashFiles('tox.ini') }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('pytest.ini') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+ - name: Install tox
+ run: |
+ python -m pip install --upgrade tox
+ - name: Log installed dists
+ run: >-
+ python -m pip freeze --all
+ - name: >-
+ Initialize tox envs
+ run: >-
+ python -m
+ tox
+ --parallel auto
+ --parallel-live
+ --notest
+ --skip-missing-interpreters false
+ -vv
+ env:
+ TOXENV: ansible28,ansible29,ansible210,ansibledevel
+ - name: "Test with tox: ansible28"
+ run: |
+ python -m tox
+ env:
+ TOXENV: ansible28
+ # sequential run improves browsing experience (almost no speed impact)
+ - name: "Test with tox: ansible29"
+ run: |
+ python -m tox
+ env:
+ TOXENV: ansible29
+ - name: "Test with tox: ansible210"
+ run: |
+ python -m tox
+ env:
+ TOXENV: ansible210
+ - name: "Test with tox: ansibledevel"
+ run: |
+ python -m tox
+ env:
+ TOXENV: ansibledevel
+ - name: Archive logs
+ uses: actions/upload-artifact@v2
+ with:
+ name: logs.zip
+ path: .tox/**/log/
+ # https://github.com/actions/upload-artifact/issues/123
+ continue-on-error: true
+ - name: Report junit failures
+ uses: shyim/junit-report-annotations-action@3d2e5374f2b13e70f6f3209a21adfdbc42c466ae
+ with:
+ path: .tox/junit.*.xml
+ if: always()
+
+ publish:
+ name: Publish to PyPI registry
+ needs:
+ - linters
+ - unit
+ runs-on: ubuntu-latest
+
+ env:
+ PY_COLORS: 1
+ TOXENV: build-dists,metadata-validation
+ TOX_PARALLEL_NO_SPINNER: 1
+
+ steps:
+ - name: Switch to using Python 3.8 by default
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install tox
+ run: >-
+ python -m
+ pip install
+ --user
+ tox
+ - name: Check out src from Git
+ uses: actions/checkout@v2
+ with:
+ # Get shallow Git history (default) for tag creation events
+ # but have a complete clone for any other workflows.
+ # Both options fetch tags but since we're going to remove
+ # one from HEAD in non-create-tag workflows, we need full
+ # history for them.
+ fetch-depth: >-
+ ${{
+ (
+ github.event_name == 'create' &&
+ github.event.ref_type == 'tag'
+ ) &&
+ 1 || 0
+ }}
+ - name: Drop Git tags from HEAD for non-tag-create events
+ if: >-
+ github.event_name != 'create' ||
+ github.event.ref_type != 'tag'
+ run: >-
+ git tag --points-at HEAD
+ |
+ xargs git tag --delete
+ - name: Instruct setuptools-scm not to add a local version part
+ if: >-
+ github.event_name == 'push' &&
+ github.ref == format(
+ 'refs/heads/{0}', github.event.repository.default_branch
+ )
+ run: |
+ echo 'local_scheme = "no-local-version"' >> pyproject.toml
+ git update-index --assume-unchanged pyproject.toml
+ - name: Pre-populate tox env
+ run: >-
+ python -m
+ tox
+ --parallel auto
+ --parallel-live
+ --notest
+ --skip-missing-interpreters false
+ -vvvv
+ - name: Build dists
+ run: python -m tox -p auto --parallel-live -vvvv
+ - name: Publish to test.pypi.org
+ if: >-
+ (
+ github.event_name == 'push' &&
+ github.ref == format(
+ 'refs/heads/{0}', github.event.repository.default_branch
+ )
+ ) ||
+ (
+ github.event_name == 'create' &&
+ github.event.ref_type == 'tag'
+ )
+ uses: pypa/gh-action-pypi-publish@master
+ with:
+ password: ${{ secrets.testpypi_password }}
+ repository_url: https://test.pypi.org/legacy/
+ - name: Publish to pypi.org
+ if: >- # "create" workflows run separately from "push" & "pull_request"
+ github.event_name == 'create' &&
+ github.event.ref_type == 'tag'
+ uses: pypa/gh-action-pypi-publish@master
+ with:
+ password: ${{ secrets.pypi_password }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c542d2d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# Byte-compiled / optimized / DLL files
+__pycache__
+*.py[co]
+*$py.class
+
+# Packages
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib64/
+parts/
+pip-wheel-metadata
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.tox
+
+# Needed for CLI tests
+.sandbox
+
+# pyenv
+.python-version
+
+# Coverage artifacts
+.coverage
+coverage.xml
+pip-wheel-metadata
+.test-results/
+
+# mypy
+.mypy_cache
+
+# .cache is used by progressive mode
+.cache
diff --git a/.isort.cfg b/.isort.cfg
new file mode 100644
index 0000000..1ad6bcc
--- /dev/null
+++ b/.isort.cfg
@@ -0,0 +1,9 @@
+[settings]
+include_trailing_comma = true
+known_first_party = ansiblelint
+known_third_party = ansible,pytest,ruamel,setuptools,sphinx,yaml
+# picked to match the current flake8 value:
+line_length = 100
+# https://github.com/timothycrosley/isort#multi-line-output-modes
+multi_line_output = 5
+use_parentheses = true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..2cac471
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,71 @@
+---
+repos:
+- repo: local
+ hooks:
+ - id: immutable-setup-py
+ name: Verify that setup.py stays immutable
+ description: >-
+ This is a sanity check that makes sure that
+ the `setup.py` file isn't changed.
+ # Using Python here because using
+ # shell test does not seem to work in CIs:
+ entry: >-
+ sh -c 'git hash-object setup.py
+ |
+ python -c raise\ SystemExit\(input\(\)\ !=\ \"b72e95ce049c4c67c6487a2171fec8d1b0b958b1\"\)
+ '
+ pass_filenames: false
+ language: system
+ files: >-
+ ^setup\.py$
+- repo: https://github.com/pre-commit/pre-commit-hooks.git
+ rev: v3.1.0
+ hooks:
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ exclude: >
+ (?x)^(
+ test/(with-skip-tag-id|unicode).yml|
+ examples/example.yml
+ )$
+ - id: mixed-line-ending
+ - id: check-byte-order-marker
+ - id: check-executables-have-shebangs
+ - id: check-merge-conflict
+ - id: debug-statements
+ language_version: python3
+- repo: https://github.com/adrienverge/yamllint.git
+ rev: v1.24.2
+ hooks:
+ - id: yamllint
+ files: \.(yaml|yml)$
+ types: [file, yaml]
+ entry: yamllint --strict
+- repo: https://github.com/pre-commit/mirrors-isort
+ rev: v5.1.4
+ hooks:
+ - id: isort
+ args:
+ # https://github.com/pre-commit/mirrors-isort/issues/9#issuecomment-624404082
+ - --filter-files
+- repo: https://gitlab.com/pycqa/flake8.git
+ rev: 3.8.3
+ hooks:
+ - id: flake8
+ language_version: python3
+ additional_dependencies:
+ - flake8-2020>=1.6.0
+ - flake8-docstrings>=1.5.0
+ - flake8-pytest-style>=1.2.2
+- repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v0.782
+ hooks:
+ - id: mypy
+ # empty args needed in order to match mypy cli behavior
+ args: []
+ additional_dependencies:
+ - Sphinx>=3.1.2
+- repo: https://github.com/pre-commit/mirrors-pylint
+ rev: v2.5.3
+ hooks:
+ - id: pylint
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..d917473
--- /dev/null
+++ b/.pre-commit-hooks.yaml
@@ -0,0 +1,14 @@
+---
+
+# For use with pre-commit.
+# See usage instructions at http://pre-commit.com
+
+- id: ansible-lint
+ name: Ansible-lint
+ description: This hook runs ansible-lint.
+ entry: ansible-lint --force-color
+ language: python
+ # do not pass files to ansible-lint, see:
+ # https://github.com/ansible/ansible-lint/issues/611
+ pass_filenames: false
+ always_run: true
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..a7881e0
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,37 @@
+[IMPORTS]
+preferred-modules =
+ unittest:pytest,
+
+[MESSAGES CONTROL]
+
+disable =
+ # TODO(ssbarnea): remove temporary skips adding during initial adoption:
+ bad-continuation,
+ broad-except,
+ consider-using-in,
+ dangerous-default-value,
+ duplicate-code,
+ fixme,
+ import-error,
+ inconsistent-return-statements,
+ invalid-name,
+ missing-class-docstring,
+ missing-function-docstring,
+ missing-module-docstring,
+ no-else-continue,
+ no-else-return,
+ no-member,
+ no-self-use,
+ not-callable,
+ protected-access,
+ redefined-builtin,
+ redefined-outer-name,
+ too-few-public-methods,
+ too-many-arguments,
+ too-many-branches,
+ too-many-instance-attributes,
+ too-many-return-statements,
+ unused-argument,
+ unused-variable,
+ useless-object-inheritance,
+ wrong-import-order,
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..bb33e2e
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,41 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html
+# for details
+
+---
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ builder: html
+ configuration: docs/conf.py
+ fail_on_warning: true
+
+# Build documentation with MkDocs
+#mkdocs:
+# configuration: mkdocs.yml
+# fail_on_warning: true
+
+# Optionally build your docs in additional formats
+# such as PDF and ePub
+formats: []
+
+submodules:
+ include: all # []
+ exclude: []
+ recursive: true
+
+build:
+ image: latest
+
+# Optionally set the version of Python and requirements required
+# to build docs
+python:
+ version: 3.8
+ install:
+ - method: pip
+ path: .
+ - requirements: docs/requirements.txt
+ system_packages: false
diff --git a/.yamllint b/.yamllint
new file mode 100644
index 0000000..c7a1f61
--- /dev/null
+++ b/.yamllint
@@ -0,0 +1,4 @@
+rules:
+ indentation:
+ level: error
+ indent-sequences: consistent
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..661e007
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,188 @@
+Current changes can now be accessed from `github releases <https://github.com/ansible/ansible-lint/releases/>`_.
+
+4.3.0 - Released 2020-08-17
+===========================
+
+Major Changes:
+
+* Require Python 3.6 or newer (#775) @ssbarnea
+* Require Ansible 2.8 or newer (#721) @ssbarnea
+* LRU Cache for frequently called functions (#891) @ragne
+* Change documentation website to RTD (#875) @ssbarnea
+* Add rules for verifying the existence of imported and included files (#691) @jlusiardi
+* Add a new rule for detecting nested jinja mustache syntax (#686) @europ
+
+Minor Changes:
+
+* Refactored import_playbook tests (#951) @ssbarnea
+* Added MissingFilePermissionsRule (#943) @ssbarnea
+* Enable github actions parsable format (#926) @ssbarnea
+* Add linter branding for docs (#914) @ssbarnea
+* Assure we do not produce duplicated matches (#912) @ssbarnea
+* Enable annotations on failed tests (#910) @ssbarnea
+* Refactor `_taskshandlers_children` complexity (#903) @webknjaz
+* Make import sections consistent (#897) @ssbarnea
+* Allow backticks in shell commands (#894) @turettn
+* Add ansible210 testing (#888) @ssbarnea
+* Enable isort (#887) @ssbarnea
+* Combine MatchError into Match (#884) @ssbarnea
+* Improve MatchError class (#881) @ssbarnea
+* Expose package version (#867) @ssbarnea
+* Replace custom theme with sphinx-ansible-theme (#856) @ssbarnea
+* Improve unjinja function (#853) @ssbarnea
+* Refactor MetaMainHasInfoRule (#846) @ssato
+* Remove dependency on ansible.utils.color (#833) @ssbarnea
+* Moved exit codes to constants (#821) @ssbarnea
+* Document module dependencies (#817) @ssbarnea
+* Refactor Runner out of __init__ (#816) @ssbarnea
+* Added reproducer for become in blocks (#793) @ssbarnea
+* Convert failed to find required 'name' key in include_role into a match (#781) @ssbarnea
+* Fix exclude_paths from get_playbooks_and_roles (#774) @ssbarnea
+* Update ComparisonToEmptyStringRule.py (#770) @vbotka
+* Remove bin/ansible-lint script (#762) @ssbarnea
+* Fix logging configuration (#757) @ssbarnea
+* Allow returning line number in matchplay (#756) @albinvass
+* Update cli output on README (#754) @ssbarnea
+* Migrate some test to pytest (#740) @cans
+* Use python logging (#732) @ssbarnea
+* Make config loading failures visible (#726) @ssbarnea
+* Add a test that fails with `AttributeError` on malformed `import_tasks` file content (#720) @mdaniel
+* Consistent relative path display (#692) @cans
+
+Bugfixes:
+
+* E501: Add become_user and become inheritance (#964) @Tompage1994
+* Add missing hosts to test files (#952) @ssbarnea
+* E208: Improve MissingFilePermissionsRule detection (#949) @ssbarnea
+* Make pre-commit hook use auto-detect mode (#932) @ssbarnea
+* Fix severity formatter wrong use of color (#919) @ssbarnea
+* Avoid displaying Null with missing filenames (#918) @ssbarnea
+* Include contributing inside docs (#905) @ssbarnea
+* Fix spelling mistakes in documentation (#901) @MorganGeek
+* Avoid failure with playbooks having tasks key a null value (#899) @ssbarnea
+* Fix `MatchError` comparison fallback implementation (#896) @webknjaz
+* Avoid sorting failure with matches without an id (#879) @ssbarnea
+* Fix broken always_run rule on Ansible 2.10 (#878) @ssbarnea
+* Allow null config file (#814) @ssbarnea
+* Fixed the search method when the file path not exists (#807) @cahlchang
+* Restore playbook auto-detection (#767) @ssbarnea
+* Gracefully process a missing git binary when falling-back to pure-python discovery (#738) @anryko
+* Resurrect support for editable mode installs (#722) @webknjaz
+* Avoid exception from 505 rule (#709) @ssbarnea
+
+4.2.0 - Released 2019-12-04
+===========================
+
+Features:
+
+- Enable ansible-lint to auto-detect roles/playbooks `#615 <https://github.com/ansible/ansible-lint/pull/615>`_
+- Normalize displayed file paths `#620 <https://github.com/ansible/ansible-lint/pull/620>`_
+
+Bugfixes:
+
+- Fix role detection to include tasks/main.yml `#631 <https://github.com/ansible/ansible-lint/pull/631>`_
+- Fix pre-commit hooks `#612 <https://github.com/ansible/ansible-lint/pull/612>`_
+- Ensure variable syntax before matching in VariableHasSpacesRule `#535 <https://github.com/ansible/ansible-lint/pull/535>`_
+- Fix false positive with multiline template in UsingBareVariablesIsDeprecatedRule `#251 <https://github.com/ansible/ansible-lint/pull/251>`_
+- Fix role metadata checks when they include unexpected types `#533 <https://github.com/ansible/ansible-lint/pull/533>`_ `#513 <https://github.com/ansible/ansible-lint/pull/513>`_
+- Support inline rule skipping inside a block `#528 <https://github.com/ansible/ansible-lint/pull/528>`_
+- Look for noqa skips in handlers, pre_tasks, and post_tasks `#520 <https://github.com/ansible/ansible-lint/pull/520>`_
+- Fix skipping when using import_playbook `#517 <https://github.com/ansible/ansible-lint/pull/517>`_
+- Fix parsing inline args for import_role and include_role `#511 <https://github.com/ansible/ansible-lint/pull/511>`_
+- Fix syntax proposed by 104 to not fail 206 `#501 <https://github.com/ansible/ansible-lint/pull/501>`_
+- Fix VariableHasSpacesRule false positive for whitespace control chars in vars `#500 <https://github.com/ansible/ansible-lint/pull/500>`_
+
+Docs/Misc:
+
+- Disable docs build on macos with py38 `#630 <https://github.com/ansible/ansible-lint/pull/630>`_
+- Update dependencies and CI to supported versions of ansible `#530 <https://github.com/ansible/ansible-lint/pull/530>`_
+- Declare support for Python 3.8 `#601 <https://github.com/ansible/ansible-lint/pull/601>`_
+
+Dev/Contributor:
+
+- Enable flake-docstrings to check for pep257 `#621 <https://github.com/ansible/ansible-lint/pull/621>`_
+- Remove code related to unsupported ansible versions before 2.4 `#622 <https://github.com/ansible/ansible-lint/pull/622>`_
+- Replace nosetests with pytest `#604 <https://github.com/ansible/ansible-lint/pull/604>`_
+- Support newer setuptools and require 34.0.0 or later `#591 <https://github.com/ansible/ansible-lint/pull/591>`_ `#600 <https://github.com/ansible/ansible-lint/pull/600>`_
+- Added SSL proxy variables to tox passenv `#593 <https://github.com/ansible/ansible-lint/pull/593>`_
+- Have RunFromText test helper use named files for playbooks `#519 <https://github.com/ansible/ansible-lint/pull/519>`_
+- Fully depend on Pip having PEP 517 implementation `#607 <https://github.com/ansible/ansible-lint/pull/607>`_
+- Fixed metadata and travis deployment `#598 <https://github.com/ansible/ansible-lint/pull/598>`_
+
+4.1.0 - Released 11-Feb-2019
+============================
+
+- Support skipping specific rule(s) for a specific task `#460 <https://github.com/ansible/ansible-lint/pull/460>`_
+- Lint all yaml in tasks/ and handlers/ regardless of import or include `#462 <https://github.com/ansible/ansible-lint/pull/462>`_
+- New rule: shell task uses pipeline without pipefail `#199 <https://github.com/ansible/ansible-lint/pull/199>`_
+- Remove rule 405 checking for retry on package modules `#465 <https://github.com/ansible/ansible-lint/pull/465>`_
+- Limit env var check to command, not shell `#477 <https://github.com/ansible/ansible-lint/pull/477>`_
+- Extend max line length rule from 120 to 160 `#474 <https://github.com/ansible/ansible-lint/pull/474>`_
+- Do not flag octal file mode permission when it is a string `#480 <https://github.com/ansible/ansible-lint/pull/480>`_
+- Check ANSIBLE_ROLES_PATH before basedir `#478 <https://github.com/ansible/ansible-lint/pull/478>`_
+- Fix crash on indexing empty cmd arguments `#473 <https://github.com/ansible/ansible-lint/pull/473>`_
+- Handle argv syntax for the command module `#424 <https://github.com/ansible/ansible-lint/pull/424>`_
+- Add another possible license default with SPDX `#472 <https://github.com/ansible/ansible-lint/pull/472>`_
+- Ignore comments for line-based rules `#453 <https://github.com/ansible/ansible-lint/pull/453>`_
+- Allow config skip_list to have rule number id not in quotes `#463 <https://github.com/ansible/ansible-lint/pull/463>`_
+
+4.0.1 - Released 04-Jan-2019
+============================
+
+Bugfix release
+
+- Allow install with python35 and add to tox testing `#452 <https://github.com/ansible/ansible-lint/pull/452>`_
+- Fix 503 UseHandlerRatherThanWhenChangedRule attempt to iterate on bool `#455 <https://github.com/ansible/ansible-lint/pull/455>`_
+- Improve regex on rule 602 `#454 <https://github.com/ansible/ansible-lint/pull/454>`_
+- Refactor RoleRelativePathRule, fix keyerror `#446 <https://github.com/ansible/ansible-lint/pull/446>`_
+- Rule 405 now ignore case of 'yum: list=package' `#444 <https://github.com/ansible/ansible-lint/pull/444>`_
+- Allow jinja escaping in variables `#440 <https://github.com/ansible/ansible-lint/pull/440>`_
+
+4.0.0 - Released 18-Dec-2018
+============================
+
+* New documentation site `docs.ansible.com/ansible-lint <https://docs.ansible.com/ansible-lint/>`_
+* Additional default rules for ansible-lint, listed in `docsite default rules <https://docs.ansible.com/ansible-lint/rules/default_rules.html>`_
+* Fixed running with role path containing single or multiple dirs #390
+* Fixed double sudo rule output #393
+* Severity property added to rules to be used by Galaxy #379
+* Packaging: consistency and automation #389
+* Updated rule TrailingWhitespaceRule.py to remove carriage return char #323
+* Allow snake_case module names for rules #82
+* Suggest tempfile module instead of mktemp command #422
+* Update tox to run with only supported ansible versions #406
+* GitHub repository edits: move to ansible org, add CODE_OF_CONDUCT, add ROADMAP, label edits
+
+3.5.1
+=====
+
+Use ``yaml.safe_load`` for loading the configuration file
+
+3.5.0
+=====
+
+* New ids and tags, add doc generator. Old tag names remain backwardly compatible (awcrosby)
+* Add more package formats to PackageIsNotLatestRule (simon04)
+* Improve handling of meta/main.yml dependencies (MatrixCrawler)
+* Correctly handle role argument trailing slash (zoredache)
+* Handle ``include_task`` and ``import_task`` (zeot)
+* Add a new rule to detect jinja in when clauses (greg-hellings)
+* Suggest ``replace`` as another alternative to ``sed`` (inponomarev)
+* YAML syntax highlighting for false positives (gundalow)
+
+3.4.23
+======
+
+Fix bug with using comma-separated ``skip_list`` arguments
+
+3.4.22
+======
+
+* Allow ``include_role`` and ``import_role`` (willthames)
+* Support arbitrary number of exclude flags (KellerFuchs)
+* Fix task has name check for empty name fields (ekeih)
+* Allow vault encrypted variables in YAML files (mozz)
+* Octal permission check improvements - readability, test
+ coverage and bug fixes (willthames)
+* Fix very weird bug with line numbers in some test environments (kouk)
+* Python 3 fixes for octal literals in tests (willthames)
diff --git a/DCO_1_1.md b/DCO_1_1.md
new file mode 100644
index 0000000..1c497a0
--- /dev/null
+++ b/DCO_1_1.md
@@ -0,0 +1,45 @@
+DCO
+===
+
+All contributors must use `git commit --signoff` for any
+commit to be merged, and agree that usage of --signoff constitutes
+agreement with the terms of DCO 1.1, which appears below:
+
+```
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+```
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4b48ba2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2013-2018 Will Thames <will@thames.id.au>
+Copyright (c) 2018 Ansible by Red Hat
+
+
+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.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..0e061fa
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,640 @@
+.. image:: https://img.shields.io/pypi/v/ansible-lint.svg
+ :target: https://pypi.org/project/ansible-lint
+ :alt: PyPI version
+
+.. image:: https://img.shields.io/badge/Ansible--lint-rules%20table-blue.svg
+ :target: https://ansible-lint.readthedocs.io/en/latest/default_rules.html
+ :alt: Ansible-lint rules explanation
+
+.. image:: https://img.shields.io/badge/Code%20of%20Conduct-black.svg
+ :target: https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
+ :alt: Ansible Code of Conduct
+
+.. image:: https://img.shields.io/badge/Mailing%20lists-Ansible-orange.svg
+ :target: https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information
+ :alt: Ansible mailing lists
+
+.. image:: https://github.com/ansible/ansible-lint/workflows/gh/badge.svg
+ :target: https://github.com/ansible/ansible-lint/actions?query=workflow%3Agh+branch%3Amaster+event%3Apush
+ :alt: GitHub Actions CI/CD
+
+.. image:: https://img.shields.io/lgtm/grade/python/g/ansible/ansible-lint.svg?logo=lgtm&logoWidth=18
+ :target: https://lgtm.com/projects/g/ansible/ansible-lint/context:python
+ :alt: Language grade: Python
+
+.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white
+ :target: https://github.com/pre-commit/pre-commit
+ :alt: pre-commit
+
+
+Ansible-lint
+============
+
+``ansible-lint`` checks playbooks for practices and behaviour that could
+potentially be improved. As a community backed project ansible-lint supports
+only the last two major versions of Ansible.
+
+`Visit the Ansible Lint docs site <https://ansible-lint.readthedocs.io/en/latest/>`_
+
+Installing
+==========
+
+.. installing-docs-inclusion-marker-do-not-remove
+
+Installing on Windows is not supported because we use symlinks inside Python packages.
+
+Using Pip
+---------
+
+.. code-block:: bash
+
+ pip install ansible-lint
+
+.. _installing_from_source:
+
+From Source
+-----------
+
+**Note**: pip 19.0+ is required for installation. Please consult with the `PyPA User Guide`_
+to learn more about managing Pip versions.
+
+.. code-block:: bash
+
+ pip install git+https://github.com/ansible/ansible-lint.git
+
+.. _PyPA User Guide: https://packaging.python.org/tutorials/installing-packages/#ensure-pip-setuptools-and-wheel-are-up-to-date
+
+.. installing-docs-inclusion-marker-end-do-not-remove
+
+Usage
+=====
+
+.. usage-docs-inclusion-marker-do-not-remove
+
+Command Line Options
+--------------------
+
+The following is the output from ``ansible-lint --help``, providing an overview of the basic command line options:
+
+.. code-block::
+
+ usage: ansible-lint [-h] [-L] [-f {rich,plain,rst}] [-q] [-p] [--parseable-severity] [-r RULESDIR]
+ [-R] [--show-relpath] [-t TAGS] [-T] [-v] [-x SKIP_LIST]
+ [-w WARN_LIST [WARN_LIST ...]] [--nocolor] [--force-color]
+ [--exclude EXCLUDE_PATHS] [-c CONFIG_FILE] [--version]
+ [playbook [playbook ...]]
+
+ positional arguments:
+ playbook One or more files or paths. When missing it will enable auto-detection mode.
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -L list all the rules
+ -f {rich,plain,rst} Format used rules output, (default: rich)
+ -q quieter, although not silent output
+ -p parseable output in the format of pep8
+ --parseable-severity parseable output including severity of rule
+ --progressive Return success if it detects a reduction in number of violations compared with
+ previous git commit. This feature works only on git repository clones.
+ -r RULESDIR Specify custom rule directories. Add -R to keep using embedded rules from
+ /usr/local/lib/python3.8/site-packages/ansiblelint/rules
+ -R Keep default rules when using -r
+ --show-relpath Display path relative to CWD
+ -t TAGS only check rules whose id/tags match these values
+ -T list all the tags
+ -v Increase verbosity level
+ -x SKIP_LIST only check rules whose id/tags do not match these values
+ -w WARN_LIST [WARN_LIST ...]
+ only warn about these rules
+ --nocolor disable colored output
+ --force-color Try force colored output (relying on ansible's code)
+ --exclude EXCLUDE_PATHS
+ path to directories or files to skip. This option is repeatable.
+ -c CONFIG_FILE Specify configuration file to use. Defaults to ".ansible-lint"
+ --version show program's version number and exit
+
+Progressive mode
+----------------
+
+In order to ease tool adoption, git users can enable the progressive mode using
+``--progressive`` option. This makes the linter return a success even if
+some failures are found, as long the total number of violations did not
+increase since the previous commit.
+
+As expected, this mode makes the linter run twice if it finds any violations.
+The second run is performed against a temporary git working copy that contains
+the previous commit. All the violations that were already present are removed
+from the list and the final result is displayed.
+
+The most notable benefit introduced by this mode it does not prevent merging
+new code while allowing developer to address historical violation at his own
+speed.
+
+CI/CD
+-----
+
+If execution under `Github Actions`_ is detected via the presence of
+``GITHUB_ACTIONS=true`` and ``GITHUB_WORFLOW=...`` variables, the linter will
+also print errors using their `annotation`_ format.
+
+.. _GitHub Actions: https://github.com/features/actions
+.. _annotation: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
+
+Linting Playbooks and Roles
+---------------------------
+
+It's important to note that ``ansible-lint`` accepts a list of Ansible playbook files or a list of role directories. Starting from a directory that contains the following, the playbook file, ``playbook.yml``, or one of the role subdirectories, such as ``geerlingguy.apache``, can be passed:
+
+.. code-block::
+
+ playbook.yml
+ roles/
+ geerlingguy.apache/
+ tasks/
+ handlers/
+ files/
+ templates/
+ vars/
+ defaults/
+ meta/
+ geerlingguy.elasticsearch/
+ tasks/
+ handlers/
+ files/
+ templates/
+ vars/
+ defaults/
+ meta/
+
+The following lints the role ``geerlingguy.apache``:
+
+.. code-block::
+
+ $ ansible-lint geerlingguy.apache
+
+ [305] Use shell only when shell functionality is required
+ /Users/chouseknecht/.ansible/roles/geerlingguy.apache/tasks/main.yml:19
+ Task/Handler: Get installed version of Apache.
+
+ [502] All tasks should be named
+ /Users/chouseknecht/.ansible/roles/geerlingguy.apache/tasks/main.yml:29
+ Task/Handler: include_vars apache-22.yml
+
+ [502] All tasks should be named
+ /Users/chouseknecht/.ansible/roles/geerlingguy.apache/tasks/main.yml:32
+ Task/Handler: include_vars apache-24.yml
+
+Here's the contents of ``playbook.yml``, which references multiples roles:
+
+.. code-block:: yaml
+
+ - name: Lint multiple roles
+ hosts: all
+ tasks:
+
+ - include_role:
+ name: geerlingguy.apache
+
+ - include_role:
+ name: geerlingguy.elasticsearch
+
+The following lints ``playbook.yml``, which evaluates both the playbook and the referenced roles:
+
+.. code-block::
+
+ $ ansible-lint playbook.yml
+
+ [305] Use shell only when shell functionality is required
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:19
+ Task/Handler: Get installed version of Apache.
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:29
+ Task/Handler: include_vars apache-22.yml
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:32
+ Task/Handler: include_vars apache-24.yml
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.elasticsearch/tasks/main.yml:17
+ Task/Handler: service state=started name=elasticsearch enabled=yes
+
+Since ``ansible-lint`` accepts a list of roles or playbooks, the following works as well, producing the same output as the example above:
+
+.. code-block::
+
+ $ ansible-lint geerlingguy.apache geerlingguy.elasticsearch
+
+ [305] Use shell only when shell functionality is required
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:19
+ Task/Handler: Get installed version of Apache.
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:29
+ Task/Handler: include_vars apache-22.yml
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.apache/tasks/main.yml:32
+ Task/Handler: include_vars apache-24.yml
+
+ [502] All tasks should be named
+ /Users/chouseknecht/roles/geerlingguy.elasticsearch/tasks/main.yml:17
+ Task/Handler: service state=started name=elasticsearch enabled=yes
+
+Examples
+--------
+
+Included in ``ansible-lint/examples`` are some example playbooks with undesirable features. Running ansible-lint on them works, as demonstrated in the following:
+
+.. code-block::
+
+ $ ansible-lint examples/example.yml
+
+ [301] Commands should not change things if nothing needs doing
+ examples/example.yml:9
+ Task/Handler: unset variable
+
+ [206] Variables should have spaces before and after: {{ var_name }}
+ examples/example.yml:10
+ action: command echo {{thisvariable}} is not set in this playbook
+
+ [301] Commands should not change things if nothing needs doing
+ examples/example.yml:12
+ Task/Handler: trailing whitespace
+
+ [201] Trailing whitespace
+ examples/example.yml:13
+ action: command echo do nothing
+
+ [401] Git checkouts must contain explicit version
+ examples/example.yml:15
+ Task/Handler: git check
+
+ [401] Git checkouts must contain explicit version
+ examples/example.yml:18
+ Task/Handler: git check 2
+
+ [301] Commands should not change things if nothing needs doing
+ examples/example.yml:24
+ Task/Handler: executing git through command
+
+ [303] git used in place of git module
+ examples/example.yml:24
+ Task/Handler: executing git through command
+
+ [303] git used in place of git module
+ examples/example.yml:27
+ Task/Handler: executing git through command
+
+ [401] Git checkouts must contain explicit version
+ examples/example.yml:30
+ Task/Handler: using git module
+
+ [206] Variables should have spaces before and after: {{ var_name }}
+ examples/example.yml:34
+ action: debug msg="{{item}}"
+
+ [201] Trailing whitespace
+ examples/example.yml:35
+ with_items:
+
+ [403] Package installs should not use latest
+ examples/example.yml:39
+ Task/Handler: yum latest
+
+ [403] Package installs should not use latest
+ examples/example.yml:44
+ Task/Handler: apt latest
+
+ [101] Deprecated always_run
+ examples/example.yml:47
+ Task/Handler: always run
+
+
+If playbooks include other playbooks, or tasks, or handlers or roles, these are also handled:
+
+.. code-block::
+
+ $ ansible-lint examples/include.yml
+
+ [301] Commands should not change things if nothing needs doing
+ examples/play.yml:5
+ Task/Handler: a bad play
+
+ [303] service used in place of service module
+ examples/play.yml:5
+ Task/Handler: a bad play
+
+ [401] Git checkouts must contain explicit version
+ examples/roles/bobbins/tasks/main.yml:2
+ Task/Handler: test tasks
+
+ [701] No 'galaxy_info' found
+ examples/roles/hello/meta/main.yml:1
+ {'meta/main.yml': {'dependencies': [{'role': 'bobbins', '__line__': 3, '__file__': '/Users/akx/build/ansible-lint/examples/roles/hello/meta/main.yml'}], '__line__': 1, '__file__': '/Users/akx/build/ansible-lint/examples/roles/hello/meta/main.yml', 'skipped_rules': []}}
+
+ [303] service used in place of service module
+ examples/roles/morecomplex/handlers/main.yml:1
+ Task/Handler: restart service using command
+
+ [301] Commands should not change things if nothing needs doing
+ examples/roles/morecomplex/tasks/main.yml:1
+ Task/Handler: test bad command
+
+ [302] mkdir used in place of argument state=directory to file module
+ examples/roles/morecomplex/tasks/main.yml:1
+ Task/Handler: test bad command
+
+ [301] Commands should not change things if nothing needs doing
+ examples/roles/morecomplex/tasks/main.yml:4
+ Task/Handler: test bad command v2
+
+ [302] mkdir used in place of argument state=directory to file module
+ examples/roles/morecomplex/tasks/main.yml:4
+ Task/Handler: test bad command v2
+
+ [301] Commands should not change things if nothing needs doing
+ examples/roles/morecomplex/tasks/main.yml:7
+ Task/Handler: test bad local command
+
+ [305] Use shell only when shell functionality is required
+ examples/roles/morecomplex/tasks/main.yml:7
+ Task/Handler: test bad local command
+
+ [504] Do not use 'local_action', use 'delegate_to: localhost'
+ examples/roles/morecomplex/tasks/main.yml:8
+ local_action: shell touch foo
+
+ [201] Trailing whitespace
+ examples/tasks/x.yml:3
+ args:
+
+ [201] Trailing whitespace
+ examples/tasks/x.yml:3
+ args:
+
+.. usage-docs-inclusion-marker-end-do-not-remove
+
+Configuring
+===========
+
+.. configuring-docs-inclusion-marker-do-not-remove
+
+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.
+
+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**.
+
+The following values are supported, and function identically to their CLI counterparts:
+
+.. code-block:: 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
+ - skip_this_id
+ - '401'
+ tags:
+ - run_this_tag
+ use_default_rules: true
+ verbosity: 1
+
+
+Pre-commit Setup
+----------------
+
+To use ansible-lint with `pre-commit`_, just add the following to your local repo's ``.pre-commit-config.yaml`` file. Make sure to change **rev:** to be either a git commit sha or tag of ansible-lint containing ``hooks.yaml``.
+
+.. code-block:: yaml
+
+ - repo: https://github.com/ansible/ansible-lint.git
+ rev: v4.1.0
+ hooks:
+ - id: ansible-lint
+ files: \.(yaml|yml)$
+
+.. _pre-commit: https://pre-commit.com
+
+.. configuring-docs-inclusion-marker-end-do-not-remove
+
+Rules
+=====
+
+.. rules-docs-inclusion-marker-do-not-remove
+
+Specifying Rules at Runtime
+---------------------------
+
+By default, ``ansible-lint`` uses the rules found in ``ansible-lint/lib/ansiblelint/rules``. To override this behavior and use a custom set of rules, use the ``-r /path/to/custom-rules`` option to provide a directory path containing the custom rules. For multiple rule sets, pass multiple ``-r`` options.
+
+It's also possible to use the default rules, plus custom rules. This can be done by passing the ``-R`` to indicate that the default rules are to be used, along with one or more ``-r`` options.
+
+Using Tags to Include Rules
+```````````````````````````
+
+Each rule has an associated set of one or more tags. To view the list of tags for each available rule, use the ``-T`` option.
+
+The following shows the available tags in an example set of rules, and the rules associated with each tag:
+
+.. code-block:: bash
+
+ $ ansible-lint -v -T
+
+ behaviour ['[503]']
+ bug ['[304]']
+ command-shell ['[305]', '[302]', '[304]', '[306]', '[301]', '[303]']
+ deprecated ['[105]', '[104]', '[103]', '[101]', '[102]']
+ formatting ['[104]', '[203]', '[201]', '[204]', '[206]', '[205]', '[202]']
+ idempotency ['[301]']
+ idiom ['[601]', '[602]']
+ metadata ['[701]', '[704]', '[703]', '[702]']
+ module ['[404]', '[401]', '[403]', '[402]']
+ oddity ['[501]']
+ readability ['[502]']
+ repeatability ['[401]', '[403]', '[402]']
+ resources ['[302]', '[303]']
+ safety ['[305]']
+ task ['[502]', '[503]', '[504]', '[501]']
+
+To run just the *idempotency* rules, for example, run the following:
+
+.. code-block:: bash
+
+ $ ansible-lint -t idempotency playbook.yml
+
+Excluding Rules
+```````````````
+
+To exclude rules from the available set of rules, use the ``-x SKIP_LIST`` option. For example, the following runs all of the rules except those with the tags *readability* and *safety*:
+
+.. code-block:: bash
+
+ $ ansible-lint -x readability,safety playbook.yml
+
+It's also possible to skip specific rules by passing the rule ID. For example, the following excludes rule *502*:
+
+.. code-block:: bash
+
+ $ ansible-lint -x 502 playbook.yml
+
+False Positives: Skipping Rules
+-------------------------------
+
+Some rules are a bit of a rule of thumb. Advanced *git*, *yum* or *apt* usage, for example, is typically difficult to achieve through the modules. In this case, you should mark the task so that warnings aren't produced.
+
+To skip a specific rule for a specific task, inside your ansible yaml add ``# noqa [rule_id]`` at the end of the line. If the rule is task-based (most are), add at the end of any line in the task. You can skip multiple rules via a space-separated list.
+
+.. code-block:: yaml
+
+ - name: this would typically fire GitHasVersionRule 401 and BecomeUserWithoutBecomeRule 501
+ become_user: alice # noqa 401 501
+ git: src=/path/to/git/repo dest=checkout
+
+If the rule is line-based, ``# noqa [rule_id]`` must be at the end of the particular line to be skipped
+
+.. code-block:: yaml
+
+ - name: this would typically fire LineTooLongRule 204 and VariableHasSpacesRule 206
+ get_url:
+ url: http://example.com/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/file.conf # noqa 204
+ dest: "{{dest_proj_path}}/foo.conf" # noqa 206
+
+
+It's also a good practice to comment the reasons why a task is being skipped.
+
+If you want skip running a rule entirely, you can use either use ``-x`` command
+line argument, or add it to ``skip_list`` inside the configuration file.
+
+A less-preferred method of skipping is to skip all task-based rules for a task (this does not skip line-based rules). There are two mechanisms for this: the ``skip_ansible_lint`` tag works with all tasks, and the ``warn`` parameter works with the *command* or *shell* modules only. Examples:
+
+.. code-block:: yaml
+
+ - name: this would typically fire CommandsInsteadOfArgumentRule 302
+ command: warn=no chmod 644 X
+
+ - name: this would typically fire CommandsInsteadOfModuleRule 303
+ command: git pull --rebase
+ args:
+ warn: False
+
+ - name: this would typically fire GitHasVersionRule 401
+ git: src=/path/to/git/repo dest=checkout
+ tags:
+ - skip_ansible_lint
+
+Creating Custom Rules
+---------------------
+
+Rules are described using a class file per rule. Default rules are named *DeprecatedVariableRule.py*, etc.
+
+Each rule definition should have the following:
+
+* ID: A unique identifier
+* Short description: Brief description of the rule
+* Description: Behaviour the rule is looking for
+* Tags: one or more tags that may be used to include or exclude the rule
+* At least one of the following methods:
+
+ * ``match`` that takes a line and returns None or False, if the line doesn't match the test, and True or a custom message, when it does. (This allows one rule to test multiple behaviours - see e.g. the *CommandsInsteadOfModulesRule*.)
+ * ``matchtask`` that operates on a single task or handler, such that tasks get standardized to always contain a *module* key and *module_arguments* key. Other common task modifiers, such as *when*, *with_items*, etc., are also available as keys, if present in the task.
+
+An example rule using ``match`` is:
+
+.. code-block:: python
+
+ from ansiblelint.rules import AnsibleLintRule
+
+ class DeprecatedVariableRule(AnsibleLintRule):
+
+ id = 'EXAMPLE002'
+ shortdesc = 'Deprecated variable declarations'
+ description = 'Check for lines that have old style ${var} ' + \
+ 'declarations'
+ tags = { 'deprecated' }
+
+ def match(self, file, line):
+ return '${' in line
+
+An example rule using ``matchtask`` is:
+
+.. code-block:: python
+
+ import ansiblelint.utils
+ from ansiblelint.rules import AnsibleLintRule
+
+ class TaskHasTag(AnsibleLintRule):
+ id = 'EXAMPLE001'
+ shortdesc = 'Tasks must have tag'
+ description = 'Tasks must have tag'
+ tags = ['productivity']
+
+ def matchtask(self, file, task):
+ # If the task include another task or make the playbook fail
+ # Don't force to have a tag
+ if not set(task.keys()).isdisjoint(['include','fail']):
+ return False
+
+ # Task should have tags
+ if not task.has_key('tags'):
+ return True
+
+ return False
+
+The task argument to ``matchtask`` contains a number of keys - the critical one is *action*. The value of *task['action']* contains the module being used, and the arguments passed, both as key-value pairs and a list of other arguments (e.g. the command used with shell).
+
+In ansible-lint 2.0.0, *task['action']['args']* was renamed *task['action']['module_arguments']* to avoid a clash when a module actually takes args as a parameter key (e.g. ec2_tag)
+
+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__']*.
+
+Packaging Custom Rules
+``````````````````````
+
+Ansible-lint provides a sub directory named *custom* in its built-in rules,
+``/usr/lib/python3.8/site-packages/ansiblelint/rules/custom/`` for example, to
+install custom rules since v4.3.1. The custom rules which are packaged as an
+usual python package installed into this directory will be loaded and enabled
+automatically by ansible-lint.
+
+To make custom rules loaded automatically, you need the followings:
+
+- Packaging your custom rules as an usual python package named some descriptive ones like ``ansible_lint_custom_rules_foo``.
+- Make it installed into ``<ansible_lint_custom_rules_dir>/custom/<your_custom_rules_subdir>/``.
+
+You may accomplish the second by adding some configurations into the [options]
+section of the ``setup.cfg`` of your custom rules python package like the following.
+
+.. code-block::
+
+ [options]
+ packages =
+ ansiblelint.rules.custom.<your_custom_rules_subdir>
+ package_dir =
+ ansiblelint.rules.custom.<your_custom_rules_subdir> = <your_rules_source_code_subdir>
+
+.. rules-docs-inclusion-marker-end-do-not-remove
+
+Contributing
+============
+
+Please read `Contribution guidelines`_ if you wish to contribute.
+
+Authors
+=======
+
+ansible-lint was created by `Will Thames`_ and is now maintained as part of the `Ansible`_ by `Red Hat`_ project.
+
+.. _Contribution guidelines: https://ansible-lint.readthedocs.io/en/latest/contributing.html
+.. _Will Thames: https://github.com/willthames
+.. _Ansible: https://ansible.com
+.. _Red Hat: https://redhat.com
diff --git a/bindep.txt b/bindep.txt
new file mode 100644
index 0000000..7a0390b
--- /dev/null
+++ b/bindep.txt
@@ -0,0 +1,13 @@
+# This is a cross-platform list tracking distribution packages needed by tests;
+# see https://pypi.org/project/bindep/ for additional information.
+
+bzip2 [platform:rpm]
+gcc [test platform:rpm]
+gcc-c++ [test platform:rpm]
+libselinux-python [platform:centos-7]
+python3 [test platform:rpm !platform:centos-7]
+python3-devel [test platform:rpm !platform:centos-7]
+python3-libselinux [test platform:rpm !platform:centos-7]
+python3-netifaces [test !platform:centos-7 platform:rpm]
+
+openssl-devel [test platform:rpm]
diff --git a/conftest.py b/conftest.py
new file mode 100644
index 0000000..7b05ce4
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,30 @@
+"""PyTest Fixtures."""
+import os
+
+import pytest
+
+from ansiblelint.constants import DEFAULT_RULESDIR
+from ansiblelint.rules import RulesCollection
+from ansiblelint.testing import RunFromText
+
+
+@pytest.fixture
+def default_rules_collection():
+ """Return default rule collection."""
+ assert os.path.isdir(DEFAULT_RULESDIR)
+ return RulesCollection(rulesdirs=[DEFAULT_RULESDIR])
+
+
+@pytest.fixture
+def default_text_runner(default_rules_collection):
+ """Return RunFromText instance for the default set of collections."""
+ return RunFromText(default_rules_collection)
+
+
+@pytest.fixture
+def rule_runner(request):
+ """Return runner for a specific rule class."""
+ rule_class = request.param
+ collection = RulesCollection()
+ collection.register(rule_class())
+ return RunFromText(collection)
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..22695c6
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,16 @@
+# Old compiled python stuff
+*.py[co]
+# package building stuff
+build
+# Emacs backup files...
+*~
+.\#*
+.doctrees
+# Generated docs stuff
+ansible*.xml
+.buildinfo
+objects.inv
+.doctrees
+*.min.css
+_build
+rst_warnings
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/.nojekyll
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..b84f668
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,9 @@
+# Documentation source for Ansible Lint
+
+To build the docs, run `tox -e docs`. At the end of the build, you will
+see the local location of your built docs.
+
+Building docs locally may not be identical to CI/CD builds. We recommend
+you to create a draft PR and check the RTD PR preview page too.
+
+If you do not want to learn the reStructuredText format, you can also [file an issue](https://github.com/ansible/ansible-lint/issues), and let us know how we can improve our documentation.
diff --git a/docs/_static/ansible-lint.svg b/docs/_static/ansible-lint.svg
new file mode 100644
index 0000000..e1270a8
--- /dev/null
+++ b/docs/_static/ansible-lint.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g transform="matrix(1,0,0,1,-67.7268,-0.0813272)">
+ <g id="ansible-lint" transform="matrix(0.103624,0,0,0.0970726,-51.3462,16.5365)">
+ <rect x="1149.09" y="-169.514" width="617.619" height="659.3" style="fill:none;"/>
+ <g transform="matrix(-9.64401,0.0125512,-0.0123169,-10.7844,1789.35,505.457)">
+ <path d="M34.404,5.547C56.248,14.75 62.901,29.008 62.864,58.451C48.638,53.766 48.041,53.62 34.295,58.418C19.563,53.502 19.058,53.483 6.355,58.386C6.392,29.206 14.197,15.017 34.404,5.547Z" style="fill:rgb(128,128,128);stroke:rgb(128,128,128);stroke-width:3.91px;"/>
+ </g>
+ <g transform="matrix(2.86444,0,0,3.05775,1016.6,-336.23)">
+ <path d="M154.799,112.893L182.438,181.102L140.692,148.221L154.799,112.893ZM203.895,196.815L161.385,94.514C160.385,91.762 157.727,89.94 154.799,90.001C151.826,89.933 149.113,91.743 148.034,94.514L101.377,206.726L117.338,206.726L135.806,160.458L190.923,204.988C193.142,206.782 194.739,207.593 196.82,207.593C196.88,207.594 196.939,207.595 196.999,207.595C201.181,207.595 204.623,204.153 204.623,199.97C204.623,199.968 204.623,199.966 204.623,199.964C204.551,198.882 204.305,197.819 203.895,196.815" style="fill:white;fill-rule:nonzero;"/>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/docs/_static/images/logo_invert.png b/docs/_static/images/logo_invert.png
new file mode 100644
index 0000000..dfeba66
--- /dev/null
+++ b/docs/_static/images/logo_invert.png
Binary files differ
diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css
new file mode 100644
index 0000000..2eea3ee
--- /dev/null
+++ b/docs/_static/theme_overrides.css
@@ -0,0 +1,15 @@
+/* table width fix via: https://rackerlabs.github.io/docs-rackspace/tools/rtd-tables.html */
+
+/* override table width restrictions */
+@media screen and (min-width: 767px) {
+
+ .wy-table-responsive table td {
+ /* !important prevents the common CSS stylesheets from overriding
+ * this as on RTD they are loaded after this stylesheet */
+ white-space: normal !important;
+ }
+
+ .wy-table-responsive {
+ overflow: visible !important;
+ }
+}
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..9a911ed
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,256 @@
+# -*- coding: utf-8 -*-
+#
+# documentation build configuration file, created by
+# sphinx-quickstart on Sat Sep 27 13:23:22 2008-2009.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# The contents of this file are pickled, so don't put values in the namespace
+# that aren't pickleable (module imports are okay, they're removed
+# automatically).
+#
+# All configuration values have a default value; values that are commented out
+# serve to show the default value.
+"""Documentation Configuration."""
+
+import os
+import sys
+from pathlib import Path
+
+# Make in-tree extension importable in non-tox setups/envs, like RTD.
+# Refs:
+# https://github.com/readthedocs/readthedocs.org/issues/6311
+# https://github.com/readthedocs/readthedocs.org/issues/7182
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+
+# pip install sphinx_rtd_theme
+# import sphinx_rtd_theme
+# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# If your extensions are in another directory, add it here. If the directory
+# is relative to the documentation root, use os.path.abspath to make it
+# absolute, like shown here.
+# sys.path.append(os.path.abspath('some/directory'))
+#
+sys.path.insert(0, os.path.join('ansible', 'lib'))
+sys.path.append(os.path.abspath('_themes'))
+
+VERSION = '2.6'
+AUTHOR = 'Ansible, Inc'
+
+
+# General configuration
+# ---------------------
+
+# Add any Sphinx extension module names here, as strings.
+# They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+# TEST: 'sphinxcontrib.fulltoc'
+extensions = [
+ 'myst_parser',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+ 'sphinxcontrib.programoutput',
+ 'rules_table_generator_ext', # in-tree extension
+]
+
+# Later on, add 'sphinx.ext.viewcode' to the list if you want to have
+# colorized code generated too for references.
+
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['.templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General substitutions.
+project = 'Ansible Lint Documentation'
+copyright = "2013-2020 Ansible, Inc"
+
+# The default replacements for |version| and |release|, also used in various
+# other places throughout the built documents.
+#
+# The short X.Y version.
+version = VERSION
+# The full version, including alpha/beta/rc tags.
+release = VERSION
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+# unused_docs = []
+
+# List of directories, relative to source directories, that shouldn't be
+# searched for source files.
+# exclude_dirs = []
+
+# A list of glob-style patterns that should be excluded when looking
+# for source files.
+# OBSOLETE - removing this - dharmabumstead 2018-02-06
+exclude_patterns = ['README.md']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+default_role = 'any'
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+highlight_language = 'YAML+Jinja'
+
+# Substitutions, variables, entities, & shortcuts for text which do not need to link to anything.
+# For titles which should be a link, use the intersphinx anchors set at the index, chapter, and
+# section levels, such as qi_start_:
+rst_epilog = """
+.. |acapi| replace:: *Ansible Core API Guide*
+.. |acrn| replace:: *Ansible Core Release Notes*
+.. |ac| replace:: Ansible Core
+.. |acversion| replace:: Ansible Core Version 2.1
+.. |acversionshort| replace:: Ansible Core 2.1
+.. |versionshortest| replace:: 2.2
+.. |versiondev| replace:: 2.3
+.. |pubdate| replace:: July 19, 2016
+.. |rhel| replace:: Red Hat Enterprise Linux
+
+"""
+
+
+# Options for HTML output
+# -----------------------
+
+html_theme_path = ['../_themes']
+html_theme = 'sphinx_ansible_theme'
+html_short_title = 'Ansible Lint Documentation'
+
+# The style sheet to use for HTML and HTML Help pages. A file of that name
+# must exist either in Sphinx' static/ path, or in one of the custom paths
+# given in html_static_path.
+# html_style = 'solar.css'
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+html_title = 'Ansible Lint Documentation'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (within the static path) to place at the top of
+# the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+html_favicon = '_static/ansible-lint.svg'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['.static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_use_modindex = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, the reST sources are included in the HTML build as _sources/<name>.
+html_copy_source = False
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+html_use_opensearch = 'https://ansible-lint.readthedocs.io/en/latest/'
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Poseidodoc'
+
+
+# Options for LaTeX output
+# ------------------------
+
+# The paper size ('letter' or 'a4').
+# latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+# latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, document class
+# [howto/manual]).
+latex_documents = [
+ ('index', 'ansible.tex', 'Ansible 2.2 Documentation', AUTHOR, 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+# latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_use_modindex = True
+
+autoclass_content = 'both'
+
+intersphinx_mapping = {'python': ('https://docs.python.org/2/', (None, '../python2-2.7.13.inv')),
+ 'python3': ('https://docs.python.org/3/', (None, '../python3-3.6.2.inv')),
+ 'jinja2': ('http://jinja.pocoo.org/docs/', (None, '../jinja2-2.9.7.inv'))}
+
+
+# table width fix via: https://rackerlabs.github.io/docs-rackspace/tools/rtd-tables.html
+html_static_path = ['_static']
+html_context = {
+ 'css_files': [
+ '_static/theme_overrides.css', # override wide tables in RTD theme
+ ],
+}
diff --git a/docs/configuring.rst b/docs/configuring.rst
new file mode 100644
index 0000000..fac77d3
--- /dev/null
+++ b/docs/configuring.rst
@@ -0,0 +1,14 @@
+
+.. _configuring_lint:
+
+***********
+Configuring
+***********
+
+.. contents:: Topics
+
+This topic describes how to configure Ansible Lint
+
+.. include:: ../README.rst
+ :start-after: configuring-docs-inclusion-marker-do-not-remove
+ :end-before: configuring-docs-inclusion-marker-end-do-not-remove
diff --git a/docs/contributing.rst b/docs/contributing.rst
new file mode 100644
index 0000000..3f0d3bb
--- /dev/null
+++ b/docs/contributing.rst
@@ -0,0 +1,50 @@
+.. include:: ../.github/CONTRIBUTING.rst
+ :end-before: DO-NOT-REMOVE-deps-snippet-PLACEHOLDER
+
+Module dependency graph
+-----------------------
+
+Extra care should be taken when considering adding any dependency. Removing
+most dependencies on Ansible internals is desired as these can change
+without any warning.
+
+.. command-output:: pipdeptree -p ansible-lint
+
+.. include:: ../.github/CONTRIBUTING.rst
+ :start-after: DO-NOT-REMOVE-deps-snippet-PLACEHOLDER
+
+Adding a new rule
+-----------------
+
+Writing a new rule is as easy as adding a single new rule, one that combines
+**implementation, testing and documentation**.
+
+One good example is MetaTagValidRule_ which can easily be copied in order
+to create a new rule by following the steps below:
+
+* Use a short but clear class name, which must match the filename
+* Pick an unused ``id``, the first number is used to determine rule section.
+ Look at rules_ page and pick one that matches the best your new rule.
+ see which one fits best.
+* Include ``experimental`` tag. Any new rule must stay as
+ experimental for at least two weeks until this tag is removed in next major
+ release.
+* Update all class level variables.
+* Implement linting methods needed by your rule, these are those starting with
+ **match** prefix. Implement only those you need. For the moment you will need
+ to look at how similar rules were implemented to figure out what to do.
+* Update the tests. It must have at least one test and likely also a negative
+ match one.
+* If the rule is task specific, it may be best to include a test to verify its
+ use inside blocks as well.
+* Optionally run only the rule specific tests with a command like:
+ :command:`tox -e py38-ansible29 -- -k NewRule`
+* Run :command:`tox` in order to run all ansible-lint tests. Adding a new rule
+ can break some other tests. Update them if needed.
+* Run :command:`ansible-lint -L` and check that the rule description renders
+ correctly.
+* Build the docs using :command:`tox -e docs` and check that the new rule is
+ displayed correctly in them.
+
+.. _MetaTagValidRule: https://github.com/ansible/ansible-lint/blob/master/lib/ansiblelint/rules/MetaTagValidRule.py
+.. _rules: https://ansible-lint.readthedocs.io/en/latest/default_rules.html
diff --git a/docs/default_rules.rst b/docs/default_rules.rst
new file mode 100644
index 0000000..daf66a0
--- /dev/null
+++ b/docs/default_rules.rst
@@ -0,0 +1 @@
+.. ansible-lint-default-rules-list::
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..3836198
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,47 @@
+.. _lint_documentation:
+
+Ansible Lint Documentation
+==========================
+
+About Ansible Lint
+``````````````````
+
+Ansible Lint is a commandline tool for linting playbooks. Use it to detect behaviors and practices that could potentially
+be improved.
+
+The tool is used by the `Ansible Galaxy project <https://github.com/ansible/galaxy/>`_ to lint and calculate quality scores
+for content contributed to the `Galaxy Hub <https://galaxy.ansible.com>`_.
+
+The project was originally started by `@willthames <https://github.com/willthames/>`_, and has since been
+transferred to the Ansible project team.
+
+
+.. toctree::
+ :maxdepth: 3
+ :caption: Installing
+
+ installing
+
+.. toctree::
+ :maxdepth: 3
+ :caption: Usage
+
+ usage
+
+.. toctree::
+ :maxdepth: 3
+ :caption: Configuring
+
+ configuring
+
+.. toctree::
+ :maxdepth: 4
+ :caption: Rules
+
+ rules
+ default_rules
+
+.. toctree::
+ :caption: Contributing
+
+ contributing
diff --git a/docs/installing.rst b/docs/installing.rst
new file mode 100644
index 0000000..1b84abd
--- /dev/null
+++ b/docs/installing.rst
@@ -0,0 +1,15 @@
+
+.. _installing_lint:
+
+
+**********
+Installing
+**********
+
+.. contents:: Topics
+
+This topic describes how to install Ansible Lint.
+
+.. include:: ../README.rst
+ :start-after: installing-docs-inclusion-marker-do-not-remove
+ :end-before: installing-docs-inclusion-marker-end-do-not-remove
diff --git a/docs/jinja2-2.9.7.inv b/docs/jinja2-2.9.7.inv
new file mode 100644
index 0000000..a45888b
--- /dev/null
+++ b/docs/jinja2-2.9.7.inv
Binary files differ
diff --git a/docs/python2-2.7.13.inv b/docs/python2-2.7.13.inv
new file mode 100644
index 0000000..ab7587f
--- /dev/null
+++ b/docs/python2-2.7.13.inv
Binary files differ
diff --git a/docs/python3-3.6.2.inv b/docs/python3-3.6.2.inv
new file mode 100644
index 0000000..1d2ed4e
--- /dev/null
+++ b/docs/python3-3.6.2.inv
Binary files differ
diff --git a/docs/requirements.in b/docs/requirements.in
new file mode 100644
index 0000000..9ed90dd
--- /dev/null
+++ b/docs/requirements.in
@@ -0,0 +1,5 @@
+myst-parser
+pipdeptree
+Sphinx
+sphinx_ansible_theme
+sphinxcontrib.programoutput
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..4820753
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,184 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile --generate-hashes --output-file=docs/requirements.txt docs/requirements.in
+#
+alabaster==0.7.12 \
+ --hash=sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359 \
+ --hash=sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02 \
+ # via sphinx
+attrs==19.3.0 \
+ --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \
+ --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 \
+ # via markdown-it-py
+babel==2.8.0 \
+ --hash=sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38 \
+ --hash=sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4 \
+ # via sphinx
+certifi==2020.6.20 \
+ --hash=sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3 \
+ --hash=sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41 \
+ # via requests
+chardet==3.0.4 \
+ --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
+ --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
+ # via requests
+docutils==0.16 \
+ --hash=sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af \
+ --hash=sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc \
+ # via myst-parser, sphinx
+idna==2.10 \
+ --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 \
+ --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
+ # via requests
+imagesize==1.2.0 \
+ --hash=sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1 \
+ --hash=sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1 \
+ # via sphinx
+jinja2==2.11.2 \
+ --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 \
+ --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \
+ # via sphinx
+markdown-it-py==0.5.4 \
+ --hash=sha256:d1782446f7fcbf2db9a1bc0430230cb879498ad6d76168d7e7c762bab04cb4ea \
+ --hash=sha256:f18ec8f1c1a424ab2a9ac06b5ba87d6d2a01e450cd8678edbc71002106dd68a8 \
+ # via myst-parser
+markupsafe==1.1.1 \
+ --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
+ --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
+ --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
+ --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
+ --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
+ --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
+ --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \
+ --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
+ --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
+ --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
+ --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
+ --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
+ --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
+ --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
+ --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
+ --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
+ --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
+ --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
+ --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
+ --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
+ --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
+ --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
+ --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
+ --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
+ --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
+ --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
+ --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
+ --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
+ --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
+ --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
+ --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
+ --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
+ --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
+ # via jinja2
+myst-parser==0.12.10 \
+ --hash=sha256:4612c46196e0344bb7e49dbc3deb288f9b9a88fcf6e9f210f7f3ea5bc9899bfc \
+ --hash=sha256:a5311da4398869e596250d5a93b523735c3beb8bc9d3eba853223c705802043b \
+ # via -r requirements.in
+packaging==20.4 \
+ --hash=sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8 \
+ --hash=sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181 \
+ # via sphinx
+pipdeptree==1.0.0 \
+ --hash=sha256:35a81058c9568a29c5a9569109304b25f11cd9333fa2661a4d4c2c5da0e3939d \
+ --hash=sha256:5fe866a38113d28d527033ececc57b8e86df86b7c29edbacb33f41ee50f75b31 \
+ --hash=sha256:a7e4f744f3ae149cf94dd5e517fae682780c4729f4a279e6fb81a928f57fea23 \
+ # via -r requirements.in
+pygments==2.6.1 \
+ --hash=sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44 \
+ --hash=sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324 \
+ # via sphinx
+pyparsing==2.4.7 \
+ --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \
+ --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
+ # via packaging
+pytz==2020.1 \
+ --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \
+ --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048 \
+ # via babel
+pyyaml==5.3.1 \
+ --hash=sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97 \
+ --hash=sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76 \
+ --hash=sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2 \
+ --hash=sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648 \
+ --hash=sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf \
+ --hash=sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f \
+ --hash=sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2 \
+ --hash=sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee \
+ --hash=sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d \
+ --hash=sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c \
+ --hash=sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a \
+ # via myst-parser
+requests==2.24.0 \
+ --hash=sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b \
+ --hash=sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898 \
+ # via sphinx
+six==1.15.0 \
+ --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
+ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
+ # via packaging
+snowballstemmer==2.0.0 \
+ --hash=sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0 \
+ --hash=sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52 \
+ # via sphinx
+sphinx-ansible-theme==0.3.2 \
+ --hash=sha256:250e46bc318062a2e95cc55db5dfecddb5f847f38d672d487162920f3f3ae205 \
+ --hash=sha256:424ec6fbc61bc8bba3e6eb482d3ceb92e6e2d80d7e8e06599e2bbc856026feaf \
+ # via -r requirements.in
+sphinx-notfound-page==0.4 \
+ --hash=sha256:0105a40d8a305d3e1003630d8ee99296baa08cf2a4c1ce1db8d91fbbe78f90db \
+ --hash=sha256:609fd7cd7f9ea73c030f1b67a3f2bc90f60bff87b30026fbd2bcb19c7c59c484 \
+ # via sphinx-ansible-theme
+sphinx-rtd-theme==0.5.0 \
+ --hash=sha256:22c795ba2832a169ca301cd0a083f7a434e09c538c70beb42782c073651b707d \
+ --hash=sha256:373413d0f82425aaa28fb288009bf0d0964711d347763af2f1b65cafcb028c82 \
+ # via sphinx-ansible-theme
+sphinx==3.2.1 \
+ --hash=sha256:321d6d9b16fa381a5306e5a0b76cd48ffbc588e6340059a729c6fdd66087e0e8 \
+ --hash=sha256:ce6fd7ff5b215af39e2fcd44d4a321f6694b4530b6f2b2109b64d120773faea0 \
+ # via -r requirements.in, myst-parser, sphinx-rtd-theme, sphinxcontrib.programoutput
+sphinxcontrib-applehelp==1.0.2 \
+ --hash=sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a \
+ --hash=sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58 \
+ # via sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 \
+ # via sphinx
+sphinxcontrib-htmlhelp==1.0.3 \
+ --hash=sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f \
+ --hash=sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b \
+ # via sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 \
+ # via sphinx
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 \
+ # via sphinx
+sphinxcontrib-serializinghtml==1.1.4 \
+ --hash=sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc \
+ --hash=sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a \
+ # via sphinx
+sphinxcontrib.programoutput==0.16 \
+ --hash=sha256:0caaa216d0ad8d2cfa90a9a9dba76820e376da6e3152be28d10aedc09f82a3b0 \
+ --hash=sha256:8009d1326b89cd029ee477ce32b45c58d92b8504d48811461c3117014a8f4b1e \
+ # via -r requirements.in
+urllib3==1.25.9 \
+ --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \
+ --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115 \
+ # via requests
+
+# WARNING: The following packages were not pinned, but pip requires them to be
+# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
+# pip
+# setuptools
diff --git a/docs/rules.rst b/docs/rules.rst
new file mode 100644
index 0000000..5a02193
--- /dev/null
+++ b/docs/rules.rst
@@ -0,0 +1,13 @@
+.. _lint_rules:
+
+*****
+Rules
+*****
+
+.. contents:: Topics
+
+This topic describes how to use the default Ansible Lint rules, as well as how to create and use custom rules.
+
+.. include:: ../README.rst
+ :start-after: rules-docs-inclusion-marker-do-not-remove
+ :end-before: rules-docs-inclusion-marker-end-do-not-remove
diff --git a/docs/rules_table_generator_ext.py b/docs/rules_table_generator_ext.py
new file mode 100644
index 0000000..51e2f4d
--- /dev/null
+++ b/docs/rules_table_generator_ext.py
@@ -0,0 +1,68 @@
+#! /usr/bin/env python3
+# Requires Python 3.6+
+"""Sphinx extension for generating the rules table document."""
+
+from typing import Dict, List, Union
+
+from sphinx.application import Sphinx
+from sphinx.util.docutils import SphinxDirective
+from sphinx.util.nodes import nested_parse_with_titles, nodes
+
+# isort: split
+
+from docutils import statemachine
+
+from ansiblelint import __version__
+from ansiblelint.constants import DEFAULT_RULESDIR
+from ansiblelint.generate_docs import rules_as_rst
+from ansiblelint.rules import RulesCollection
+
+
+def _nodes_from_rst(
+ state: statemachine.State,
+ rst_source: str,
+) -> List[nodes.Node]:
+ """Turn an RST string into a list of nodes.
+
+ These nodes can be used in the document.
+ """
+ node = nodes.Element()
+ node.document = state.document
+ nested_parse_with_titles(
+ state=state,
+ content=statemachine.ViewList(
+ statemachine.string2lines(rst_source),
+ source='[ansible-lint autogenerated]',
+ ),
+ node=node,
+ )
+ return node.children
+
+
+class AnsibleLintDefaultRulesDirective(SphinxDirective):
+ """Directive ``ansible-lint-default-rules-list`` definition."""
+
+ has_content = False
+
+ def run(self) -> List[nodes.Node]:
+ """Generate a node tree in place of the directive."""
+ self.env.note_reread() # rebuild the current RST doc unconditionally
+
+ default_rules = RulesCollection([DEFAULT_RULESDIR])
+ rst_rules_table = rules_as_rst(default_rules)
+
+ return _nodes_from_rst(state=self.state, rst_source=rst_rules_table)
+
+
+def setup(app: Sphinx) -> Dict[str, Union[bool, str]]:
+ """Initialize the Sphinx extension."""
+ app.add_directive(
+ 'ansible-lint-default-rules-list',
+ AnsibleLintDefaultRulesDirective,
+ )
+
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ 'version': __version__,
+ }
diff --git a/docs/usage.rst b/docs/usage.rst
new file mode 100644
index 0000000..8fb166e
--- /dev/null
+++ b/docs/usage.rst
@@ -0,0 +1,15 @@
+
+.. _using_lint:
+
+
+*****
+Usage
+*****
+
+.. contents:: Topics
+
+This topic describes how to use ``ansible-lint``.
+
+.. include:: ../README.rst
+ :start-after: usage-docs-inclusion-marker-do-not-remove
+ :end-before: usage-docs-inclusion-marker-end-do-not-remove
diff --git a/examples/example.yml b/examples/example.yml
new file mode 100644
index 0000000..1ce910d
--- /dev/null
+++ b/examples/example.yml
@@ -0,0 +1,52 @@
+---
+- hosts: webservers
+
+ vars:
+ oldskool: "1.2.3"
+ bracket: "and close bracket"
+
+ tasks:
+ - name: unset variable
+ action: command echo {{thisvariable}} is not set in this playbook
+
+ - name: trailing whitespace
+ action: command echo do nothing
+
+ - name: git check
+ action: git a=b c=d
+
+ - name: git check 2
+ action: git version=HEAD c=d
+
+ - name: git check 3
+ git: version=a1b2c3d4 repo=xyz bobbins=d
+
+ - name: executing git through command
+ action: command git clone blah
+
+ - name: executing git through command
+ action: command chdir=bobbins creates=whatever /usr/bin/git clone blah
+
+ - name: using git module
+ action: git repo=blah
+
+ - name: passing git as an argument to another task
+ action: debug msg="{{item}}"
+ with_items:
+ - git
+ - bobbins
+
+ - name: yum latest
+ yum: state=latest name=httpd
+
+ - debug: msg="task without a name"
+
+ - name: apt latest
+ apt: state=latest name=apache2
+
+ - name: always run
+ debug: msg="always_run is deprecated"
+ always_run: true
+
+ # empty task is currently accepted by ansible as valid code:
+ -
diff --git a/examples/handlers/y.yml b/examples/handlers/y.yml
new file mode 100644
index 0000000..fe98a7a
--- /dev/null
+++ b/examples/handlers/y.yml
@@ -0,0 +1,2 @@
+- name: funny handler
+ action: service name=funny state=started force=true
diff --git a/examples/include.yml b/examples/include.yml
new file mode 100644
index 0000000..0e056ec
--- /dev/null
+++ b/examples/include.yml
@@ -0,0 +1,19 @@
+---
+- hosts: bobbins
+
+
+ pre_tasks:
+ - include: tasks/x.yml
+
+ roles:
+ - hello
+ - { role: morecomplex, t: z }
+
+ tasks:
+ - include: tasks/x.yml
+ - include: tasks/x.yml y=z
+
+ handlers:
+ - include: handlers/y.yml
+
+- include: play.yml
diff --git a/examples/lineno.yml b/examples/lineno.yml
new file mode 100644
index 0000000..57f879d
--- /dev/null
+++ b/examples/lineno.yml
@@ -0,0 +1,2 @@
+- tasks:
+ - git: repo=hello
diff --git a/examples/lots_of_warnings.yml b/examples/lots_of_warnings.yml
new file mode 100644
index 0000000..38136e1
--- /dev/null
+++ b/examples/lots_of_warnings.yml
@@ -0,0 +1,1000 @@
+---
+# This playbook causes ansible-lint to output tons of warnings
+# Enough to exceed typical stdout buffering size and thus to show the need for
+# catching IOError (EPIEP) errors.
+
+- hosts: webservers
+
+ tasks:
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
+ - name: executing git through command
+ action: command git clone blah
diff --git a/examples/nomatches.yml b/examples/nomatches.yml
new file mode 100644
index 0000000..2cc726e
--- /dev/null
+++ b/examples/nomatches.yml
@@ -0,0 +1,9 @@
+---
+- hosts: whatever
+
+ tasks:
+ - name: hello world
+ action: debug msg="Hello!"
+
+ - name: this should be fine too
+ action: file state=touch mode=0644 dest=./wherever
diff --git a/examples/play.yml b/examples/play.yml
new file mode 100644
index 0000000..63e0678
--- /dev/null
+++ b/examples/play.yml
@@ -0,0 +1,6 @@
+---
+- hosts: bobbins
+
+ tasks:
+ - name: a bad play
+ action: command service blah restart
diff --git a/examples/roles/bobbins/tasks/main.yml b/examples/roles/bobbins/tasks/main.yml
new file mode 100644
index 0000000..8df6c6b
--- /dev/null
+++ b/examples/roles/bobbins/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: test tasks
+ action: git a=b c=d
diff --git a/examples/roles/hello/meta/main.yml b/examples/roles/hello/meta/main.yml
new file mode 100644
index 0000000..b15a998
--- /dev/null
+++ b/examples/roles/hello/meta/main.yml
@@ -0,0 +1,3 @@
+---
+dependencies:
+ - role: bobbins
diff --git a/examples/roles/morecomplex/handlers/main.yml b/examples/roles/morecomplex/handlers/main.yml
new file mode 100644
index 0000000..3d5b393
--- /dev/null
+++ b/examples/roles/morecomplex/handlers/main.yml
@@ -0,0 +1,2 @@
+- name: restart service using command
+ command: service bar restart
diff --git a/examples/roles/morecomplex/tasks/main.yml b/examples/roles/morecomplex/tasks/main.yml
new file mode 100644
index 0000000..ed68394
--- /dev/null
+++ b/examples/roles/morecomplex/tasks/main.yml
@@ -0,0 +1,8 @@
+- name: test bad command
+ action: command mkdir blah
+
+- name: test bad command v2
+ command: mkdir blah
+
+- name: test bad local command
+ local_action: shell touch foo
diff --git a/examples/rules/TaskHasTag.py b/examples/rules/TaskHasTag.py
new file mode 100644
index 0000000..58dfa7c
--- /dev/null
+++ b/examples/rules/TaskHasTag.py
@@ -0,0 +1,37 @@
+"""Example implementation of a rule requiring tasks to have tags set."""
+from ansiblelint.rules import AnsibleLintRule
+
+
+class TaskHasTag(AnsibleLintRule):
+ """Tasks must have tag."""
+
+ id = 'EXAMPLE001'
+ shortdesc = 'Tasks must have tag'
+ description = 'Tasks must have tag'
+ tags = ['productivity', 'tags']
+
+ def matchtask(self, file, task):
+ """Task matching method."""
+ # The meta files don't have tags
+ if file['type'] in ["meta", "playbooks"]:
+ return False
+
+ if isinstance(task, str):
+ return False
+
+ # If the task include another task or make the playbook fail
+ # Don't force to have a tag
+ 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
+
+ return False
diff --git a/examples/tasks/x.yml b/examples/tasks/x.yml
new file mode 100644
index 0000000..c5a4f10
--- /dev/null
+++ b/examples/tasks/x.yml
@@ -0,0 +1,4 @@
+- name: test include
+ action: funny value=clown
+ args:
+ key: value
diff --git a/examples/unicode.yml b/examples/unicode.yml
new file mode 100644
index 0000000..c16ce92
--- /dev/null
+++ b/examples/unicode.yml
@@ -0,0 +1,5 @@
+---
+- hosts: all
+ tasks:
+ - name: тест
+ command: uname
diff --git a/hooks.yaml b/hooks.yaml
new file mode 100644
index 0000000..e9131c8
--- /dev/null
+++ b/hooks.yaml
@@ -0,0 +1,11 @@
+---
+
+# For use with pre-commit.
+# See usage instructions at http://pre-commit.com
+
+- id: ansible-lint
+ name: Ansible-lint
+ description: This hook runs ansible-lint.
+ entry: ansible-lint
+ language: python
+ files: \.(yaml|yml)$
diff --git a/lib/ansiblelint/__init__.py b/lib/ansiblelint/__init__.py
new file mode 100644
index 0000000..d8a4cef
--- /dev/null
+++ b/lib/ansiblelint/__init__.py
@@ -0,0 +1,28 @@
+# 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.
+"""Main ansible-lint package."""
+
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.version import __version__
+
+__all__ = (
+ "__version__",
+ "AnsibleLintRule" # deprecated, import it directly from rules
+)
diff --git a/lib/ansiblelint/__main__.py b/lib/ansiblelint/__main__.py
new file mode 100755
index 0000000..ff8d477
--- /dev/null
+++ b/lib/ansiblelint/__main__.py
@@ -0,0 +1,270 @@
+#!/usr/bin/env python
+# 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.
+"""Command line implementation."""
+
+import errno
+import logging
+import os
+import pathlib
+import subprocess
+import sys
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, List, Set, Type, Union
+
+from rich.markdown import Markdown
+
+from ansiblelint import cli, formatters
+from ansiblelint.color import console, console_stderr
+from ansiblelint.file_utils import cwd
+from ansiblelint.generate_docs import rules_as_rich, rules_as_rst
+from ansiblelint.rules import RulesCollection
+from ansiblelint.runner import Runner
+from ansiblelint.utils import get_playbooks_and_roles, get_rules_dirs
+
+if TYPE_CHECKING:
+ from argparse import Namespace
+
+ from ansiblelint.errors import MatchError
+
+_logger = logging.getLogger(__name__)
+
+_rule_format_map = {
+ 'plain': str,
+ 'rich': rules_as_rich,
+ 'rst': rules_as_rst
+}
+
+
+def initialize_logger(level: int = 0) -> None:
+ """Set up the global logging level based on the verbosity number."""
+ VERBOSITY_MAP = {
+ 0: logging.NOTSET,
+ 1: logging.INFO,
+ 2: logging.DEBUG
+ }
+
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter('%(levelname)-8s %(message)s')
+ handler.setFormatter(formatter)
+ logger = logging.getLogger(__package__)
+ logger.addHandler(handler)
+ # Unknown logging level is treated as DEBUG
+ logging_level = VERBOSITY_MAP.get(level, logging.DEBUG)
+ logger.setLevel(logging_level)
+ # Use module-level _logger instance to validate it
+ _logger.debug("Logging initialized to level %s", logging_level)
+
+
+def choose_formatter_factory(
+ options_list: "Namespace"
+) -> Type[formatters.BaseFormatter]:
+ """Select an output formatter based on the incoming command line arguments."""
+ r: Type[formatters.BaseFormatter] = formatters.Formatter
+ if options_list.quiet:
+ r = formatters.QuietFormatter
+ elif options_list.parseable:
+ r = formatters.ParseableFormatter
+ elif options_list.parseable_severity:
+ r = formatters.ParseableSeverityFormatter
+ return r
+
+
+def report_outcome(matches: List["MatchError"], options) -> int:
+ """Display information about how to skip found rules.
+
+ Returns exit code, 2 if errors were found, 0 when only warnings were found.
+ """
+ failure = False
+ msg = """\
+You can skip specific rules or tags by adding them to your configuration file:
+```yaml
+# .ansible-lint
+warn_list: # or 'skip_list' to silence them completely
+"""
+ matches_unignored = [match for match in matches if not match.ignored]
+
+ matched_rules = {match.rule.id: match.rule for match in matches_unignored}
+ for id in sorted(matched_rules.keys()):
+ if {id, *matched_rules[id].tags}.isdisjoint(options.warn_list):
+ msg += f" - '{id}' # {matched_rules[id].shortdesc}\n"
+ failure = True
+ for match in matches:
+ if "experimental" in match.rule.tags:
+ msg += " - experimental # all rules tagged as experimental\n"
+ break
+ msg += "```"
+
+ if matches and not options.quiet:
+ console_stderr.print(Markdown(msg))
+
+ if failure:
+ return 2
+ else:
+ return 0
+
+
+def main() -> int:
+ """Linter CLI entry point."""
+ cwd = pathlib.Path.cwd()
+
+ options = cli.get_config(sys.argv[1:])
+
+ initialize_logger(options.verbosity)
+ _logger.debug("Options: %s", options)
+
+ formatter_factory = choose_formatter_factory(options)
+ formatter = formatter_factory(cwd, options.display_relative_path)
+
+ rulesdirs = get_rules_dirs([str(rdir) for rdir in options.rulesdir],
+ options.use_default_rules)
+ rules = RulesCollection(rulesdirs)
+
+ if options.listrules:
+ console.print(
+ _rule_format_map[options.format](rules),
+ highlight=False)
+ return 0
+
+ if options.listtags:
+ print(rules.listtags())
+ return 0
+
+ if isinstance(options.tags, str):
+ options.tags = options.tags.split(',')
+
+ skip = set()
+ for s in options.skip_list:
+ skip.update(str(s).split(','))
+ options.skip_list = frozenset(skip)
+
+ matches = _get_matches(rules, options)
+
+ # Assure we do not print duplicates and the order is consistent
+ matches = sorted(set(matches))
+
+ mark_as_success = False
+ if matches and options.progressive:
+ _logger.info(
+ "Matches found, running again on previous revision in order to detect regressions")
+ with _previous_revision():
+ old_matches = _get_matches(rules, options)
+ # remove old matches from current list
+ matches_delta = list(set(matches) - set(old_matches))
+ if len(matches_delta) == 0:
+ _logger.warning(
+ "Total violations not increased since previous "
+ "commit, will mark result as success. (%s -> %s)",
+ len(old_matches), len(matches_delta))
+ mark_as_success = True
+
+ ignored = 0
+ for match in matches:
+ # if match is not new, mark is as ignored
+ if match not in matches_delta:
+ match.ignored = True
+ ignored += 1
+ if ignored:
+ _logger.warning(
+ "Marked %s previously known violation(s) as ignored due to"
+ " progressive mode.", ignored)
+
+ _render_matches(matches, options, formatter, cwd)
+
+ if matches and not mark_as_success:
+ return report_outcome(matches, options=options)
+ else:
+ return 0
+
+
+def _render_matches(
+ matches: List,
+ options: "Namespace",
+ formatter: Any,
+ cwd: Union[str, pathlib.Path]):
+
+ ignored_matches = [match for match in matches if match.ignored]
+ fatal_matches = [match for match in matches if not match.ignored]
+ # Displayed ignored matches first
+ if ignored_matches:
+ _logger.warning(
+ "Listing %s violation(s) marked as ignored, likely already known",
+ len(ignored_matches))
+ for match in ignored_matches:
+ if match.ignored:
+ print(formatter.format(match, options.colored))
+ if fatal_matches:
+ _logger.warning("Listing %s violation(s) that are fatal", len(fatal_matches))
+ for match in fatal_matches:
+ if not match.ignored:
+ print(formatter.format(match, options.colored))
+
+ # If run under GitHub Actions we also want to emit output recognized by it.
+ if os.getenv('GITHUB_ACTIONS') == 'true' and os.getenv('GITHUB_WORKFLOW'):
+ formatter = formatters.AnnotationsFormatter(cwd, True)
+ for match in matches:
+ print(formatter.format(match))
+
+
+def _get_matches(rules: RulesCollection, options: "Namespace") -> list:
+
+ if not options.playbook:
+ # no args triggers auto-detection mode
+ playbooks = get_playbooks_and_roles(options=options)
+ else:
+ playbooks = sorted(set(options.playbook))
+
+ matches = list()
+ checked_files: Set[str] = set()
+ for playbook in playbooks:
+ runner = Runner(rules, playbook, options.tags,
+ options.skip_list, options.exclude_paths,
+ options.verbosity, checked_files)
+ matches.extend(runner.run())
+ return matches
+
+
+@contextmanager
+def _previous_revision():
+ """Create or update a temporary workdir containing the previous revision."""
+ worktree_dir = ".cache/old-rev"
+ revision = subprocess.run(
+ ["git", "rev-parse", "HEAD^1"],
+ check=True,
+ universal_newlines=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ ).stdout
+ p = pathlib.Path(worktree_dir)
+ p.mkdir(parents=True, exist_ok=True)
+ os.system(f"git worktree add -f {worktree_dir} 2>/dev/null")
+ with cwd(worktree_dir):
+ os.system(f"git checkout {revision}")
+ yield
+
+
+if __name__ == "__main__":
+ try:
+ sys.exit(main())
+ except IOError as exc:
+ if exc.errno != errno.EPIPE:
+ raise
+ except RuntimeError as e:
+ raise SystemExit(str(e))
diff --git a/lib/ansiblelint/cli.py b/lib/ansiblelint/cli.py
new file mode 100644
index 0000000..6b39561
--- /dev/null
+++ b/lib/ansiblelint/cli.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+"""CLI parser setup and helpers."""
+import argparse
+import logging
+import os
+import sys
+from pathlib import Path
+from typing import List, NamedTuple
+
+import yaml
+
+from ansiblelint.constants import DEFAULT_RULESDIR, INVALID_CONFIG_RC
+from ansiblelint.utils import expand_path_vars
+from ansiblelint.version import __version__
+
+_logger = logging.getLogger(__name__)
+_PATH_VARS = ['exclude_paths', 'rulesdir', ]
+
+
+def abspath(path: str, base_dir: str) -> str:
+ """Make relative path absolute relative to given directory.
+
+ Args:
+ path (str): the path to make absolute
+ base_dir (str): the directory from which make relative paths
+ absolute
+ default_drive: Windows drive to use to make the path
+ absolute if none is given.
+ """
+ if not os.path.isabs(path):
+ # Don't use abspath as it assumes path is relative to cwd.
+ # We want it relative to base_dir.
+ path = os.path.join(base_dir, path)
+
+ return os.path.normpath(path)
+
+
+def expand_to_normalized_paths(config: dict, base_dir: str = None) -> None:
+ # config can be None (-c /dev/null)
+ if not config:
+ return
+ base_dir = base_dir or os.getcwd()
+ for paths_var in _PATH_VARS:
+ if paths_var not in config:
+ continue # Cause we don't want to add a variable not present
+
+ normalized_paths = []
+ for path in config.pop(paths_var):
+ normalized_path = abspath(expand_path_vars(path), base_dir=base_dir)
+
+ normalized_paths.append(normalized_path)
+
+ config[paths_var] = normalized_paths
+
+
+def load_config(config_file: str) -> dict:
+ config_path = os.path.abspath(config_file or '.ansible-lint')
+
+ if config_file:
+ if not os.path.exists(config_path):
+ _logger.error("Config file not found '%s'", config_path)
+ sys.exit(INVALID_CONFIG_RC)
+ elif not os.path.exists(config_path):
+ # a missing default config file should not trigger an error
+ return {}
+
+ try:
+ with open(config_path, "r") as stream:
+ config = yaml.safe_load(stream)
+ except yaml.YAMLError as e:
+ _logger.error(e)
+ sys.exit(INVALID_CONFIG_RC)
+ # TODO(ssbarnea): implement schema validation for config file
+ if isinstance(config, list):
+ _logger.error(
+ "Invalid configuration '%s', expected YAML mapping in the config file.",
+ config_path)
+ sys.exit(INVALID_CONFIG_RC)
+
+ config_dir = os.path.dirname(config_path)
+ expand_to_normalized_paths(config, config_dir)
+ return config
+
+
+class AbspathArgAction(argparse.Action):
+ def __call__(self, parser, namespace, values, option_string=None):
+ if isinstance(values, (str, Path)):
+ values = [values]
+ normalized_values = [Path(expand_path_vars(path)).resolve() for path in values]
+ previous_values = getattr(namespace, self.dest, [])
+ setattr(namespace, self.dest, previous_values + normalized_values)
+
+
+def get_cli_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser()
+
+ parser.add_argument('-L', dest='listrules', default=False,
+ action='store_true', help="list all the rules")
+ parser.add_argument('-f', dest='format', default='rich',
+ choices=['rich', 'plain', 'rst'],
+ help="Format used rules output, (default: %(default)s)")
+ parser.add_argument('-q', dest='quiet',
+ default=False,
+ action='store_true',
+ help="quieter, although not silent output")
+ parser.add_argument('-p', dest='parseable',
+ default=False,
+ action='store_true',
+ help="parseable output in the format of pep8")
+ parser.add_argument('--parseable-severity', dest='parseable_severity',
+ default=False,
+ action='store_true',
+ help="parseable output including severity of rule")
+ parser.add_argument('--progressive', dest='progressive',
+ default=False,
+ action='store_true',
+ help="Return success if it detects a reduction in number"
+ " of violations compared with previous git commit. This "
+ "feature works only in git repositories.")
+ parser.add_argument('-r', action=AbspathArgAction, dest='rulesdir',
+ default=[], type=Path,
+ help="Specify custom rule directories. Add -R "
+ f"to keep using embedded rules from {DEFAULT_RULESDIR}")
+ parser.add_argument('-R', action='store_true',
+ default=False,
+ dest='use_default_rules',
+ help="Keep default rules when using -r")
+ parser.add_argument('--show-relpath', dest='display_relative_path', action='store_false',
+ default=True,
+ help="Display path relative to CWD")
+ parser.add_argument('-t', dest='tags',
+ action='append',
+ default=[],
+ help="only check rules whose id/tags match these values")
+ parser.add_argument('-T', dest='listtags', action='store_true',
+ help="list all the tags")
+ parser.add_argument('-v', dest='verbosity', action='count',
+ help="Increase verbosity level",
+ default=0)
+ parser.add_argument('-x', dest='skip_list', default=[], action='append',
+ help="only check rules whose id/tags do not "
+ "match these values")
+ parser.add_argument('-w', dest='warn_list', default=[], action='append',
+ help="only warn about these rules, unless overridden in "
+ "config file defaults to 'experimental'")
+ parser.add_argument('--nocolor', dest='colored',
+ default=hasattr(sys.stdout, 'isatty') and sys.stdout.isatty(),
+ action='store_false',
+ help="disable colored output")
+ parser.add_argument('--force-color', dest='colored',
+ action='store_true',
+ help="Try force colored output (relying on ansible's code)")
+ parser.add_argument('--exclude', dest='exclude_paths',
+ action=AbspathArgAction,
+ type=Path, default=[],
+ help='path to directories or files to skip. '
+ 'This option is repeatable.',
+ )
+ parser.add_argument('-c', dest='config_file',
+ help='Specify configuration file to use. '
+ 'Defaults to ".ansible-lint"')
+ parser.add_argument('--version', action='version',
+ version='%(prog)s {ver!s}'.format(ver=__version__),
+ )
+ parser.add_argument(dest='playbook', nargs='*',
+ help="One or more files or paths. When missing it will "
+ " enable auto-detection mode.")
+
+ return parser
+
+
+def merge_config(file_config, cli_config) -> NamedTuple:
+ bools = (
+ 'display_relative_path',
+ 'parseable',
+ 'parseable_severity',
+ 'quiet',
+ 'use_default_rules',
+ )
+ # maps lists to their default config values
+ lists_map = {
+ 'exclude_paths': [],
+ 'rulesdir': [],
+ 'skip_list': [],
+ 'tags': [],
+ 'warn_list': ['experimental'],
+ }
+
+ if not file_config:
+ return cli_config
+
+ for entry in bools:
+ x = getattr(cli_config, entry) or file_config.get(entry, False)
+ setattr(cli_config, entry, x)
+
+ for entry, default in lists_map.items():
+ getattr(cli_config, entry).extend(file_config.get(entry, default))
+
+ if 'verbosity' in file_config:
+ cli_config.verbosity = (cli_config.verbosity +
+ file_config['verbosity'])
+
+ return cli_config
+
+
+def get_config(arguments: List[str]):
+ parser = get_cli_parser()
+ options = parser.parse_args(arguments)
+
+ config = load_config(options.config_file)
+
+ return merge_config(config, options)
+
+
+def print_help(file=sys.stdout):
+ get_cli_parser().print_help(file=file)
+
+
+# vim: et:sw=4:syntax=python:ts=4:
diff --git a/lib/ansiblelint/color.py b/lib/ansiblelint/color.py
new file mode 100644
index 0000000..b30a89c
--- /dev/null
+++ b/lib/ansiblelint/color.py
@@ -0,0 +1,31 @@
+"""Console coloring and terminal support."""
+import sys
+from enum import Enum
+
+from rich.console import Console
+from rich.theme import Theme
+
+_theme = Theme({
+ "info": "cyan",
+ "warning": "dim yellow",
+ "danger": "bold red",
+ "title": "yellow"
+})
+console = Console(theme=_theme)
+console_stderr = Console(file=sys.stderr, theme=_theme)
+
+
+class Color(Enum):
+ """Color styles."""
+
+ reset = "0"
+ error_code = "1;31" # bright red
+ error_title = "0;31" # red
+ filename = "0;34" # blue
+ linenumber = "0;36" # cyan
+ line = "0;35" # purple
+
+
+def colorize(text: str, color: Color) -> str:
+ """Return ANSI formated string."""
+ return f"\u001b[{color.value}m{text}\u001b[{Color.reset.value}m"
diff --git a/lib/ansiblelint/constants.py b/lib/ansiblelint/constants.py
new file mode 100644
index 0000000..89094f9
--- /dev/null
+++ b/lib/ansiblelint/constants.py
@@ -0,0 +1,18 @@
+"""Constants used by AnsibleLint."""
+import os.path
+import sys
+
+# mypy/pylint idiom for py36-py38 compatibility
+# https://github.com/python/typeshed/issues/3500#issuecomment-560958608
+if sys.version_info >= (3, 8):
+ from typing import Literal # pylint: disable=no-name-in-module
+else:
+ from typing_extensions import Literal
+
+DEFAULT_RULESDIR = os.path.join(os.path.dirname(__file__), 'rules')
+CUSTOM_RULESDIR_ENVVAR = "ANSIBLE_LINT_CUSTOM_RULESDIR"
+
+INVALID_CONFIG_RC = 2
+ANSIBLE_FAILURE_RC = 3
+
+FileType = Literal["playbook", "pre_tasks", "post_tasks"]
diff --git a/lib/ansiblelint/errors.py b/lib/ansiblelint/errors.py
new file mode 100644
index 0000000..8569dca
--- /dev/null
+++ b/lib/ansiblelint/errors.py
@@ -0,0 +1,81 @@
+"""Exceptions and error representations."""
+import functools
+
+from ansiblelint.file_utils import normpath
+
+
+@functools.total_ordering
+class MatchError(ValueError):
+ """Rule violation detected during linting.
+
+ It can be raised as Exception but also just added to the list of found
+ rules violations.
+
+ Note that line argument is not considered when building hash of an
+ instance.
+ """
+
+ # IMPORTANT: any additional comparison protocol methods must return
+ # IMPORTANT: `NotImplemented` singleton to allow the check to use the
+ # IMPORTANT: other object's fallbacks.
+ # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__
+
+ def __init__(
+ self,
+ message=None,
+ linenumber=0,
+ details: str = "",
+ filename=None,
+ rule=None) -> None:
+ """Initialize a MatchError instance."""
+ super().__init__(message)
+
+ if not (message or rule):
+ raise TypeError(
+ f'{self.__class__.__name__}() missing a '
+ "required argument: one of 'message' or 'rule'",
+ )
+
+ self.message = message or getattr(rule, 'shortdesc', "")
+ self.linenumber = linenumber
+ self.details = details
+ self.filename = normpath(filename) if filename else None
+ self.rule = rule
+ self.ignored = False # If set it will be displayed but not counted as failure
+
+ def __repr__(self):
+ """Return a MatchError instance representation."""
+ formatstr = u"[{0}] ({1}) matched {2}:{3} {4}"
+ # note that `rule.id` can be int, str or even missing, as users
+ # can defined their own custom rules.
+ _id = getattr(self.rule, "id", "000")
+
+ return formatstr.format(_id, self.message,
+ self.filename, self.linenumber, self.details)
+
+ @property
+ def _hash_key(self):
+ # line attr is knowingly excluded, as dict is not hashable
+ return (
+ self.filename,
+ self.linenumber,
+ str(getattr(self.rule, 'id', 0)),
+ self.message,
+ self.details,
+ )
+
+ def __lt__(self, other):
+ """Return whether the current object is less than the other."""
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self._hash_key < other._hash_key
+
+ def __hash__(self):
+ """Return a hash value of the MatchError instance."""
+ return hash(self._hash_key)
+
+ def __eq__(self, other):
+ """Identify whether the other object represents the same rule match."""
+ if not isinstance(other, self.__class__):
+ return NotImplemented
+ return self.__hash__() == other.__hash__()
diff --git a/lib/ansiblelint/file_utils.py b/lib/ansiblelint/file_utils.py
new file mode 100644
index 0000000..f25382f
--- /dev/null
+++ b/lib/ansiblelint/file_utils.py
@@ -0,0 +1,25 @@
+"""Utility functions related to file operations."""
+import os
+from contextlib import contextmanager
+
+
+def normpath(path) -> str:
+ """
+ Normalize a path in order to provide a more consistent output.
+
+ Currently it generates a relative path but in the future we may want to
+ make this user configurable.
+ """
+ # convertion to string in order to allow receiving non string objects
+ return os.path.relpath(str(path))
+
+
+@contextmanager
+def cwd(path):
+ """Context manager for temporary changing current working directory."""
+ old_pwd = os.getcwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(old_pwd)
diff --git a/lib/ansiblelint/formatters/__init__.py b/lib/ansiblelint/formatters/__init__.py
new file mode 100644
index 0000000..7395183
--- /dev/null
+++ b/lib/ansiblelint/formatters/__init__.py
@@ -0,0 +1,167 @@
+"""Output formatters."""
+import os
+from pathlib import Path
+from typing import TYPE_CHECKING, Generic, TypeVar, Union
+
+from ansiblelint.color import Color, colorize
+
+if TYPE_CHECKING:
+ from ansiblelint.errors import MatchError
+
+T = TypeVar('T', bound='BaseFormatter')
+
+
+class BaseFormatter(Generic[T]):
+ """Formatter of ansible-lint output.
+
+ Base class for output formatters.
+
+ Args:
+ base_dir (str|Path): reference directory against which display relative path.
+ display_relative_path (bool): whether to show path as relative or absolute
+ """
+
+ def __init__(self, base_dir: Union[str, Path], display_relative_path: bool) -> None:
+ """Initialize a BaseFormatter instance."""
+ if isinstance(base_dir, str):
+ base_dir = Path(base_dir)
+ if base_dir: # can be None
+ base_dir = base_dir.absolute()
+
+ # Required 'cause os.path.relpath() does not accept Path before 3.6
+ if isinstance(base_dir, Path):
+ base_dir = str(base_dir) # Drop when Python 3.5 is no longer supported
+
+ self._base_dir = base_dir if display_relative_path else None
+
+ def _format_path(self, path: Union[str, Path]) -> str:
+ # Required 'cause os.path.relpath() does not accept Path before 3.6
+ if isinstance(path, Path):
+ path = str(path) # Drop when Python 3.5 is no longer supported
+
+ if not self._base_dir:
+ return path
+ # Use os.path.relpath 'cause Path.relative_to() misbehaves
+ return os.path.relpath(path, start=self._base_dir)
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ return str(match)
+
+
+class Formatter(BaseFormatter):
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ formatstr = u"{0} {1}\n{2}:{3}\n{4}\n"
+ _id = getattr(match.rule, 'id', '000')
+ if colored:
+ return formatstr.format(
+ colorize(u"[{0}]".format(_id), Color.error_code),
+ colorize(match.message, Color.error_title),
+ colorize(self._format_path(match.filename or ""), Color.filename),
+ colorize(str(match.linenumber), Color.linenumber),
+ colorize(u"{0}".format(match.details), Color.line))
+ else:
+ return formatstr.format(_id,
+ match.message,
+ match.filename or "",
+ match.linenumber,
+ match.details)
+
+
+class QuietFormatter(BaseFormatter):
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ formatstr = u"{0} {1}:{2}"
+ if colored:
+ return formatstr.format(
+ colorize(u"[{0}]".format(match.rule.id), Color.error_code),
+ colorize(self._format_path(match.filename or ""), Color.filename),
+ colorize(str(match.linenumber), Color.linenumber))
+ else:
+ return formatstr.format(match.rule.id, self._format_path(match.filename or ""),
+ match.linenumber)
+
+
+class ParseableFormatter(BaseFormatter):
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ formatstr = u"{0}:{1}: [{2}] {3}"
+ if colored:
+ return formatstr.format(
+ colorize(self._format_path(match.filename or ""), Color.filename),
+ colorize(str(match.linenumber), Color.linenumber),
+ colorize(u"E{0}".format(match.rule.id), Color.error_code),
+ colorize(u"{0}".format(match.message), Color.error_title))
+ else:
+ return formatstr.format(self._format_path(match.filename or ""),
+ match.linenumber,
+ "E" + match.rule.id,
+ match.message)
+
+
+class AnnotationsFormatter(BaseFormatter):
+ # https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message
+ """Formatter for emitting violations as GitHub Workflow Commands.
+
+ These commands trigger the GHA Workflow runners platform to post violations
+ in a form of GitHub Checks API annotations that appear rendered in pull-
+ request files view.
+
+ ::debug file={name},line={line},col={col},severity={severity}::{message}
+ ::warning file={name},line={line},col={col},severity={severity}::{message}
+ ::error file={name},line={line},col={col},severity={severity}::{message}
+
+ Supported levels: debug, warning, error
+ """
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ """Prepare a match instance for reporting as a GitHub Actions annotation."""
+ if colored:
+ raise ValueError('The colored mode is not supported.')
+
+ level = self._severity_to_level(match.rule.severity)
+ file_path = self._format_path(match.filename or "")
+ line_num = match.linenumber
+ rule_id = match.rule.id
+ severity = match.rule.severity
+ violation_details = match.message
+ return (
+ f"::{level} file={file_path},line={line_num},severity={severity}"
+ f"::[E{rule_id}] {violation_details}"
+ )
+
+ @staticmethod
+ def _severity_to_level(severity: str) -> str:
+ if severity in ['VERY_LOW', 'LOW']:
+ return 'warning'
+ elif severity in ['INFO']:
+ return 'debug'
+ # ['MEDIUM', 'HIGH', 'VERY_HIGH'] or anything else
+ return 'error'
+
+
+class ParseableSeverityFormatter(BaseFormatter):
+
+ def format(self, match: "MatchError", colored: bool = False) -> str:
+ formatstr = u"{0}:{1}: [{2}] [{3}] {4}"
+
+ filename = self._format_path(match.filename or "")
+ linenumber = str(match.linenumber)
+ rule_id = u"E{0}".format(match.rule.id)
+ severity = match.rule.severity
+ message = str(match.message)
+
+ if colored:
+ filename = colorize(filename, Color.filename)
+ linenumber = colorize(linenumber, Color.linenumber)
+ rule_id = colorize(rule_id, Color.error_code)
+ severity = colorize(severity, Color.error_code)
+ message = colorize(message, Color.error_title)
+
+ return formatstr.format(
+ filename,
+ linenumber,
+ rule_id,
+ severity,
+ message,
+ )
diff --git a/lib/ansiblelint/generate_docs.py b/lib/ansiblelint/generate_docs.py
new file mode 100644
index 0000000..b735b1f
--- /dev/null
+++ b/lib/ansiblelint/generate_docs.py
@@ -0,0 +1,66 @@
+"""Utils to generate rule table .rst documentation."""
+import logging
+from typing import Iterable
+
+from rich import box
+from rich.console import render_group
+from rich.markdown import Markdown
+from rich.table import Table
+
+from ansiblelint.rules import RulesCollection
+
+DOC_HEADER = """
+.. _lint_default_rules:
+
+Default Rules
+=============
+
+.. contents::
+ :local:
+
+Below you can see the list of default rules Ansible Lint use to evaluate playbooks and roles:
+
+"""
+
+_logger = logging.getLogger(__name__)
+
+
+def rules_as_rst(rules: RulesCollection) -> str:
+ """Return RST documentation for a list of rules."""
+ r = DOC_HEADER
+
+ for d in rules:
+ if not hasattr(d, 'id'):
+ _logger.warning(
+ "Rule %s skipped from being documented as it does not have an `id` attribute.",
+ d.__class__.__name__)
+ continue
+
+ if d.id.endswith('01'):
+
+ section = '{} Rules ({}xx)'.format(
+ d.tags[0].title(),
+ d.id[-3:-2])
+ r += f'\n\n{section}\n{ "-" * len(section) }'
+
+ title = f"{d.id}: {d.shortdesc}"
+ r += f"\n\n.. _{d.id}:\n\n{title}\n{'*' * len(title)}\n\n{d.description}"
+
+ return r
+
+
+@render_group()
+def rules_as_rich(rules: RulesCollection) -> Iterable[Table]:
+ """Print documentation for a list of rules, returns empty string."""
+ for rule in rules:
+ table = Table(show_header=True, header_style="title", box=box.MINIMAL)
+ table.add_column(rule.id, style="dim", width=16)
+ table.add_column(Markdown(rule.shortdesc))
+ table.add_row("description", Markdown(rule.description))
+ if rule.version_added:
+ table.add_row("version_added", rule.version_added)
+ if rule.tags:
+ table.add_row("tags", ", ".join(rule.tags))
+ if rule.severity:
+ table.add_row("severity", rule.severity)
+ yield table
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."""
diff --git a/lib/ansiblelint/runner.py b/lib/ansiblelint/runner.py
new file mode 100644
index 0000000..f73945f
--- /dev/null
+++ b/lib/ansiblelint/runner.py
@@ -0,0 +1,111 @@
+"""Runner implementation."""
+import logging
+import os
+from typing import TYPE_CHECKING, Any, FrozenSet, Generator, List, Optional, Set
+
+import ansiblelint.file_utils
+import ansiblelint.skip_utils
+import ansiblelint.utils
+from ansiblelint.errors import MatchError
+from ansiblelint.rules.LoadingFailureRule import LoadingFailureRule
+
+if TYPE_CHECKING:
+ from ansiblelint.rules import RulesCollection
+
+
+_logger = logging.getLogger(__name__)
+
+
+class Runner(object):
+ """Runner class performs the linting process."""
+
+ def __init__(
+ self,
+ rules: "RulesCollection",
+ playbook: str,
+ tags: FrozenSet[Any] = frozenset(),
+ skip_list: Optional[FrozenSet[Any]] = frozenset(),
+ exclude_paths: List[str] = [],
+ verbosity: int = 0,
+ checked_files: Set[str] = None) -> None:
+ """Initialize a Runner instance."""
+ self.rules = rules
+ self.playbooks = set()
+ # assume role if directory
+ if os.path.isdir(playbook):
+ self.playbooks.add((os.path.join(playbook, ''), 'role'))
+ self.playbook_dir = playbook
+ else:
+ self.playbooks.add((playbook, 'playbook'))
+ self.playbook_dir = os.path.dirname(playbook)
+ self.tags = tags
+ self.skip_list = skip_list
+ self._update_exclude_paths(exclude_paths)
+ self.verbosity = verbosity
+ if checked_files is None:
+ checked_files = set()
+ self.checked_files = checked_files
+
+ def _update_exclude_paths(self, exclude_paths: List[str]) -> None:
+ if exclude_paths:
+ # These will be (potentially) relative paths
+ paths = ansiblelint.utils.expand_paths_vars(exclude_paths)
+ # Since ansiblelint.utils.find_children returns absolute paths,
+ # and the list of files we create in `Runner.run` can contain both
+ # relative and absolute paths, we need to cover both bases.
+ self.exclude_paths = paths + [os.path.abspath(p) for p in paths]
+ else:
+ self.exclude_paths = []
+
+ def is_excluded(self, file_path: str) -> bool:
+ """Verify if a file path should be excluded."""
+ # Any will short-circuit as soon as something returns True, but will
+ # be poor performance for the case where the path under question is
+ # not excluded.
+ return any(file_path.startswith(path) for path in self.exclude_paths)
+
+ def run(self) -> List[MatchError]:
+ """Execute the linting process."""
+ files = list()
+ for playbook in self.playbooks:
+ if self.is_excluded(playbook[0]) or playbook[1] == 'role':
+ continue
+ files.append({'path': ansiblelint.file_utils.normpath(playbook[0]),
+ 'type': playbook[1],
+ # add an absolute path here, so rules are able to validate if
+ # referenced files exist
+ 'absolute_directory': os.path.dirname(playbook[0])})
+ matches = set(self._emit_matches(files))
+
+ # remove duplicates from files list
+ files = [value for n, value in enumerate(files) if value not in files[:n]]
+
+ # remove files that have already been checked
+ files = [x for x in files if x['path'] not in self.checked_files]
+ for file in files:
+ _logger.debug(
+ "Examining %s of type %s",
+ ansiblelint.file_utils.normpath(file['path']),
+ file['type'])
+ matches = matches.union(
+ self.rules.run(file, tags=set(self.tags),
+ skip_list=self.skip_list))
+ # update list of checked files
+ self.checked_files.update([x['path'] for x in files])
+
+ return sorted(matches)
+
+ def _emit_matches(self, files: List) -> Generator[MatchError, None, None]:
+ visited: Set = set()
+ while visited != self.playbooks:
+ for arg in self.playbooks - visited:
+ try:
+ for child in ansiblelint.utils.find_children(arg, self.playbook_dir):
+ if self.is_excluded(child['path']):
+ continue
+ self.playbooks.add((child['path'], child['type']))
+ files.append(child)
+ except MatchError as e:
+ e.rule = LoadingFailureRule
+ yield e
+ visited.add(arg)
diff --git a/lib/ansiblelint/skip_utils.py b/lib/ansiblelint/skip_utils.py
new file mode 100644
index 0000000..c3c0a88
--- /dev/null
+++ b/lib/ansiblelint/skip_utils.py
@@ -0,0 +1,189 @@
+# (c) 2019–2020, Ansible by Red Hat
+#
+# 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.
+
+"""Utils related to inline skipping of rules."""
+import logging
+from functools import lru_cache
+from itertools import product
+from typing import Any, Generator, List, Sequence
+
+import ruamel.yaml
+
+from ansiblelint.constants import FileType
+
+INLINE_SKIP_FLAG = '# noqa '
+
+_logger = logging.getLogger(__name__)
+
+
+# playbook: Sequence currently expects only instances of one of the two
+# classes below but we should consider avoiding this chimera.
+# ruamel.yaml.comments.CommentedSeq
+# ansible.parsing.yaml.objects.AnsibleSequence
+
+
+def get_rule_skips_from_line(line: str) -> List:
+ """Return list of rule ids skipped via comment on the line of yaml."""
+ _before_noqa, _noqa_marker, noqa_text = line.partition(INLINE_SKIP_FLAG)
+ return noqa_text.split()
+
+
+def append_skipped_rules(pyyaml_data: str, file_text: str, file_type: FileType) -> Sequence:
+ """Append 'skipped_rules' to individual tasks or single metadata block.
+
+ For a file, uses 2nd parser (ruamel.yaml) to pull comments out of
+ yaml subsets, check for '# noqa' skipped rules, and append any skips to the
+ original parser (pyyaml) data relied on by remainder of ansible-lint.
+
+ :param pyyaml_data: file text parsed via ansible and pyyaml.
+ :param file_text: raw file text.
+ :param file_type: type of file: tasks, handlers or meta.
+ :returns: original pyyaml_data altered with a 'skipped_rules' list added
+ to individual tasks, or added to the single metadata block.
+ """
+ try:
+ yaml_skip = _append_skipped_rules(pyyaml_data, file_text, file_type)
+ except RuntimeError:
+ # Notify user of skip error, do not stop, do not change exit code
+ _logger.error('Error trying to append skipped rules', exc_info=True)
+ return pyyaml_data
+ return yaml_skip
+
+
+@lru_cache(maxsize=128)
+def load_data(file_text: str) -> Any:
+ """Parse `file_text` as yaml and return parsed structure.
+
+ This is the main culprit for slow performance, each rule asks for loading yaml again and again
+ ideally the `maxsize` on the decorator above MUST be great or equal total number of rules
+ :param file_text: raw text to parse
+ :return: Parsed yaml
+ """
+ yaml = ruamel.yaml.YAML()
+ return yaml.load(file_text)
+
+
+def _append_skipped_rules(pyyaml_data: Sequence, file_text: str, file_type: FileType) -> Sequence:
+ # parse file text using 2nd parser library
+ ruamel_data = load_data(file_text)
+
+ if file_type == 'meta':
+ pyyaml_data[0]['skipped_rules'] = _get_rule_skips_from_yaml(ruamel_data)
+ return pyyaml_data
+
+ # create list of blocks of tasks or nested tasks
+ if file_type in ('tasks', 'handlers'):
+ ruamel_task_blocks = ruamel_data
+ pyyaml_task_blocks = pyyaml_data
+ elif file_type in ('playbook', 'pre_tasks', 'post_tasks'):
+ try:
+ pyyaml_task_blocks = _get_task_blocks_from_playbook(pyyaml_data)
+ ruamel_task_blocks = _get_task_blocks_from_playbook(ruamel_data)
+ except (AttributeError, TypeError):
+ # TODO(awcrosby): running ansible-lint on any .yml file will
+ # assume it is a playbook, check needs to be added higher in the
+ # call stack, and can remove this except
+ return pyyaml_data
+ else:
+ raise RuntimeError('Unexpected file type: {}'.format(file_type))
+
+ # get tasks from blocks of tasks
+ pyyaml_tasks = _get_tasks_from_blocks(pyyaml_task_blocks)
+ ruamel_tasks = _get_tasks_from_blocks(ruamel_task_blocks)
+
+ # append skipped_rules for each task
+ for ruamel_task, pyyaml_task in zip(ruamel_tasks, pyyaml_tasks):
+
+ # ignore empty tasks
+ if not pyyaml_task and not ruamel_task:
+ continue
+
+ if pyyaml_task.get('name') != ruamel_task.get('name'):
+ raise RuntimeError('Error in matching skip comment to a task')
+ pyyaml_task['skipped_rules'] = _get_rule_skips_from_yaml(ruamel_task)
+
+ return pyyaml_data
+
+
+def _get_task_blocks_from_playbook(playbook: Sequence) -> List:
+ """Return parts of playbook that contains tasks, and nested tasks.
+
+ :param playbook: playbook yaml from yaml parser.
+ :returns: list of task dictionaries.
+ """
+ PLAYBOOK_TASK_KEYWORDS = [
+ 'tasks',
+ 'pre_tasks',
+ 'post_tasks',
+ 'handlers',
+ ]
+
+ task_blocks = []
+ for play, key in product(playbook, PLAYBOOK_TASK_KEYWORDS):
+ task_blocks.extend(play.get(key, []))
+ return task_blocks
+
+
+def _get_tasks_from_blocks(task_blocks: Sequence) -> Generator:
+ """Get list of tasks from list made of tasks and nested tasks."""
+ NESTED_TASK_KEYS = [
+ 'block',
+ 'always',
+ 'rescue',
+ ]
+
+ def get_nested_tasks(task: Any) -> Generator[Any, None, None]:
+ return (
+ subtask
+ for k in NESTED_TASK_KEYS if task and k in task
+ for subtask in task[k]
+ )
+
+ for task in task_blocks:
+ for sub_task in get_nested_tasks(task):
+ yield sub_task
+ yield task
+
+
+def _get_rule_skips_from_yaml(yaml_input: Sequence) -> Sequence:
+ """Traverse yaml for comments with rule skips and return list of rules."""
+ yaml_comment_obj_strs = []
+
+ def traverse_yaml(obj: Any) -> None:
+ yaml_comment_obj_strs.append(str(obj.ca.items))
+ if isinstance(obj, dict):
+ for key, val in obj.items():
+ if isinstance(val, (dict, list)):
+ traverse_yaml(val)
+ elif isinstance(obj, list):
+ for e in obj:
+ if isinstance(e, (dict, list)):
+ traverse_yaml(e)
+ else:
+ return
+
+ traverse_yaml(yaml_input)
+
+ rule_id_list = []
+ for comment_obj_str in yaml_comment_obj_strs:
+ for line in comment_obj_str.split(r'\n'):
+ rule_id_list.extend(get_rule_skips_from_line(line))
+
+ return rule_id_list
diff --git a/lib/ansiblelint/testing/__init__.py b/lib/ansiblelint/testing/__init__.py
new file mode 100644
index 0000000..1ed686f
--- /dev/null
+++ b/lib/ansiblelint/testing/__init__.py
@@ -0,0 +1,84 @@
+"""Test utils for ansible-lint."""
+
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+from typing import TYPE_CHECKING, Dict, List
+
+from ansible import __version__ as ansible_version_str
+
+from ansiblelint.runner import Runner
+
+if TYPE_CHECKING:
+ from ansiblelint.errors import MatchError
+
+
+ANSIBLE_MAJOR_VERSION = tuple(map(int, ansible_version_str.split('.')[:2]))
+
+
+class RunFromText(object):
+ """Use Runner on temp files created from unittest text snippets."""
+
+ def __init__(self, collection):
+ """Initialize a RunFromText instance with rules collection."""
+ self.collection = collection
+
+ def _call_runner(self, path) -> List["MatchError"]:
+ runner = Runner(self.collection, path)
+ return runner.run()
+
+ def run_playbook(self, playbook_text):
+ """Lints received text as a playbook."""
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yml", prefix="playbook") as fp:
+ fp.write(playbook_text)
+ fp.flush()
+ results = self._call_runner(fp.name)
+ return results
+
+ def run_role_tasks_main(self, tasks_main_text):
+ """Lints received text as tasks."""
+ role_path = tempfile.mkdtemp(prefix='role_')
+ tasks_path = os.path.join(role_path, 'tasks')
+ os.makedirs(tasks_path)
+ with open(os.path.join(tasks_path, 'main.yml'), 'w') as fp:
+ fp.write(tasks_main_text)
+ results = self._call_runner(role_path)
+ shutil.rmtree(role_path)
+ return results
+
+ def run_role_meta_main(self, meta_main_text):
+ """Lints received text as meta."""
+ role_path = tempfile.mkdtemp(prefix='role_')
+ meta_path = os.path.join(role_path, 'meta')
+ os.makedirs(meta_path)
+ with open(os.path.join(meta_path, 'main.yml'), 'w') as fp:
+ fp.write(meta_main_text)
+ results = self._call_runner(role_path)
+ shutil.rmtree(role_path)
+ return results
+
+
+def run_ansible_lint(
+ *argv: str,
+ cwd: str = None,
+ bin: str = None,
+ env: Dict[str, str] = None) -> subprocess.CompletedProcess:
+ """Run ansible-lint on a given path and returns its output."""
+ if not bin:
+ bin = sys.executable
+ args = [sys.executable, "-m", "ansiblelint", *argv]
+ else:
+ args = [bin, *argv]
+
+ return subprocess.run(
+ args,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ shell=False, # needed when command is a list
+ check=False,
+ cwd=cwd,
+ env=env,
+ universal_newlines=True
+ )
diff --git a/lib/ansiblelint/utils.py b/lib/ansiblelint/utils.py
new file mode 100644
index 0000000..feac4d7
--- /dev/null
+++ b/lib/ansiblelint/utils.py
@@ -0,0 +1,836 @@
+# 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.
+"""Generic utility helpers."""
+
+import contextlib
+import inspect
+import logging
+import os
+import pprint
+import subprocess
+from argparse import Namespace
+from collections import OrderedDict
+from functools import lru_cache
+from pathlib import Path
+from typing import Callable, ItemsView, List, Optional, Tuple
+
+import yaml
+from ansible import constants
+from ansible.errors import AnsibleError, AnsibleParserError
+from ansible.parsing.dataloader import DataLoader
+from ansible.parsing.mod_args import ModuleArgsParser
+from ansible.parsing.splitter import split_args
+from ansible.parsing.yaml.constructor import AnsibleConstructor
+from ansible.parsing.yaml.loader import AnsibleLoader
+from ansible.parsing.yaml.objects import AnsibleSequence
+from ansible.plugins.loader import add_all_plugin_dirs
+from ansible.template import Templar
+from yaml.composer import Composer
+from yaml.representer import RepresenterError
+
+from ansiblelint.constants import (
+ ANSIBLE_FAILURE_RC, CUSTOM_RULESDIR_ENVVAR, DEFAULT_RULESDIR, FileType,
+)
+from ansiblelint.errors import MatchError
+from ansiblelint.file_utils import normpath
+
+# ansible-lint doesn't need/want to know about encrypted secrets, so we pass a
+# string as the password to enable such yaml files to be opened and parsed
+# successfully.
+DEFAULT_VAULT_PASSWORD = 'x'
+
+PLAYBOOK_DIR = os.environ.get('ANSIBLE_PLAYBOOK_DIR', None)
+
+
+_logger = logging.getLogger(__name__)
+
+
+def parse_yaml_from_file(filepath: str) -> dict:
+ dl = DataLoader()
+ if hasattr(dl, 'set_vault_password'):
+ dl.set_vault_password(DEFAULT_VAULT_PASSWORD)
+ return dl.load_from_file(filepath)
+
+
+def path_dwim(basedir: str, given: str) -> str:
+ dl = DataLoader()
+ dl.set_basedir(basedir)
+ return dl.path_dwim(given)
+
+
+def ansible_template(basedir, varname, templatevars, **kwargs):
+ dl = DataLoader()
+ dl.set_basedir(basedir)
+ templar = Templar(dl, variables=templatevars)
+ return templar.template(varname, **kwargs)
+
+
+LINE_NUMBER_KEY = '__line__'
+FILENAME_KEY = '__file__'
+
+VALID_KEYS = [
+ 'name', 'action', 'when', 'async', 'poll', 'notify',
+ 'first_available_file', 'include', 'include_tasks', 'import_tasks', 'import_playbook',
+ 'tags', 'register', 'ignore_errors', 'delegate_to',
+ 'local_action', 'transport', 'remote_user', 'sudo',
+ 'sudo_user', 'sudo_pass', 'when', 'connection', 'environment', 'args', 'always_run',
+ 'any_errors_fatal', 'changed_when', 'failed_when', 'check_mode', 'delay',
+ 'retries', 'until', 'su', 'su_user', 'su_pass', 'no_log', 'run_once',
+ 'become', 'become_user', 'become_method', FILENAME_KEY,
+]
+
+BLOCK_NAME_TO_ACTION_TYPE_MAP = {
+ 'tasks': 'task',
+ 'handlers': 'handler',
+ 'pre_tasks': 'task',
+ 'post_tasks': 'task',
+ 'block': 'meta',
+ 'rescue': 'meta',
+ 'always': 'meta',
+}
+
+
+def tokenize(line):
+ tokens = line.lstrip().split(" ")
+ if tokens[0] == '-':
+ tokens = tokens[1:]
+ if tokens[0] == 'action:' or tokens[0] == 'local_action:':
+ tokens = tokens[1:]
+ command = tokens[0].replace(":", "")
+
+ args = list()
+ kwargs = dict()
+ nonkvfound = False
+ for arg in tokens[1:]:
+ if "=" in arg and not nonkvfound:
+ kv = arg.split("=", 1)
+ kwargs[kv[0]] = kv[1]
+ else:
+ nonkvfound = True
+ args.append(arg)
+ return (command, args, kwargs)
+
+
+def _playbook_items(pb_data: dict) -> ItemsView:
+ if isinstance(pb_data, dict):
+ return pb_data.items()
+ elif not pb_data:
+ return []
+ else:
+ return [item for play in pb_data for item in play.items()]
+
+
+def _rebind_match_filename(filename: str, func) -> Callable:
+ def func_wrapper(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except MatchError as e:
+ e.filename = filename
+ raise e
+ return func_wrapper
+
+
+def _set_collections_basedir(basedir: str):
+ # Sets the playbook directory as playbook_paths for the collection loader
+ try:
+ # Ansible 2.10+
+ # noqa: # pylint:disable=cyclic-import,import-outside-toplevel
+ from ansible.utils.collection_loader import AnsibleCollectionConfig
+
+ AnsibleCollectionConfig.playbook_paths = basedir
+ except ImportError:
+ # Ansible 2.8 or 2.9
+ # noqa: # pylint:disable=cyclic-import,import-outside-toplevel
+ from ansible.utils.collection_loader import set_collection_playbook_paths
+
+ set_collection_playbook_paths(basedir)
+
+
+def find_children(playbook: Tuple[str, str], playbook_dir: str) -> List:
+ if not os.path.exists(playbook[0]):
+ return []
+ _set_collections_basedir(playbook_dir or '.')
+ add_all_plugin_dirs(playbook_dir or '.')
+ if playbook[1] == 'role':
+ playbook_ds = {'roles': [{'role': playbook[0]}]}
+ else:
+ try:
+ playbook_ds = parse_yaml_from_file(playbook[0])
+ except AnsibleError as e:
+ raise SystemExit(str(e))
+ results = []
+ basedir = os.path.dirname(playbook[0])
+ items = _playbook_items(playbook_ds)
+ for item in items:
+ for child in _rebind_match_filename(playbook[0], play_children)(
+ basedir, item, playbook[1], playbook_dir):
+ if "$" in child['path'] or "{{" in child['path']:
+ continue
+ valid_tokens = list()
+ for token in split_args(child['path']):
+ if '=' in token:
+ break
+ valid_tokens.append(token)
+ path = ' '.join(valid_tokens)
+ results.append({
+ 'path': path_dwim(basedir, path),
+ 'type': child['type']
+ })
+ return results
+
+
+def template(basedir, value, vars, fail_on_undefined=False, **kwargs):
+ try:
+ value = ansible_template(os.path.abspath(basedir), value, vars,
+ **dict(kwargs, fail_on_undefined=fail_on_undefined))
+ # Hack to skip the following exception when using to_json filter on a variable.
+ # I guess the filter doesn't like empty vars...
+ except (AnsibleError, ValueError, RepresenterError):
+ # templating failed, so just keep value as is.
+ pass
+ return value
+
+
+def play_children(basedir, item, parent_type, playbook_dir):
+ delegate_map = {
+ 'tasks': _taskshandlers_children,
+ 'pre_tasks': _taskshandlers_children,
+ 'post_tasks': _taskshandlers_children,
+ 'block': _taskshandlers_children,
+ 'include': _include_children,
+ 'import_playbook': _include_children,
+ 'roles': _roles_children,
+ 'dependencies': _roles_children,
+ 'handlers': _taskshandlers_children,
+ 'include_tasks': _include_children,
+ 'import_tasks': _include_children,
+ }
+ (k, v) = item
+ add_all_plugin_dirs(os.path.abspath(basedir))
+
+ if k in delegate_map:
+ if v:
+ v = template(os.path.abspath(basedir),
+ v,
+ dict(playbook_dir=PLAYBOOK_DIR or os.path.abspath(basedir)),
+ fail_on_undefined=False)
+ return delegate_map[k](basedir, k, v, parent_type)
+ return []
+
+
+def _include_children(basedir, k, v, parent_type):
+ # handle special case include_tasks: name=filename.yml
+ if k == 'include_tasks' and isinstance(v, dict) and 'file' in v:
+ v = v['file']
+
+ # handle include: filename.yml tags=blah
+ (command, args, kwargs) = tokenize("{0}: {1}".format(k, v))
+
+ result = path_dwim(basedir, args[0])
+ if not os.path.exists(result):
+ result = path_dwim(os.path.join(os.path.dirname(basedir)), v)
+ return [{'path': result, 'type': parent_type}]
+
+
+def _taskshandlers_children(basedir, k, v, parent_type: FileType) -> List:
+ results = []
+ for th in v:
+
+ # ignore empty tasks, `-`
+ if not th:
+ continue
+
+ with contextlib.suppress(LookupError):
+ children = _get_task_handler_children_for_tasks_or_playbooks(
+ th, basedir, k, parent_type,
+ )
+ results.append(children)
+ continue
+
+ if 'include_role' in th or 'import_role' in th: # lgtm [py/unreachable-statement]
+ th = normalize_task_v2(th)
+ _validate_task_handler_action_for_role(th['action'])
+ results.extend(_roles_children(basedir, k, [th['action'].get("name")],
+ parent_type,
+ main=th['action'].get('tasks_from', 'main')))
+ continue
+
+ if 'block' not in th:
+ continue
+
+ 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 _get_task_handler_children_for_tasks_or_playbooks(
+ task_handler, basedir: str, k, parent_type: FileType,
+) -> dict:
+ """Try to get children of taskhandler for include/import tasks/playbooks."""
+ child_type = k if parent_type == 'playbook' else parent_type
+
+ task_include_keys = 'include', 'include_tasks', 'import_playbook', 'import_tasks'
+ for task_handler_key in task_include_keys:
+
+ with contextlib.suppress(KeyError):
+
+ # ignore empty tasks
+ if not task_handler:
+ continue
+
+ return {
+ 'path': path_dwim(basedir, task_handler[task_handler_key]),
+ 'type': child_type,
+ }
+
+ raise LookupError(
+ f'The node contains none of: {", ".join(task_include_keys)}',
+ )
+
+
+def _validate_task_handler_action_for_role(th_action: dict) -> None:
+ """Verify that the task handler action is valid for role include."""
+ module = th_action['__ansible_module__']
+
+ if 'name' not in th_action:
+ raise MatchError(f"Failed to find required 'name' key in {module!s}")
+
+ if not isinstance(th_action['name'], str):
+ raise RuntimeError(
+ f"Value assigned to 'name' key on '{module!s}' is not a string.",
+ )
+
+
+def _roles_children(basedir: str, k, v, parent_type: FileType, main='main') -> list:
+ results = []
+ for role in v:
+ if isinstance(role, dict):
+ if 'role' in role or 'name' in role:
+ if 'tags' not in role or 'skip_ansible_lint' not in role['tags']:
+ results.extend(_look_for_role_files(basedir,
+ role.get('role', role.get('name')),
+ main=main))
+ elif k != 'dependencies':
+ raise SystemExit('role dict {0} does not contain a "role" '
+ 'or "name" key'.format(role))
+ else:
+ results.extend(_look_for_role_files(basedir, role, main=main))
+ return results
+
+
+def _rolepath(basedir: str, role: str) -> Optional[str]:
+ role_path = None
+
+ possible_paths = [
+ # if included from a playbook
+ path_dwim(basedir, os.path.join('roles', role)),
+ path_dwim(basedir, role),
+ # if included from roles/[role]/meta/main.yml
+ path_dwim(
+ basedir, os.path.join('..', '..', '..', 'roles', role)
+ ),
+ path_dwim(basedir, os.path.join('..', '..', role)),
+ ]
+
+ if constants.DEFAULT_ROLES_PATH:
+ search_locations = constants.DEFAULT_ROLES_PATH
+ if isinstance(search_locations, str):
+ search_locations = search_locations.split(os.pathsep)
+ for loc in search_locations:
+ loc = os.path.expanduser(loc)
+ possible_paths.append(path_dwim(loc, role))
+
+ possible_paths.append(path_dwim(basedir, ''))
+
+ for path_option in possible_paths:
+ if os.path.isdir(path_option):
+ role_path = path_option
+ break
+
+ if role_path:
+ add_all_plugin_dirs(role_path)
+
+ return role_path
+
+
+def _look_for_role_files(basedir: str, role: str, main='main') -> list:
+ role_path = _rolepath(basedir, role)
+ if not role_path:
+ return []
+
+ results = []
+
+ for th in ['tasks', 'handlers', 'meta']:
+ current_path = os.path.join(role_path, th)
+ for dir, subdirs, files in os.walk(current_path):
+ for file in files:
+ file_ignorecase = file.lower()
+ if file_ignorecase.endswith(('.yml', '.yaml')):
+ thpath = os.path.join(dir, file)
+ results.append({'path': thpath, 'type': th})
+
+ return results
+
+
+def rolename(filepath):
+ idx = filepath.find('roles/')
+ if idx < 0:
+ return ''
+ role = filepath[idx + 6:]
+ role = role[:role.find('/')]
+ return role
+
+
+def _kv_to_dict(v):
+ (command, args, kwargs) = tokenize(v)
+ return dict(__ansible_module__=command, __ansible_arguments__=args, **kwargs)
+
+
+def _sanitize_task(task: dict) -> dict:
+ """Return a stripped-off task structure compatible with new Ansible.
+
+ This helper takes a copy of the incoming task and drops
+ any internally used keys from it.
+ """
+ result = task.copy()
+ # task is an AnsibleMapping which inherits from OrderedDict, so we need
+ # to use `del` to remove unwanted keys.
+ for k in ['skipped_rules', FILENAME_KEY, LINE_NUMBER_KEY]:
+ if k in result:
+ del result[k]
+ return result
+
+
+# FIXME: drop noqa once this function is made simpler
+# Ref: https://github.com/ansible/ansible-lint/issues/744
+def normalize_task_v2(task: dict) -> dict: # noqa: C901
+ """Ensure tasks have an action key and strings are converted to python objects."""
+ result = dict()
+ if 'always_run' in task:
+ # FIXME(ssbarnea): Delayed import to avoid circular import
+ # See https://github.com/ansible/ansible-lint/issues/880
+ # noqa: # pylint:disable=cyclic-import,import-outside-toplevel
+ from ansiblelint.rules.AlwaysRunRule import AlwaysRunRule
+
+ raise MatchError(
+ rule=AlwaysRunRule,
+ filename=task[FILENAME_KEY],
+ linenumber=task[LINE_NUMBER_KEY])
+
+ sanitized_task = _sanitize_task(task)
+ mod_arg_parser = ModuleArgsParser(sanitized_task)
+ try:
+ action, arguments, result['delegate_to'] = mod_arg_parser.parse()
+ except AnsibleParserError as e:
+ try:
+ task_info = "%s:%s" % (task[FILENAME_KEY], task[LINE_NUMBER_KEY])
+ except KeyError:
+ task_info = "Unknown"
+ pp = pprint.PrettyPrinter(indent=2)
+ task_pprint = pp.pformat(sanitized_task)
+
+ _logger.critical("Couldn't parse task at %s (%s)\n%s", task_info, e.message, task_pprint)
+ raise SystemExit(ANSIBLE_FAILURE_RC)
+
+ # denormalize shell -> command conversion
+ if '_uses_shell' in arguments:
+ action = 'shell'
+ del arguments['_uses_shell']
+
+ for (k, v) in list(task.items()):
+ if k in ('action', 'local_action', 'args', 'delegate_to') or k == action:
+ # we don't want to re-assign these values, which were
+ # determined by the ModuleArgsParser() above
+ continue
+ else:
+ result[k] = v
+
+ result['action'] = dict(__ansible_module__=action)
+
+ if '_raw_params' in arguments:
+ result['action']['__ansible_arguments__'] = arguments['_raw_params'].split(' ')
+ del arguments['_raw_params']
+ else:
+ result['action']['__ansible_arguments__'] = list()
+
+ if 'argv' in arguments and not result['action']['__ansible_arguments__']:
+ result['action']['__ansible_arguments__'] = arguments['argv']
+ del arguments['argv']
+
+ result['action'].update(arguments)
+ return result
+
+
+# FIXME: drop noqa once this function is made simpler
+# Ref: https://github.com/ansible/ansible-lint/issues/744
+def normalize_task_v1(task): # noqa: C901
+ result = dict()
+ for (k, v) in task.items():
+ if k in VALID_KEYS or k.startswith('with_'):
+ if k == 'local_action' or k == 'action':
+ if not isinstance(v, dict):
+ v = _kv_to_dict(v)
+ v['__ansible_arguments__'] = v.get('__ansible_arguments__', list())
+ result['action'] = v
+ else:
+ result[k] = v
+ else:
+ if isinstance(v, str):
+ v = _kv_to_dict(k + ' ' + v)
+ elif not v:
+ v = dict(__ansible_module__=k)
+ else:
+ if isinstance(v, dict):
+ v.update(dict(__ansible_module__=k))
+ else:
+ if k == '__line__':
+ # Keep the line number stored
+ result[k] = v
+ continue
+
+ else:
+ # Tasks that include playbooks (rather than task files)
+ # can get here
+ # https://github.com/ansible/ansible-lint/issues/138
+ raise RuntimeError("Was not expecting value %s of type %s for key %s\n"
+ "Task: %s. Check the syntax of your playbook using "
+ "ansible-playbook --syntax-check" %
+ (str(v), type(v), k, str(task)))
+ v['__ansible_arguments__'] = v.get('__ansible_arguments__', list())
+ result['action'] = v
+ if 'module' in result['action']:
+ # this happens when a task uses
+ # local_action:
+ # module: ec2
+ # etc...
+ result['action']['__ansible_module__'] = result['action']['module']
+ del result['action']['module']
+ if 'args' in result:
+ result['action'].update(result.get('args'))
+ del result['args']
+ return result
+
+
+def normalize_task(task, filename):
+ ansible_action_type = task.get('__ansible_action_type__', 'task')
+ if '__ansible_action_type__' in task:
+ del task['__ansible_action_type__']
+ task = normalize_task_v2(task)
+ task[FILENAME_KEY] = filename
+ task['__ansible_action_type__'] = ansible_action_type
+ return task
+
+
+def task_to_str(task):
+ name = task.get("name")
+ if name:
+ return name
+ action = task.get("action")
+ 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)
+
+
+def extract_from_list(blocks, candidates):
+ results = list()
+ for block in blocks:
+ for candidate in candidates:
+ if isinstance(block, dict) and candidate in block:
+ if isinstance(block[candidate], list):
+ results.extend(add_action_type(block[candidate], candidate))
+ elif block[candidate] is not None:
+ raise RuntimeError(
+ "Key '%s' defined, but bad value: '%s'" %
+ (candidate, str(block[candidate])))
+ return results
+
+
+def add_action_type(actions, action_type):
+ results = list()
+ for action in actions:
+ # ignore empty task
+ if not action:
+ continue
+ action['__ansible_action_type__'] = BLOCK_NAME_TO_ACTION_TYPE_MAP[action_type]
+ results.append(action)
+ return results
+
+
+def get_action_tasks(yaml, file):
+ tasks = list()
+ if file['type'] in ['tasks', 'handlers']:
+ tasks = add_action_type(yaml, file['type'])
+ else:
+ tasks.extend(extract_from_list(yaml, ['tasks', 'handlers', 'pre_tasks', 'post_tasks']))
+
+ # Add sub-elements of block/rescue/always to tasks list
+ tasks.extend(extract_from_list(tasks, ['block', 'rescue', 'always']))
+ # Remove block/rescue/always elements from tasks list
+ 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
+ set(['include', 'include_tasks',
+ 'import_playbook', 'import_tasks']).isdisjoint(task.keys())]
+
+
+def get_normalized_tasks(yaml, file):
+ tasks = get_action_tasks(yaml, file)
+ res = []
+ for task in tasks:
+ # An empty `tags` block causes `None` to be returned if
+ # the `or []` is not present - `task.get('tags', [])`
+ # does not suffice.
+ if 'skip_ansible_lint' in (task.get('tags') or []):
+ # No need to normalize_task is we are skipping it.
+ continue
+ res.append(normalize_task(task, file['path']))
+
+ return res
+
+
+@lru_cache(maxsize=128)
+def parse_yaml_linenumbers(data, filename):
+ """Parse yaml as ansible.utils.parse_yaml but with linenumbers.
+
+ The line numbers are stored in each node's LINE_NUMBER_KEY key.
+ """
+ def compose_node(parent, index):
+ # the line number where the previous token has ended (plus empty lines)
+ line = loader.line
+ node = Composer.compose_node(loader, parent, index)
+ node.__line__ = line + 1
+ return node
+
+ def construct_mapping(node, deep=False):
+ mapping = AnsibleConstructor.construct_mapping(loader, node, deep=deep)
+ if hasattr(node, '__line__'):
+ mapping[LINE_NUMBER_KEY] = node.__line__
+ else:
+ mapping[LINE_NUMBER_KEY] = mapping._line_number
+ mapping[FILENAME_KEY] = filename
+ return mapping
+
+ try:
+ kwargs = {}
+ if 'vault_password' in inspect.getfullargspec(AnsibleLoader.__init__).args:
+ kwargs['vault_password'] = DEFAULT_VAULT_PASSWORD
+ loader = AnsibleLoader(data, **kwargs)
+ loader.compose_node = compose_node
+ loader.construct_mapping = construct_mapping
+ data = loader.get_single_data()
+ except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
+ raise SystemExit("Failed to parse YAML in %s: %s" % (filename, str(e)))
+ return data
+
+
+def get_first_cmd_arg(task):
+ try:
+ if 'cmd' in task['action']:
+ first_cmd_arg = task['action']['cmd'].split()[0]
+ else:
+ first_cmd_arg = task['action']['__ansible_arguments__'][0]
+ except IndexError:
+ return None
+ return first_cmd_arg
+
+
+def is_playbook(filename: str) -> bool:
+ """
+ Check if the file is a playbook.
+
+ Given a filename, it should return true if it looks like a playbook. The
+ function is not supposed to raise exceptions.
+ """
+ # we assume is a playbook if we loaded a sequence of dictionaries where
+ # at least one of these keys is present:
+ playbooks_keys = {
+ "gather_facts",
+ "hosts",
+ "import_playbook",
+ "post_tasks",
+ "pre_tasks",
+ "roles"
+ "tasks",
+ }
+
+ # makes it work with Path objects by converting them to strings
+ if not isinstance(filename, str):
+ filename = str(filename)
+
+ try:
+ f = parse_yaml_from_file(filename)
+ except Exception as e:
+ _logger.warning(
+ "Failed to load %s with %s, assuming is not a playbook.",
+ filename, e)
+ else:
+ if (
+ isinstance(f, AnsibleSequence) and
+ hasattr(f, 'keys') and
+ playbooks_keys.intersection(next(iter(f), {}).keys())
+ ):
+ return True
+ return False
+
+
+def get_yaml_files(options: Namespace) -> dict:
+ """Find all yaml files."""
+ # git is preferred as it also considers .gitignore
+ git_command = ['git', 'ls-files', '*.yaml', '*.yml']
+ _logger.info("Discovering files to lint: %s", ' '.join(git_command))
+
+ out = None
+
+ try:
+ out = subprocess.check_output(
+ git_command,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True
+ ).split()
+ except subprocess.CalledProcessError as exc:
+ _logger.warning(
+ "Failed to discover yaml files to lint using git: %s",
+ exc.output.rstrip('\n')
+ )
+ except FileNotFoundError as exc:
+ if options.verbosity:
+ _logger.warning(
+ "Failed to locate command: %s", exc
+ )
+
+ if out is None:
+ out = [
+ os.path.join(root, name)
+ for root, dirs, files in os.walk('.')
+ for name in files
+ if name.endswith('.yaml') or name.endswith('.yml')
+ ]
+
+ return OrderedDict.fromkeys(sorted(out))
+
+
+# FIXME: drop noqa once this function is made simpler
+# Ref: https://github.com/ansible/ansible-lint/issues/744
+def get_playbooks_and_roles(options=None) -> List[str]: # noqa: C901
+ """Find roles and playbooks."""
+ if options is None:
+ options = {}
+
+ files = get_yaml_files(options)
+
+ playbooks = []
+ role_dirs = []
+ role_internals = {
+ 'defaults',
+ 'files',
+ 'handlers',
+ 'meta',
+ 'tasks',
+ 'templates',
+ 'vars',
+ }
+
+ # detect role in repository root:
+ if 'tasks/main.yml' in files or 'tasks/main.yaml' in files:
+ role_dirs.append('.')
+
+ for p in map(Path, files):
+
+ try:
+ for file_path in options.exclude_paths:
+ if str(p.resolve()).startswith(str(file_path)):
+ raise FileNotFoundError(
+ f'File {file_path} matched exclusion entry: {p}')
+ except FileNotFoundError as e:
+ _logger.debug('Ignored %s due to: %s', p, e)
+ continue
+
+ if (next((i for i in p.parts if i.endswith('playbooks')), None) or
+ 'playbook' in p.parts[-1]):
+ playbooks.append(normpath(p))
+ continue
+
+ # ignore if any folder ends with _vars
+ if next((i for i in p.parts if i.endswith('_vars')), None):
+ continue
+ elif 'roles' in p.parts or '.' in role_dirs:
+ if 'tasks' in p.parts and p.parts[-1] in ['main.yaml', 'main.yml']:
+ role_dirs.append(str(p.parents[1]))
+ continue
+ elif role_internals.intersection(p.parts):
+ continue
+ elif 'tests' in p.parts:
+ playbooks.append(normpath(p))
+ if 'molecule' in p.parts:
+ if p.parts[-1] != 'molecule.yml':
+ playbooks.append(normpath(p))
+ continue
+ # hidden files are clearly not playbooks, likely config files.
+ if p.parts[-1].startswith('.'):
+ continue
+
+ if is_playbook(str(p)):
+ playbooks.append(normpath(p))
+ continue
+
+ _logger.info('Unknown file type: %s', normpath(p))
+
+ _logger.info('Found roles: %s', ' '.join(role_dirs))
+ _logger.info('Found playbooks: %s', ' '.join(playbooks))
+
+ return role_dirs + playbooks
+
+
+def expand_path_vars(path: str) -> str:
+ """Expand the environment or ~ variables in a path string."""
+ # It may be possible for function to be called with a Path object
+ path = str(path).strip()
+ path = os.path.expanduser(path)
+ path = os.path.expandvars(path)
+ return path
+
+
+def expand_paths_vars(paths: List[str]) -> List[str]:
+ """Expand the environment or ~ variables in a list."""
+ paths = [expand_path_vars(p) for p in paths]
+ return paths
+
+
+def get_rules_dirs(rulesdir: List[str], use_default: bool) -> List[str]:
+ """Return a list of rules dirs."""
+ default_ruledirs = [DEFAULT_RULESDIR]
+ default_custom_rulesdir = os.environ.get(
+ CUSTOM_RULESDIR_ENVVAR, os.path.join(DEFAULT_RULESDIR, "custom")
+ )
+ custom_ruledirs = sorted(
+ str(rdir.resolve())
+ for rdir in Path(default_custom_rulesdir).iterdir()
+ if rdir.is_dir() and (rdir / "__init__.py").exists()
+ )
+ if use_default:
+ return rulesdir + custom_ruledirs + default_ruledirs
+
+ return rulesdir or custom_ruledirs + default_ruledirs
diff --git a/lib/ansiblelint/version.py b/lib/ansiblelint/version.py
new file mode 100644
index 0000000..7bbf973
--- /dev/null
+++ b/lib/ansiblelint/version.py
@@ -0,0 +1,12 @@
+"""Ansible-lint version information."""
+
+try:
+ import pkg_resources
+except ImportError:
+ pass
+
+
+try:
+ __version__ = pkg_resources.get_distribution('ansible-lint').version
+except Exception:
+ __version__ = 'unknown'
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..3b94f98
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,37 @@
+[mypy]
+python_version = 3.6
+color_output = True
+error_summary = True
+disallow_untyped_calls = True
+; warn_redundant_casts=True
+
+[mypy-ansiblelint.*]
+ignore_missing_imports = True
+
+# 3rd party ignores
+[mypy-ansible]
+ignore_missing_imports = True
+
+[mypy-ansible.*]
+ignore_missing_imports = True
+
+[mypy-pytest]
+ignore_missing_imports = True
+
+[mypy-packaging.version]
+ignore_missing_imports = True
+
+[mypy-importlib_metadata]
+ignore_missing_imports = True
+
+[mypy-rich.*]
+ignore_missing_imports = True
+
+[mypy-ruamel.*]
+ignore_missing_imports = True
+
+[mypy-setuptools]
+ignore_missing_imports = True
+
+[mypy-sphinx_ansible_theme]
+ignore_missing_imports = True
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..db5f0f2
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,13 @@
+[build-system]
+requires = [
+ "setuptools >= 42.0.0", # required by pyproject+setuptools_scm integration
+ "setuptools_scm[toml] >= 3.5.0", # required for "no-local-version" scheme
+ "setuptools_scm_git_archive >= 1.0",
+ "wheel",
+]
+build-backend = "setuptools.build_meta"
+
+# ATTENTION: the following section must be kept last in
+# `pyproject.toml` because our CI/CD appends one line in
+# the end when publishing non-tagged versions to test.pypi.org
+[tool.setuptools_scm]
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..6206f8a
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,68 @@
+[pytest]
+addopts =
+ # `pytest-xdist`:
+ -n auto
+
+ # `pytest-mon`:
+ # useful for live testing with `pytest-watch` during development:
+ # --testmon
+
+ --durations=10
+ -v
+ -ra
+ --showlocals
+ --doctest-modules
+ --junitxml=.test-results/pytest/results.xml
+
+ # `pytest-cov`:
+ --cov=ansiblelint
+ --cov-report term-missing:skip-covered
+ --cov-report xml:.test-results/pytest/cov.xml
+ --no-cov-on-fail
+
+ # interpret all the target args as importables:
+ --pyargs
+
+ # importable packages for test lookup:
+ test ansiblelint.rules
+doctest_optionflags = ALLOW_UNICODE ELLIPSIS
+filterwarnings =
+ error
+
+ # TODO: delete the following ignores once Ansible that we support gets rid of `imp`
+ # Ref: https://github.com/ansible/ansible-lint/pull/734
+ ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:DeprecationWarning:ansible.plugins.loader
+
+ # TODO: delete the following ignores once Ansible gets rid of direct
+ # imports from `collections`
+ ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working:DeprecationWarning
+ ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working:DeprecationWarning
+ ignore:Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3,and in 3.9 it will stop working:DeprecationWarning
+junit_duration_report = call
+# Our github annotation parser from .github/workflows/tox.yml requires xunit1 format. Ref:
+# https://github.com/shyim/junit-report-annotations-action/issues/3#issuecomment-663241378
+junit_family = xunit1
+junit_suite_name = ansible_lint_test_suite
+minversion = 4.6.6
+norecursedirs =
+ build
+ dist
+ docs
+ lib/ansible_lint.egg-info
+ .cache
+ .eggs
+ .git
+ .github
+ .tox
+ *.egg
+python_files =
+ test_*.py
+ # Ref: https://docs.pytest.org/en/latest/reference.html#confval-python_files
+ # Needed to discover legacy nose test modules:
+ Test*.py
+ # Needed to discover embedded Rule tests
+ *Rule.py
+# Using --pyargs instead of testpath as we embed some tests
+# See: https://github.com/pytest-dev/pytest/issues/6451#issuecomment-687043537
+# testpaths =
+xfail_strict = true
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3cd60a4
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,86 @@
+[aliases]
+dists = clean --all sdist bdist_wheel
+
+[bdist_wheel]
+universal = 1
+
+[metadata]
+name = ansible-lint
+url = https://github.com/ansible/ansible-lint
+project_urls =
+ Bug Tracker = https://github.com/ansible/ansible-lint/issues
+ CI: GitHub = https://github.com/ansible/ansible-lint/actions?query=workflow:gh+branch:master+event:push
+ Code of Conduct = https://docs.ansible.com/ansible/latest/community/code_of_conduct.html
+ Documentation = https://ansible-lint.readthedocs.io/en/latest/
+ Mailing lists = https://docs.ansible.com/ansible/latest/community/communication.html#mailing-list-information
+ Source Code = https://github.com/ansible/ansible-lint
+description = Checks playbooks for practices and behaviour that could potentially be improved
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+author = Will Thames
+author_email = will@thames.id.au
+maintainer = Ansible by Red Hat
+maintainer_email = info@ansible.com
+license = MIT
+license_file = LICENSE
+classifiers =
+ Development Status :: 5 - Production/Stable
+
+ Environment :: Console
+
+ Intended Audience :: Developers
+ Intended Audience :: Information Technology
+ Intended Audience :: System Administrators
+
+ Operating System :: OS Independent
+
+ License :: OSI Approved :: MIT License
+
+ Programming Language :: Python
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: Implementation
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: Jython
+ Programming Language :: Python :: Implementation :: PyPy
+
+ Topic :: Software Development :: Bug Tracking
+ Topic :: Software Development :: Quality Assurance
+ Topic :: Software Development :: Testing
+
+ Topic :: Utilities
+keywords =
+ ansible
+ lint
+
+[options]
+use_scm_version = True
+python_requires = >=3.6
+package_dir =
+ = lib
+packages = find:
+zip_safe = False
+
+# These are required during `setup.py` run:
+setup_requires =
+ setuptools_scm>=1.15.0
+ setuptools_scm_git_archive>=1.0
+
+# These are required in actual runtime:
+install_requires =
+ ansible >= 2.8
+ pyyaml
+ rich
+ ruamel.yaml >= 0.15.34,<1; python_version < "3.7"
+ ruamel.yaml >= 0.15.37,<1; python_version >= "3.7"
+ # NOTE: per issue #509 0.15.34 included in debian backports
+ typing-extensions; python_version < "3.8"
+
+[options.entry_points]
+console_scripts =
+ ansible-lint = ansiblelint.__main__:main
+
+[options.packages.find]
+where = lib
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..b72e95c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,9 @@
+#! /usr/bin/env python3
+"""Ansible-lint distribution package setuptools installer.
+
+The presence of this file ensures the support
+of pip editable mode *with setuptools only*.
+"""
+from setuptools import setup
+
+__name__ == '__main__' and setup() # pylint: disable=expression-not-assigned
diff --git a/test-requirements.in b/test-requirements.in
new file mode 100755
index 0000000..ce938e7
--- /dev/null
+++ b/test-requirements.in
@@ -0,0 +1,9 @@
+#!/usr/bin/env pip-compile -q --allow-unsafe --output-file=test-requirements.txt
+# Avoid using --generate-hashes as it breaks pip install from tox with:
+# ERROR: In --require-hashes mode, all requirements must have their versions pinned with ==. These do not:
+# ansible<2.10,>=2.9 from
+pytest >= 6.0.1
+pytest-cov >= 2.10.1
+pytest-xdist >= 2.1.0
+# Needed to avoid DeprecationWarning errors in pytest:
+setuptools >= 49.6.0
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..20ddf46
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,24 @@
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile --allow-unsafe --output-file=test-requirements.txt ./test-requirements.in
+#
+apipkg==1.5 # via execnet
+attrs==20.1.0 # via pytest
+coverage==5.2.1 # via pytest-cov
+execnet==1.7.1 # via pytest-xdist
+iniconfig==1.0.1 # via pytest
+packaging==20.4 # via pytest
+pluggy==0.13.1 # via pytest
+py==1.9.0 # via pytest, pytest-forked
+pyparsing==2.4.7 # via packaging
+pytest-cov==2.10.1 # via -r test-requirements.in
+pytest-forked==1.3.0 # via pytest-xdist
+pytest-xdist==2.1.0 # via -r test-requirements.in
+pytest==6.1.2 # via -r test-requirements.in, pytest-cov, pytest-forked, pytest-xdist
+six==1.15.0 # via packaging
+toml==0.10.1 # via pytest
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==50.3.2 # via -r test-requirements.in
diff --git a/test/TestAlwaysRunRule.py b/test/TestAlwaysRunRule.py
new file mode 100644
index 0000000..011a258
--- /dev/null
+++ b/test/TestAlwaysRunRule.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.AlwaysRunRule import AlwaysRunRule
+from ansiblelint.runner import Runner
+
+
+class TestAlwaysRun(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(AlwaysRunRule())
+
+ def test_file_positive(self):
+ success = 'test/always-run-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/always-run-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(1, len(errs))
diff --git a/test/TestAnsibleLintRule.py b/test/TestAnsibleLintRule.py
new file mode 100644
index 0000000..f8bc6d1
--- /dev/null
+++ b/test/TestAnsibleLintRule.py
@@ -0,0 +1,7 @@
+from ansiblelint.rules import AnsibleLintRule
+
+
+def test_unjinja():
+ input = "{{ a }} {% b %} {# try to confuse parsing inside a comment { {{}} } #}"
+ output = "JINJA_EXPRESSION JINJA_STATEMENT JINJA_COMMENT"
+ assert AnsibleLintRule.unjinja(input) == output
diff --git a/test/TestAnsibleSyntax.py b/test/TestAnsibleSyntax.py
new file mode 100644
index 0000000..8103b61
--- /dev/null
+++ b/test/TestAnsibleSyntax.py
@@ -0,0 +1,16 @@
+"""Test Ansible Syntax.
+
+This module contains tests that validate that linter does not produce errors
+when encountering what counts as valid Ansible syntax.
+"""
+
+PB_WITH_NULL_TASKS = '''
+- hosts: all
+ tasks:
+'''
+
+
+def test_null_tasks(default_text_runner):
+ """Assure we do not fail when encountering null tasks."""
+ results = default_text_runner.run_playbook(PB_WITH_NULL_TASKS)
+ assert not results
diff --git a/test/TestBaseFormatter.py b/test/TestBaseFormatter.py
new file mode 100644
index 0000000..c6ba7fb
--- /dev/null
+++ b/test/TestBaseFormatter.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8; -*-
+from pathlib import Path
+
+import pytest
+
+from ansiblelint.formatters import BaseFormatter
+
+
+@pytest.mark.parametrize(('base_dir', 'relative_path'), (
+ (None, True),
+ ('/whatever', False),
+ (Path('/whatever'), False),
+))
+@pytest.mark.parametrize('path', ('/whatever/string', Path('/whatever/string')))
+def test_base_formatter_when_base_dir(base_dir, relative_path, path):
+ # Given
+ base_formatter = BaseFormatter(base_dir, relative_path)
+
+ # When
+ output_path = base_formatter._format_path(path)
+
+ # Then
+ assert isinstance(output_path, str)
+ assert base_formatter._base_dir is None or isinstance(base_formatter._base_dir, str)
+ assert output_path == str(path)
+
+
+@pytest.mark.parametrize('base_dir', (
+ Path('/whatever'),
+ '/whatever',
+))
+@pytest.mark.parametrize('path', ('/whatever/string', Path('/whatever/string')))
+def test_base_formatter_when_base_dir_is_given_and_relative_is_true(path, base_dir):
+ # Given
+ base_formatter = BaseFormatter(base_dir, True)
+
+ # When
+ output_path = base_formatter._format_path(path)
+
+ # Then
+ assert isinstance(output_path, str)
+ assert isinstance(base_formatter._base_dir, str)
+ assert output_path == Path(path).name
+
+
+# vim: et:sw=4:syntax=python:ts=4:
diff --git a/test/TestBecomeUserWithoutBecome.py b/test/TestBecomeUserWithoutBecome.py
new file mode 100644
index 0000000..066e52f
--- /dev/null
+++ b/test/TestBecomeUserWithoutBecome.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.BecomeUserWithoutBecomeRule import BecomeUserWithoutBecomeRule
+from ansiblelint.runner import Runner
+
+
+class TestBecomeUserWithoutBecome(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(BecomeUserWithoutBecomeRule())
+
+ def test_file_positive(self):
+ success = 'test/become-user-without-become-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/become-user-without-become-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(3, len(errs))
diff --git a/test/TestCliRolePaths.py b/test/TestCliRolePaths.py
new file mode 100644
index 0000000..a459df2
--- /dev/null
+++ b/test/TestCliRolePaths.py
@@ -0,0 +1,134 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import os
+import unittest
+from pathlib import Path
+
+import pytest
+
+from ansiblelint.testing import run_ansible_lint
+
+
+class TestCliRolePaths(unittest.TestCase):
+ def setUp(self):
+ self.local_test_dir = os.path.dirname(os.path.realpath(__file__))
+
+ def test_run_single_role_path_no_trailing_slash_module(self):
+ cwd = self.local_test_dir
+ role_path = 'test-role'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_single_role_path_no_trailing_slash_script(self):
+ cwd = self.local_test_dir
+ role_path = 'test-role'
+
+ result = run_ansible_lint(role_path, cwd=cwd, bin="ansible-lint")
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_single_role_path_with_trailing_slash(self):
+ cwd = self.local_test_dir
+ role_path = 'test-role/'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_multiple_role_path_no_trailing_slash(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/test-role'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_multiple_role_path_with_trailing_slash(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/test-role/'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_inside_role_dir(self):
+ cwd = os.path.join(self.local_test_dir, 'test-role/')
+ role_path = '.'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_role_three_dir_deep(self):
+ cwd = self.local_test_dir
+ role_path = 'testproject/roles/test-role'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ self.assertIn('Use shell only when shell functionality is required',
+ result.stdout)
+
+ def test_run_playbook(self):
+ """Call ansible-lint the way molecule does."""
+ top_src_dir = os.path.dirname(self.local_test_dir)
+ cwd = os.path.join(top_src_dir, 'test/roles/test-role')
+ role_path = 'molecule/default/include-import-role.yml'
+
+ env = os.environ.copy()
+ env['ANSIBLE_ROLES_PATH'] = os.path.dirname(cwd)
+
+ result = run_ansible_lint(role_path, cwd=cwd, env=env)
+ self.assertIn('Use shell only when shell functionality is required', result.stdout)
+
+ def test_run_role_name_invalid(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/invalid-name'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ assert '106 Role name invalid-name does not match' in result.stdout
+
+ def test_run_role_name_with_prefix(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/ansible-role-foo'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ assert len(result.stdout) == 0
+ assert len(result.stderr) == 0
+ assert result.returncode == 0
+
+ def test_run_role_name_from_meta(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/valid-due-to-meta'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ assert len(result.stdout) == 0
+ assert len(result.stderr) == 0
+ assert result.returncode == 0
+
+ def test_run_invalid_role_name_from_meta(self):
+ cwd = self.local_test_dir
+ role_path = 'roles/invalid_due_to_meta'
+
+ result = run_ansible_lint(role_path, cwd=cwd)
+ assert '106 Role name invalid-due-to-meta does not match' in result.stdout
+
+
+@pytest.mark.parametrize(('result', 'env'), (
+ (True, {
+ "GITHUB_ACTIONS": "true",
+ "GITHUB_WORKFLOW": "foo"
+ }),
+ (False, None)),
+ ids=("on", "off"))
+def test_run_playbook_github(result, env):
+ """Call ansible-lint simulating GitHub Actions environment."""
+ cwd = str(Path(__file__).parent.parent.resolve())
+ role_path = 'examples/example.yml'
+
+ result_gh = run_ansible_lint(role_path, cwd=cwd, env=env)
+
+ expected = (
+ '::error file=examples/example.yml,line=47,severity=MEDIUM::[E101] '
+ 'Deprecated always_run'
+ )
+ assert (expected in result_gh.stdout) is result
diff --git a/test/TestCommandHasChangesCheck.py b/test/TestCommandHasChangesCheck.py
new file mode 100644
index 0000000..da0aa08
--- /dev/null
+++ b/test/TestCommandHasChangesCheck.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.CommandHasChangesCheckRule import CommandHasChangesCheckRule
+from ansiblelint.runner import Runner
+
+
+class TestCommandHasChangesCheck(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(CommandHasChangesCheckRule())
+
+ def test_command_changes_positive(self):
+ success = 'test/command-check-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_command_changes_negative(self):
+ failure = 'test/command-check-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(2, len(errs))
diff --git a/test/TestCommandLineInvocationSameAsConfig.py b/test/TestCommandLineInvocationSameAsConfig.py
new file mode 100644
index 0000000..a5368fa
--- /dev/null
+++ b/test/TestCommandLineInvocationSameAsConfig.py
@@ -0,0 +1,137 @@
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+from ansiblelint import cli
+
+
+@pytest.fixture
+def base_arguments():
+ return ['../test/skiptasks.yml']
+
+
+@pytest.mark.parametrize(('args', 'config'), (
+ (["-p"], "test/fixtures/parseable.yml"),
+ (["-q"], "test/fixtures/quiet.yml"),
+ (["-r", "test/fixtures/rules/"],
+ "test/fixtures/rulesdir.yml"),
+ (["-R", "-r", "test/fixtures/rules/"],
+ "test/fixtures/rulesdir-defaults.yml"),
+ (["-t", "skip_ansible_lint"],
+ "test/fixtures/tags.yml"),
+ (["-v"], "test/fixtures/verbosity.yml"),
+ (["-x", "bad_tag"],
+ "test/fixtures/skip-tags.yml"),
+ (["--exclude", "test/"],
+ "test/fixtures/exclude-paths.yml"),
+ (["--show-relpath"],
+ "test/fixtures/show-abspath.yml"),
+ ([],
+ "test/fixtures/show-relpath.yml"),
+ ))
+def test_ensure_config_are_equal(base_arguments, args, config, monkeypatch):
+ command = base_arguments + args
+ cli_parser = cli.get_cli_parser()
+
+ _real_pathlib_resolve = Path.resolve
+
+ def _fake_pathlib_resolve(self):
+ try:
+ return _real_pathlib_resolve(self)
+ except FileNotFoundError:
+ if self != Path(args[-1]):
+ raise
+ return Path.cwd() / self
+
+ with monkeypatch.context() as mp_ctx:
+ if (
+ sys.version_info[:2] < (3, 6) and
+ args[-2:] == ["-r", "test/fixtures/rules/"]
+ ):
+ mp_ctx.setattr(Path, 'resolve', _fake_pathlib_resolve)
+ options = cli_parser.parse_args(command)
+
+ file_config = cli.load_config(config)
+
+ for key, val in file_config.items():
+ if key in {'exclude_paths', 'rulesdir'}:
+ val = [Path(p) for p in val]
+ assert val == getattr(options, key)
+
+
+def test_config_can_be_overridden(base_arguments):
+ no_override = cli.get_config(base_arguments + ["-t", "bad_tag"])
+
+ overridden = cli.get_config(base_arguments +
+ ["-t", "bad_tag",
+ "-c", "test/fixtures/tags.yml"])
+
+ assert no_override.tags + ["skip_ansible_lint"] == overridden.tags
+
+
+def test_different_config_file(base_arguments):
+ """Ensures an alternate config_file can be used."""
+ diff_config = cli.get_config(base_arguments +
+ ["-c", "test/fixtures/ansible-config.yml"])
+ no_config = cli.get_config(base_arguments + ["-v"])
+
+ assert diff_config.verbosity == no_config.verbosity
+
+
+def test_expand_path_user_and_vars_config_file(base_arguments):
+ """Ensure user and vars are expanded when specified as exclude_paths."""
+ config1 = cli.get_config(base_arguments +
+ ["-c", "test/fixtures/exclude-paths-with-expands.yml"])
+ config2 = cli.get_config(base_arguments + [
+ "--exclude", "~/.ansible/roles",
+ "--exclude", "$HOME/.ansible/roles"
+ ])
+
+ assert str(config1.exclude_paths[0]) == os.path.expanduser("~/.ansible/roles")
+ assert str(config2.exclude_paths[0]) == os.path.expanduser("~/.ansible/roles")
+ assert str(config1.exclude_paths[1]) == os.path.expandvars("$HOME/.ansible/roles")
+ assert str(config2.exclude_paths[1]) == os.path.expandvars("$HOME/.ansible/roles")
+
+
+def test_path_from_config_do_not_depend_on_cwd(monkeypatch): # Issue 572
+ config1 = cli.load_config("test/fixtures/config-with-relative-path.yml")
+ monkeypatch.chdir('test')
+ config2 = cli.load_config("fixtures/config-with-relative-path.yml")
+
+ assert config1['exclude_paths'].sort() == config2['exclude_paths'].sort()
+
+
+def test_path_from_cli_depend_on_cwd(base_arguments, monkeypatch, tmp_path):
+ # Issue 572
+ arguments = base_arguments + ["--exclude",
+ "test/fixtures/config-with-relative-path.yml"]
+
+ options1 = cli.get_cli_parser().parse_args(arguments)
+ assert 'test/test' not in str(options1.exclude_paths[0])
+
+ test_dir = 'test'
+ if sys.version_info[:2] < (3, 6):
+ test_dir = tmp_path / 'test' / 'test' / 'fixtures'
+ test_dir.mkdir(parents=True)
+ (test_dir / 'config-with-relative-path.yml').write_text('')
+ test_dir = test_dir / '..' / '..'
+ monkeypatch.chdir(test_dir)
+ options2 = cli.get_cli_parser().parse_args(arguments)
+
+ assert 'test/test' in str(options2.exclude_paths[0])
+
+
+@pytest.mark.parametrize(
+ "config_file",
+ (
+ pytest.param("test/fixtures/ansible-config-invalid.yml", id="invalid"),
+ pytest.param("/dev/null/ansible-config-missing.yml", id="missing")
+ ),
+)
+def test_config_failure(base_arguments, config_file):
+ """Ensures specific config files produce error code 2."""
+ with pytest.raises(SystemExit, match="^2$"):
+ cli.get_config(base_arguments +
+ ["-c", config_file])
diff --git a/test/TestComparisonToEmptyString.py b/test/TestComparisonToEmptyString.py
new file mode 100644
index 0000000..cdbd0fa
--- /dev/null
+++ b/test/TestComparisonToEmptyString.py
@@ -0,0 +1,39 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.ComparisonToEmptyStringRule import ComparisonToEmptyStringRule
+from ansiblelint.testing import RunFromText
+
+SUCCESS_TASKS = '''
+- name: shut down
+ command: /sbin/shutdown -t now
+ when: ansible_os_family
+'''
+
+FAIL_TASKS = '''
+- hosts: all
+ tasks:
+ - name: shut down
+ command: /sbin/shutdown -t now
+ when: ansible_os_family == ""
+ - name: shut down
+ command: /sbin/shutdown -t now
+ when: ansible_os_family !=""
+'''
+
+
+class TestComparisonToEmptyStringRule(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(ComparisonToEmptyStringRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_success(self):
+ results = self.runner.run_role_tasks_main(SUCCESS_TASKS)
+ self.assertEqual(0, len(results))
+
+ def test_fail(self):
+ results = self.runner.run_playbook(FAIL_TASKS)
+ self.assertEqual(2, len(results))
diff --git a/test/TestComparisonToLiteralBool.py b/test/TestComparisonToLiteralBool.py
new file mode 100644
index 0000000..041ca1b
--- /dev/null
+++ b/test/TestComparisonToLiteralBool.py
@@ -0,0 +1,69 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.ComparisonToLiteralBoolRule import ComparisonToLiteralBoolRule
+from ansiblelint.testing import RunFromText
+
+PASS_WHEN = '''
+- name: example task
+ debug:
+ msg: test
+ when: my_var
+'''
+
+PASS_WHEN_NOT_FALSE = '''
+- name: example task
+ debug:
+ msg: test
+ when: not my_var
+'''
+
+PASS_WHEN_NOT_NULL = '''
+- name: example task
+ debug:
+ msg: test
+ when: my_var not None
+'''
+
+FAIL_LITERAL_TRUE = '''
+- name: example task
+ debug:
+ msg: test
+ when: my_var == True
+'''
+
+FAIL_LITERAL_FALSE = '''
+- name: example task
+ debug:
+ msg: test
+ when: my_var == false
+'''
+
+
+class TestComparisonToLiteralBoolRule(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(ComparisonToLiteralBoolRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_when(self):
+ results = self.runner.run_role_tasks_main(PASS_WHEN)
+ self.assertEqual(0, len(results))
+
+ def test_when_not_false(self):
+ results = self.runner.run_role_tasks_main(PASS_WHEN_NOT_FALSE)
+ self.assertEqual(0, len(results))
+
+ def test_when_not_null(self):
+ results = self.runner.run_role_tasks_main(PASS_WHEN_NOT_NULL)
+ self.assertEqual(0, len(results))
+
+ def test_literal_true(self):
+ results = self.runner.run_role_tasks_main(FAIL_LITERAL_TRUE)
+ self.assertEqual(1, len(results))
+
+ def test_literal_false(self):
+ results = self.runner.run_role_tasks_main(FAIL_LITERAL_FALSE)
+ self.assertEqual(1, len(results))
diff --git a/test/TestDependenciesInMeta.py b/test/TestDependenciesInMeta.py
new file mode 100644
index 0000000..d272120
--- /dev/null
+++ b/test/TestDependenciesInMeta.py
@@ -0,0 +1,22 @@
+import pytest
+
+from ansiblelint.runner import Runner
+
+
+@pytest.mark.parametrize(
+ 'filename',
+ (
+ 'bitbucket',
+ 'galaxy',
+ 'github',
+ 'webserver',
+ 'gitlab',
+ ),
+)
+def test_external_dependency_is_ok(default_rules_collection, filename):
+ playbook_path = (
+ 'test/dependency-in-meta/{filename}.yml'.
+ format_map(locals())
+ )
+ good_runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ assert [] == good_runner.run()
diff --git a/test/TestDeprecatedModule.py b/test/TestDeprecatedModule.py
new file mode 100644
index 0000000..6741ed4
--- /dev/null
+++ b/test/TestDeprecatedModule.py
@@ -0,0 +1,32 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+import pytest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.DeprecatedModuleRule import DeprecatedModuleRule
+from ansiblelint.testing import ANSIBLE_MAJOR_VERSION, RunFromText
+
+MODULE_DEPRECATED = '''
+- name: task example
+ docker:
+ debug: test
+'''
+
+
+class TestDeprecatedModuleRule(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(DeprecatedModuleRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ @pytest.mark.xfail(
+ ANSIBLE_MAJOR_VERSION > (2, 9),
+ reason='Ansible devel has changed so ansible-lint needs fixing. '
+ 'Ref: https://github.com/ansible/ansible-lint/issues/675',
+ raises=SystemExit, strict=True,
+ )
+ def test_module_deprecated(self):
+ results = self.runner.run_role_tasks_main(MODULE_DEPRECATED)
+ self.assertEqual(1, len(results))
diff --git a/test/TestEnvVarsInCommand.py b/test/TestEnvVarsInCommand.py
new file mode 100644
index 0000000..e3a1d84
--- /dev/null
+++ b/test/TestEnvVarsInCommand.py
@@ -0,0 +1,90 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.EnvVarsInCommandRule import EnvVarsInCommandRule
+from ansiblelint.testing import RunFromText
+
+SUCCESS_PLAY_TASKS = '''
+- hosts: localhost
+
+ tasks:
+ - name: actual use of environment
+ shell: echo $HELLO
+ environment:
+ HELLO: hello
+
+ - name: use some key-value pairs
+ command: chdir=/tmp creates=/tmp/bobbins warn=no touch bobbins
+
+ - name: commands can have flags
+ command: abc --xyz=def blah
+
+ - name: commands can have equals in them
+ command: echo "==========="
+
+ - name: commands with cmd
+ command:
+ cmd:
+ echo "-------"
+
+ - name: command with stdin (ansible > 2.4)
+ command: /bin/cat
+ args:
+ stdin: "Hello, world!"
+
+ - name: use argv to send the command as a list
+ command:
+ argv:
+ - /bin/echo
+ - Hello
+ - World
+
+ - name: another use of argv
+ command:
+ args:
+ argv:
+ - echo
+ - testing
+
+ - name: environment variable with shell
+ shell: HELLO=hello echo $HELLO
+
+ - name: command with stdin_add_newline (ansible > 2.8)
+ command: /bin/cat
+ args:
+ stdin: "Hello, world!"
+ stdin_add_newline: false
+
+ - name: command with strip_empty_ends (ansible > 2.8)
+ command: echo
+ args:
+ strip_empty_ends: false
+'''
+
+FAIL_PLAY_TASKS = '''
+- hosts: localhost
+
+ tasks:
+ - name: environment variable with command
+ command: HELLO=hello echo $HELLO
+
+ - name: typo some stuff
+ command: cerates=/tmp/blah warn=no touch /tmp/blah
+'''
+
+
+class TestEnvVarsInCommand(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(EnvVarsInCommandRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_success(self):
+ results = self.runner.run_playbook(SUCCESS_PLAY_TASKS)
+ self.assertEqual(0, len(results))
+
+ def test_fail(self):
+ results = self.runner.run_playbook(FAIL_PLAY_TASKS)
+ self.assertEqual(2, len(results))
diff --git a/test/TestExamples.py b/test/TestExamples.py
new file mode 100644
index 0000000..48a89d3
--- /dev/null
+++ b/test/TestExamples.py
@@ -0,0 +1,8 @@
+"""Assure samples produced desire outcomes."""
+from ansiblelint.runner import Runner
+
+
+def test_example(default_rules_collection):
+ """example.yml is expected to have 5 match errors inside."""
+ result = Runner(default_rules_collection, 'examples/example.yml', [], [], []).run()
+ assert len(result) == 5
diff --git a/test/TestFormatter.py b/test/TestFormatter.py
new file mode 100644
index 0000000..fd4f122
--- /dev/null
+++ b/test/TestFormatter.py
@@ -0,0 +1,46 @@
+# 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.
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import pathlib
+import unittest
+
+from ansiblelint.errors import MatchError
+from ansiblelint.formatters import Formatter
+from ansiblelint.rules import AnsibleLintRule
+
+
+class TestFormatter(unittest.TestCase):
+
+ def setUp(self):
+ self.rule = AnsibleLintRule()
+ self.rule.id = "TCF0001"
+ self.formatter = Formatter(pathlib.Path.cwd(), True)
+
+ def test_format_coloured_string(self):
+ match = MatchError("message", 1, "hello", "filename.yml", self.rule)
+ self.formatter.format(match, True)
+
+ def test_unicode_format_string(self):
+ match = MatchError(u'\U0001f427', 1, "hello", "filename.yml", self.rule)
+ self.formatter.format(match, False)
+
+ def test_dict_format_line(self):
+ match = MatchError("xyz", 1, {'hello': 'world'}, "filename.yml", self.rule,)
+ self.formatter.format(match, True)
diff --git a/test/TestImportIncludeRole.py b/test/TestImportIncludeRole.py
new file mode 100644
index 0000000..8b581ff
--- /dev/null
+++ b/test/TestImportIncludeRole.py
@@ -0,0 +1,103 @@
+import pytest
+
+from ansiblelint.runner import Runner
+
+ROLE_TASKS_MAIN = '''
+- name: shell instead of command
+ shell: echo hello world
+'''
+
+ROLE_TASKS_WORLD = '''
+- command: echo this is a task without a name
+'''
+
+PLAY_IMPORT_ROLE = '''
+- hosts: all
+
+ tasks:
+ - import_role:
+ name: test-role
+'''
+
+PLAY_IMPORT_ROLE_INCOMPLETE = '''
+- hosts: all
+
+ tasks:
+ - import_role:
+ foo: bar
+'''
+
+PLAY_IMPORT_ROLE_INLINE = '''
+- hosts: all
+
+ tasks:
+ - import_role: name=test-role
+'''
+
+PLAY_INCLUDE_ROLE = '''
+- hosts: all
+
+ tasks:
+ - include_role:
+ name: test-role
+ tasks_from: world
+'''
+
+PLAY_INCLUDE_ROLE_INLINE = '''
+- hosts: all
+
+ tasks:
+ - include_role: name=test-role tasks_from=world
+'''
+
+
+@pytest.fixture
+def playbook_path(request, tmp_path):
+ playbook_text = request.param
+ role_tasks_dir = tmp_path / 'test-role' / 'tasks'
+ role_tasks_dir.mkdir(parents=True)
+ (role_tasks_dir / 'main.yml').write_text(ROLE_TASKS_MAIN)
+ (role_tasks_dir / 'world.yml').write_text(ROLE_TASKS_WORLD)
+ play_path = tmp_path / 'playbook.yml'
+ play_path.write_text(playbook_text)
+ return str(play_path)
+
+
+@pytest.mark.parametrize(('playbook_path', 'messages'), (
+ pytest.param(PLAY_IMPORT_ROLE,
+ ['only when shell functionality is required'],
+ id='IMPORT_ROLE',
+ ),
+ pytest.param(PLAY_IMPORT_ROLE_INLINE,
+ ['only when shell functionality is require'],
+ id='IMPORT_ROLE_INLINE',
+ ),
+ pytest.param(PLAY_INCLUDE_ROLE,
+ ['only when shell functionality is require',
+ 'All tasks should be named'],
+ id='INCLUDE_ROLE',
+ ),
+ pytest.param(PLAY_INCLUDE_ROLE_INLINE,
+ ['only when shell functionality is require',
+ 'All tasks should be named'],
+ id='INCLUDE_ROLE_INLINE',
+ ),
+), indirect=('playbook_path', ))
+def test_import_role2(default_rules_collection, playbook_path, messages):
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+ for message in messages:
+ assert message in str(results)
+
+
+@pytest.mark.parametrize(('playbook_path', 'messages'), (
+ pytest.param(PLAY_IMPORT_ROLE_INCOMPLETE,
+ ["Failed to find required 'name' key in import_role"],
+ id='IMPORT_ROLE_INCOMPLETE',
+ ),
+), indirect=('playbook_path', ))
+def test_invalid_import_role(default_rules_collection, playbook_path, messages):
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+ for message in messages:
+ assert message in str(results)
diff --git a/test/TestImportPlaybook.py b/test/TestImportPlaybook.py
new file mode 100644
index 0000000..06492e3
--- /dev/null
+++ b/test/TestImportPlaybook.py
@@ -0,0 +1,17 @@
+"""Test ability to import playbooks."""
+from ansiblelint.runner import Runner
+
+
+def test_task_hook_import_playbook(default_rules_collection):
+ """Assures import_playbook includes are recognized."""
+ playbook_path = 'test/playbook-import/playbook_parent.yml'
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+
+ results_text = str(results)
+ assert len(runner.playbooks) == 2
+ assert len(results) == 2
+ # Assures we detected the issues from imported playbook
+ assert 'Commands should not change things' in results_text
+ assert '502' in results_text
+ assert 'All tasks should be named' in results_text
diff --git a/test/TestImportWithMalformed.py b/test/TestImportWithMalformed.py
new file mode 100644
index 0000000..4f5425c
--- /dev/null
+++ b/test/TestImportWithMalformed.py
@@ -0,0 +1,65 @@
+from collections import namedtuple
+
+import pytest
+
+from ansiblelint.runner import Runner
+
+PlayFile = namedtuple('PlayFile', ['name', 'content'])
+
+
+IMPORT_TASKS_MAIN = PlayFile('import-tasks-main.yml', '''
+- oops this is invalid
+''')
+
+IMPORT_SHELL_PIP = PlayFile('import-tasks-main.yml', '''
+- shell: pip
+''')
+
+PLAY_IMPORT_TASKS = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - import_tasks: import-tasks-main.yml
+''')
+
+
+@pytest.fixture
+def play_file_path(tmp_path):
+ p = tmp_path / 'playbook.yml'
+ return str(p)
+
+
+@pytest.fixture
+def runner(play_file_path, default_rules_collection):
+ return Runner(default_rules_collection, play_file_path, [], [], [])
+
+
+@pytest.fixture
+def _play_files(tmp_path, request):
+ if request.param is None:
+ return
+ for play_file in request.param:
+ p = tmp_path / play_file.name
+ p.write_text(play_file.content)
+
+
+@pytest.mark.parametrize(
+ '_play_files',
+ (
+ pytest.param([IMPORT_SHELL_PIP, PLAY_IMPORT_TASKS], id='Import shell w/ pip'),
+ pytest.param(
+ [IMPORT_TASKS_MAIN, PLAY_IMPORT_TASKS],
+ id='import_tasks w/ malformed import',
+ marks=pytest.mark.xfail(
+ reason='Garbage non-tasks sequence is not being '
+ 'properly processed. Ref: '
+ 'https://github.com/ansible/ansible-lint/issues/707',
+ raises=AttributeError,
+ ),
+ ),
+ ),
+ indirect=['_play_files']
+)
+@pytest.mark.usefixtures('_play_files')
+def test_import_tasks_with_malformed_import(runner):
+ results = str(runner.run())
+ assert 'only when shell functionality is required' in results
diff --git a/test/TestIncludeMissFileWithRole.py b/test/TestIncludeMissFileWithRole.py
new file mode 100644
index 0000000..126e095
--- /dev/null
+++ b/test/TestIncludeMissFileWithRole.py
@@ -0,0 +1,122 @@
+import os
+from collections import namedtuple
+
+import pytest
+
+from ansiblelint.runner import Runner
+
+PlayFile = namedtuple('PlayFile', ['name', 'content'])
+
+
+PLAY_IN_THE_PLACE = PlayFile('playbook.yml', u'''
+- hosts: all
+ roles:
+ - include_in_the_place
+''')
+
+PLAY_RELATIVE = PlayFile('playbook.yml', u'''
+- hosts: all
+ roles:
+ - include_relative
+''')
+
+PLAY_MISS_INCLUDE = PlayFile('playbook.yml', u'''
+- hosts: all
+ roles:
+ - include_miss
+''')
+
+PLAY_ROLE_INCLUDED_IN_THE_PLACE = PlayFile('roles/include_in_the_place/tasks/main.yml', u'''
+---
+- include_tasks: included_file.yml
+''')
+
+PLAY_ROLE_INCLUDED_RELATIVE = PlayFile('roles/include_relative/tasks/main.yml', u'''
+---
+- include_tasks: tasks/included_file.yml
+''')
+
+PLAY_ROLE_INCLUDED_MISS = PlayFile('roles/include_miss/tasks/main.yml', u'''
+---
+- include_tasks: tasks/noexist_file.yml
+''')
+
+PLAY_INCLUDED_IN_THE_PLACE = PlayFile('roles/include_in_the_place/tasks/included_file.yml', u'''
+- debug:
+ msg: 'was found & included'
+''')
+
+PLAY_INCLUDED_RELATIVE = PlayFile('roles/include_relative/tasks/included_file.yml', u'''
+- debug:
+ msg: 'was found & included'
+''')
+
+
+@pytest.fixture
+def play_file_path(tmp_path):
+ p = tmp_path / 'playbook.yml'
+ return str(p)
+
+
+@pytest.fixture
+def runner(play_file_path, default_rules_collection):
+ return Runner(default_rules_collection, play_file_path, [], [], [])
+
+
+@pytest.fixture
+def _play_files(tmp_path, request):
+ if request.param is None:
+ return
+ for play_file in request.param:
+ print(play_file.name)
+ p = tmp_path / play_file.name
+ os.makedirs(os.path.dirname(p), exist_ok=True)
+ p.write_text(play_file.content)
+
+
+@pytest.mark.parametrize(
+ '_play_files',
+ (
+ pytest.param([PLAY_MISS_INCLUDE,
+ PLAY_ROLE_INCLUDED_MISS],
+ id='no exist file include'),
+ ),
+ indirect=['_play_files']
+)
+@pytest.mark.usefixtures('_play_files')
+def test_cases_warning_message(runner, caplog):
+ runner.run()
+ noexist_message_count = 0
+
+ for record in caplog.records:
+ print(record)
+ if "Couldn't open" in str(record):
+ noexist_message_count += 1
+
+ assert noexist_message_count == 3 # 3 retries
+
+
+@pytest.mark.parametrize(
+ '_play_files',
+ (
+ pytest.param([PLAY_IN_THE_PLACE,
+ PLAY_ROLE_INCLUDED_IN_THE_PLACE,
+ PLAY_INCLUDED_IN_THE_PLACE],
+ id='in the place include'),
+ pytest.param([PLAY_RELATIVE,
+ PLAY_ROLE_INCLUDED_RELATIVE,
+ PLAY_INCLUDED_RELATIVE],
+ id='relative include')
+ ),
+ indirect=['_play_files']
+)
+@pytest.mark.usefixtures('_play_files')
+def test_cases_that_do_not_report(runner, caplog):
+ runner.run()
+ noexist_message_count = 0
+
+ for record in caplog.records:
+ if "Couldn't open" in str(record):
+ noexist_message_count += 1
+
+ assert noexist_message_count == 0
diff --git a/test/TestIncludeMissingFileRule.py b/test/TestIncludeMissingFileRule.py
new file mode 100644
index 0000000..7248fdc
--- /dev/null
+++ b/test/TestIncludeMissingFileRule.py
@@ -0,0 +1,88 @@
+from collections import namedtuple
+
+import pytest
+
+from ansiblelint.runner import Runner
+
+PlayFile = namedtuple('PlayFile', ['name', 'content'])
+
+
+PLAY_INCLUDING_PLAIN = PlayFile('playbook.yml', u'''
+- hosts: all
+ tasks:
+ - include: some_file.yml
+''')
+
+PLAY_INCLUDING_JINJA2 = PlayFile('playbook.yml', u'''
+- hosts: all
+ tasks:
+ - include: "{{ some_path }}/some_file.yml"
+''')
+
+PLAY_INCLUDING_NOQA = PlayFile('playbook.yml', u'''
+- hosts: all
+ tasks:
+ - include: some_file.yml # noqa 505
+''')
+
+PLAY_INCLUDED = PlayFile('some_file.yml', u'''
+- debug:
+ msg: 'was found & included'
+''')
+
+PLAY_HAVING_TASK = PlayFile('playbook.yml', u'''
+- name: Play
+ hosts: all
+ pre_tasks:
+ tasks:
+ - name: Ping
+ ping:
+''')
+
+
+@pytest.fixture
+def play_file_path(tmp_path):
+ p = tmp_path / 'playbook.yml'
+ return str(p)
+
+
+@pytest.fixture
+def runner(play_file_path, default_rules_collection):
+ return Runner(default_rules_collection, play_file_path, [], [], [])
+
+
+@pytest.fixture
+def _play_files(tmp_path, request):
+ if request.param is None:
+ return
+ for play_file in request.param:
+ p = tmp_path / play_file.name
+ p.write_text(play_file.content)
+
+
+@pytest.mark.parametrize(
+ '_play_files', (pytest.param([PLAY_INCLUDING_PLAIN], id='referenced file missing'), ),
+ indirect=['_play_files']
+)
+@pytest.mark.usefixtures('_play_files')
+def test_include_file_missing(runner):
+ results = str(runner.run())
+ assert 'referenced missing file in' in results
+ assert 'playbook.yml' in results
+ assert 'some_file.yml' in results
+
+
+@pytest.mark.parametrize(
+ '_play_files',
+ (
+ pytest.param([PLAY_INCLUDING_PLAIN, PLAY_INCLUDED], id='File Exists'),
+ pytest.param([PLAY_INCLUDING_JINJA2], id='JINJA2 in reference'),
+ pytest.param([PLAY_INCLUDING_NOQA], id='NOQA was used'),
+ pytest.param([PLAY_HAVING_TASK], id='Having a task')
+ ),
+ indirect=['_play_files']
+)
+@pytest.mark.usefixtures('_play_files')
+def test_cases_that_do_not_report(runner):
+ results = str(runner.run())
+ assert 'referenced missing file in' not in results
diff --git a/test/TestLineNumber.py b/test/TestLineNumber.py
new file mode 100644
index 0000000..9355daa
--- /dev/null
+++ b/test/TestLineNumber.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2020 Albin Vass <albin.vass@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.SudoRule import SudoRule
+
+TEST_TASKLIST = """
+- debug:
+ msg: test
+
+- command: echo test
+ sudo: true
+"""
+
+
+def test_rule_linenumber(monkeypatch):
+ """Check that SudoRule offense contains a line number."""
+ rule = SudoRule()
+ matches = rule.matchyaml(dict(path="", type='tasklist'), TEST_TASKLIST)
+ assert matches[0].linenumber == 5
diff --git a/test/TestLineTooLong.py b/test/TestLineTooLong.py
new file mode 100644
index 0000000..be8c6f7
--- /dev/null
+++ b/test/TestLineTooLong.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.LineTooLongRule import LineTooLongRule
+from ansiblelint.testing import RunFromText
+
+LONG_LINE = '''
+- name: task example
+ debug:
+ msg: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua tempor incididunt ut labore et dolore'
+''' # noqa 501
+
+
+class TestLineTooLongRule(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(LineTooLongRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_long_line(self):
+ results = self.runner.run_role_tasks_main(LONG_LINE)
+ self.assertEqual(1, len(results))
diff --git a/test/TestLintRule.py b/test/TestLintRule.py
new file mode 100644
index 0000000..7377443
--- /dev/null
+++ b/test/TestLintRule.py
@@ -0,0 +1,45 @@
+# 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.
+
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from .rules import EMatcherRule, UnsetVariableMatcherRule
+
+
+class TestRule(unittest.TestCase):
+
+ def test_rule_matching(self):
+ text = ""
+ filename = 'test/ematchtest.yml'
+ with open(filename) as f:
+ text = f.read()
+ ematcher = EMatcherRule.EMatcherRule()
+ matches = ematcher.matchlines(dict(path=filename, type='playbooks'), text)
+ self.assertEqual(len(matches), 3)
+
+ def test_rule_postmatching(self):
+ text = ""
+ filename = 'test/bracketsmatchtest.yml'
+ with open(filename) as f:
+ text = f.read()
+ rule = UnsetVariableMatcherRule.UnsetVariableMatcherRule()
+ matches = rule.matchlines(dict(path=filename, type='playbooks'), text)
+ self.assertEqual(len(matches), 2)
diff --git a/test/TestLocalContent.py b/test/TestLocalContent.py
new file mode 100644
index 0000000..e78aab4
--- /dev/null
+++ b/test/TestLocalContent.py
@@ -0,0 +1,42 @@
+"""Test playbooks with local content."""
+import pytest
+
+from ansiblelint.runner import Runner
+
+
+def test_local_collection(default_rules_collection):
+ """Assures local collections are found."""
+ playbook_path = 'test/local-content/test-collection.yml'
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+
+ assert len(runner.playbooks) == 1
+ assert len(results) == 0
+
+
+def test_roles_local_content(default_rules_collection):
+ """Assures local content in roles is found."""
+ playbook_path = 'test/local-content/test-roles-success/test.yml'
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+
+ assert len(runner.playbooks) == 4
+ assert len(results) == 0
+
+
+def test_roles_local_content_failure(default_rules_collection):
+ """Assures local content in roles is found, even if Ansible itself has trouble."""
+ playbook_path = 'test/local-content/test-roles-failed/test.yml'
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+
+ assert len(runner.playbooks) == 4
+ assert len(results) == 0
+
+
+def test_roles_local_content_failure_complete(default_rules_collection):
+ """Role with local content that is not found."""
+ playbook_path = 'test/local-content/test-roles-failed-complete/test.yml'
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ with pytest.raises(SystemExit, match="^3$"):
+ runner.run()
diff --git a/test/TestMatchError.py b/test/TestMatchError.py
new file mode 100644
index 0000000..f17d865
--- /dev/null
+++ b/test/TestMatchError.py
@@ -0,0 +1,178 @@
+"""Tests for MatchError."""
+
+import operator
+
+import pytest
+
+from ansiblelint.errors import MatchError
+from ansiblelint.rules import AnsibleLintRule
+from ansiblelint.rules.AlwaysRunRule import AlwaysRunRule
+from ansiblelint.rules.BecomeUserWithoutBecomeRule import BecomeUserWithoutBecomeRule
+
+
+class DummyTestObject:
+ """A dummy object for equality tests."""
+
+ def __repr__(self):
+ """Return a dummy object representation for parmetrize."""
+ return '{self.__class__.__name__}()'.format(self=self)
+
+ def __eq__(self, other):
+ """Report the equality check failure with any object."""
+ return False
+
+ def __ne__(self, other):
+ """Report the confirmation of inequality with any object."""
+ return True
+
+
+class DummySentinelTestObject:
+ """A dummy object for equality protocol tests with sentinel."""
+
+ def __eq__(self, other):
+ """Return sentinel as result of equality check w/ anything."""
+ return 'EQ_SENTINEL'
+
+ def __ne__(self, other):
+ """Return sentinel as result of inequality check w/ anything."""
+ return 'NE_SENTINEL'
+
+ def __lt__(self, other):
+ """Return sentinel as result of less than check w/ anything."""
+ return 'LT_SENTINEL'
+
+ def __gt__(self, other):
+ """Return sentinel as result of greater than chk w/ anything."""
+ return 'GT_SENTINEL'
+
+
+@pytest.mark.parametrize(
+ ('left_match_error', 'right_match_error'),
+ (
+ (MatchError("foo"), MatchError("foo")),
+ (MatchError("a", details="foo"), MatchError("a", details="foo")),
+ ),
+)
+def test_matcherror_compare(left_match_error, right_match_error):
+ """Check that MatchError instances with similar attrs are equivalent."""
+ assert left_match_error == right_match_error
+
+
+class AnsibleLintRuleWithStringId(AnsibleLintRule):
+ id = "ANSIBLE200"
+
+
+def test_matcherror_invalid():
+ """Ensure that MatchError requires message or rule."""
+ expected_err = r"^MatchError\(\) missing a required argument: one of 'message' or 'rule'$"
+ with pytest.raises(TypeError, match=expected_err):
+ MatchError()
+
+
+@pytest.mark.parametrize(
+ ('left_match_error', 'right_match_error'), (
+ # sorting by message
+ (MatchError("z"), MatchError("a")),
+ # filenames takes priority in sorting
+ (MatchError("a", filename="b"), MatchError("a", filename="a")),
+ # rule id 501 > rule id 101
+ (MatchError(rule=BecomeUserWithoutBecomeRule), MatchError(rule=AlwaysRunRule)),
+ # rule id "200" > rule id 101
+ (MatchError(rule=AnsibleLintRuleWithStringId), MatchError(rule=AlwaysRunRule)),
+ # details are taken into account
+ (MatchError("a", details="foo"), MatchError("a", details="bar")),
+ ))
+class TestMatchErrorCompare:
+
+ def test_match_error_less_than(self, left_match_error, right_match_error):
+ """Check 'less than' protocol implementation in MatchError."""
+ assert right_match_error < left_match_error
+
+ def test_match_error_greater_than(self, left_match_error, right_match_error):
+ """Check 'greater than' protocol implementation in MatchError."""
+ assert left_match_error > right_match_error
+
+ def test_match_error_not_equal(self, left_match_error, right_match_error):
+ """Check 'not equals' protocol implementation in MatchError."""
+ assert left_match_error != right_match_error
+
+
+@pytest.mark.parametrize(
+ 'other',
+ (
+ None,
+ "foo",
+ 42,
+ Exception("foo"),
+ ),
+ ids=repr,
+)
+@pytest.mark.parametrize(
+ ('operation', 'operator_char'),
+ (
+ pytest.param(operator.le, '<=', id='<='),
+ pytest.param(operator.gt, '>', id='>'),
+ ),
+)
+def test_matcherror_compare_no_other_fallback(other, operation, operator_char):
+ """Check that MatchError comparison with other types causes TypeError."""
+ expected_error = (
+ r'^('
+ r'unsupported operand type\(s\) for {operator!s}:|'
+ r"'{operator!s}' not supported between instances of"
+ r") 'MatchError' and '{other_type!s}'$".
+ format(other_type=type(other).__name__, operator=operator_char)
+ )
+ with pytest.raises(TypeError, match=expected_error):
+ operation(MatchError("foo"), other)
+
+
+@pytest.mark.parametrize(
+ 'other',
+ (
+ None,
+ 'foo',
+ 42,
+ Exception('foo'),
+ DummyTestObject(),
+ ),
+ ids=repr,
+)
+@pytest.mark.parametrize(
+ ('operation', 'expected_value'),
+ (
+ (operator.eq, False),
+ (operator.ne, True),
+ ),
+ ids=('==', '!=')
+)
+def test_matcherror_compare_with_other_fallback(
+ other,
+ operation,
+ expected_value,
+):
+ """Check that MatchError comparison runs other types fallbacks."""
+ assert operation(MatchError("foo"), other) is expected_value
+
+
+@pytest.mark.parametrize(
+ ('operation', 'expected_value'),
+ (
+ (operator.eq, 'EQ_SENTINEL'),
+ (operator.ne, 'NE_SENTINEL'),
+ # NOTE: these are swapped because when we do `x < y`, and `x.__lt__(y)`
+ # NOTE: returns `NotImplemented`, Python will reverse the check into
+ # NOTE: `y > x`, and so `y.__gt__(x) is called.
+ # Ref: https://docs.python.org/3/reference/datamodel.html#object.__lt__
+ (operator.lt, 'GT_SENTINEL'),
+ (operator.gt, 'LT_SENTINEL'),
+ ),
+ ids=('==', '!=', '<', '>'),
+)
+def test_matcherror_compare_with_dummy_sentinel(operation, expected_value):
+ """Check that MatchError comparison runs other types fallbacks."""
+ dummy_obj = DummySentinelTestObject()
+ # NOTE: This assertion abuses the CPython property to cache short string
+ # NOTE: objects because the identity check is more presice and we don't
+ # NOTE: want extra operator protocol methods to influence the test.
+ assert operation(MatchError("foo"), dummy_obj) is expected_value
diff --git a/test/TestMetaChangeFromDefault.py b/test/TestMetaChangeFromDefault.py
new file mode 100644
index 0000000..911553a
--- /dev/null
+++ b/test/TestMetaChangeFromDefault.py
@@ -0,0 +1,33 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.MetaChangeFromDefaultRule import MetaChangeFromDefaultRule
+from ansiblelint.testing import RunFromText
+
+DEFAULT_GALAXY_INFO = '''
+galaxy_info:
+ author: your name
+ description: your description
+ company: your company (optional)
+ license: license (GPLv2, CC-BY, etc)
+'''
+
+
+class TestMetaChangeFromDefault(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(MetaChangeFromDefaultRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_default_galaxy_info(self):
+ results = self.runner.run_role_meta_main(DEFAULT_GALAXY_INFO)
+ self.assertIn("Should change default metadata: author",
+ str(results))
+ self.assertIn("Should change default metadata: description",
+ str(results))
+ self.assertIn("Should change default metadata: company",
+ str(results))
+ self.assertIn("Should change default metadata: license",
+ str(results))
diff --git a/test/TestMetaMainHasInfo.py b/test/TestMetaMainHasInfo.py
new file mode 100644
index 0000000..757a4df
--- /dev/null
+++ b/test/TestMetaMainHasInfo.py
@@ -0,0 +1,94 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.MetaMainHasInfoRule import MetaMainHasInfoRule
+from ansiblelint.testing import RunFromText
+
+NO_GALAXY_INFO = '''
+author: the author
+description: this meta/main.yml has no galaxy_info
+'''
+
+MISSING_INFO = '''
+galaxy_info:
+ # author: the author
+ description: Testing if meta contains values
+ company: Not applicable
+
+ license: MIT
+
+ # min_ansible_version: 2.5
+
+ platforms:
+ - name: Fedora
+ versions:
+ - 25
+ - missing_name: No name
+ versions:
+ - 25
+'''
+
+BAD_TYPES = '''
+galaxy_info:
+ author: 007
+ description: ['Testing meta']
+ company: Not applicable
+
+ license: MIT
+
+ min_ansible_version: 2.5
+
+ platforms: Fedora
+'''
+
+PLATFORMS_LIST_OF_STR = '''
+galaxy_info:
+ author: '007'
+ description: 'Testing meta'
+ company: Not applicable
+
+ license: MIT
+
+ min_ansible_version: 2.5
+
+ platforms: ['Fedora', 'EL']
+'''
+
+
+class TestMetaMainHasInfo(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(MetaMainHasInfoRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_no_galaxy_info(self):
+ results = self.runner.run_role_meta_main(NO_GALAXY_INFO)
+ assert len(results) == 1
+ self.assertIn("No 'galaxy_info' found",
+ str(results))
+
+ def test_missing_info(self):
+ results = self.runner.run_role_meta_main(MISSING_INFO)
+ assert len(results) == 3
+ self.assertIn("Role info should contain author",
+ str(results))
+ self.assertIn("Role info should contain min_ansible_version",
+ str(results))
+ self.assertIn("Platform should contain name",
+ str(results))
+
+ def test_bad_types(self):
+ results = self.runner.run_role_meta_main(BAD_TYPES)
+ assert len(results) == 3
+ self.assertIn("author should be a string", str(results))
+ self.assertIn("description should be a string", str(results))
+ self.assertIn("Platforms should be a list of dictionaries",
+ str(results))
+
+ def test_platform_list_of_str(self):
+ results = self.runner.run_role_meta_main(PLATFORMS_LIST_OF_STR)
+ assert len(results) == 1
+ self.assertIn("Platforms should be a list of dictionaries",
+ str(results))
diff --git a/test/TestMetaVideoLinks.py b/test/TestMetaVideoLinks.py
new file mode 100644
index 0000000..4d61891
--- /dev/null
+++ b/test/TestMetaVideoLinks.py
@@ -0,0 +1,35 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.MetaVideoLinksRule import MetaVideoLinksRule
+from ansiblelint.testing import RunFromText
+
+META_VIDEO_LINKS = '''
+galaxy_info:
+ video_links:
+ - url: https://youtu.be/aWmRepTSFKs
+ title: Proper format
+ - https://youtu.be/this_is_not_a_dictionary
+ - my_bad_key: https://youtu.be/aWmRepTSFKs
+ title: This has a bad key
+ - url: www.myvid.com/vid
+ title: Bad format of url
+'''
+
+
+class TestMetaVideoLinks(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(MetaVideoLinksRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_video_links(self):
+ results = self.runner.run_role_meta_main(META_VIDEO_LINKS)
+ self.assertIn("Expected item in 'video_links' to be a dictionary",
+ str(results))
+ self.assertIn("'video_links' to contain only keys 'url' and 'title'",
+ str(results))
+ self.assertIn("URL format 'www.myvid.com/vid' is not recognized",
+ str(results))
diff --git a/test/TestMissingFilePermissionsRule.py b/test/TestMissingFilePermissionsRule.py
new file mode 100644
index 0000000..0a67ae1
--- /dev/null
+++ b/test/TestMissingFilePermissionsRule.py
@@ -0,0 +1,110 @@
+# 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.
+"""MissingFilePermissionsRule tests."""
+import pytest
+
+from ansiblelint.rules.MissingFilePermissionsRule import MissingFilePermissionsRule
+
+SUCCESS_TASKS = '''
+---
+- hosts: hosts
+ tasks:
+ - name: permissions not missing and numeric
+ file:
+ path: foo
+ mode: 0600
+ - name: permissions missing while state is absent is fine
+ file:
+ path: foo
+ state: absent
+ - name: permissions missing while state is file (default) is fine
+ file:
+ path: foo
+ - name: permissions missing while state is link is fine
+ file:
+ path: foo2
+ src: foo
+ state: link
+ - name: file edit when create is false
+ lineinfile:
+ path: foo
+ create: false
+ line: some content here
+ - name: replace should not require mode
+ replace:
+ path: foo
+'''
+
+FAIL_TASKS = '''
+---
+- hosts: hosts
+ tasks:
+ - name: file does not allow preserve value for mode
+ file:
+ path: foo
+ mode: preserve
+ - name: permissions missing and might create file
+ file:
+ path: foo
+ state: touch
+ - name: permissions missing and might create directory
+ file:
+ path: foo
+ state: directory
+ - name: permissions needed if create is used
+ ini_file:
+ path: foo
+ create: true
+ - name: lineinfile when create is true
+ lineinfile:
+ path: foo
+ create: true
+ line: some content here
+ - name: replace does not allow preserve mode
+ replace:
+ path: foo
+ mode: preserve
+ - name: ini_file does not accept preserve mode
+ ini_file:
+ path: foo
+ create: true
+ mode: preserve
+'''
+
+
+@pytest.mark.parametrize('rule_runner', (MissingFilePermissionsRule, ), indirect=['rule_runner'])
+def test_success(rule_runner):
+ """Validate that mode presence avoids hitting the rule."""
+ results = rule_runner.run_playbook(SUCCESS_TASKS)
+ assert len(results) == 0
+
+
+@pytest.mark.parametrize('rule_runner', (MissingFilePermissionsRule, ), indirect=['rule_runner'])
+def test_fail(rule_runner):
+ """Validate that missing mode triggers the rule."""
+ results = rule_runner.run_playbook(FAIL_TASKS)
+ assert len(results) == 7
+ assert results[0].linenumber == 5
+ assert results[1].linenumber == 9
+ assert results[2].linenumber == 13
+ assert results[3].linenumber == 17
+ assert results[4].linenumber == 21
+ assert results[5].linenumber == 26
+ assert results[6].linenumber == 30
diff --git a/test/TestNestedJinjaRule.py b/test/TestNestedJinjaRule.py
new file mode 100644
index 0000000..f8367b0
--- /dev/null
+++ b/test/TestNestedJinjaRule.py
@@ -0,0 +1,208 @@
+# -*- 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.
+
+from collections import namedtuple
+
+import pytest
+
+from ansiblelint.runner import Runner
+
+PlayFile = namedtuple('PlayFile', ['name', 'content'])
+
+
+FAIL_TASK_1LN = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: one-level nesting
+ set_fact:
+ var_one: "2*(1+2) is {{ 2 * {{ 1 + 2 }} }}"
+''')
+
+FAIL_TASK_1LN_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: one-level multiline nesting
+ set_fact:
+ var_one_ml: >
+ 2*(1+2) is {{ 2 *
+ {{ 1 + 2 }}
+ }}
+''')
+
+FAIL_TASK_2LN = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: two-level nesting
+ set_fact:
+ var_two: "2*(1+(3-1)) is {{ 2 * {{ 1 + {{ 3 - 1 }} }} }}"
+''')
+
+FAIL_TASK_2LN_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: two-level multiline nesting
+ set_fact:
+ var_two_ml: >
+ 2*(1+(3-1)) is {{ 2 *
+ {{ 1 +
+ {{ 3 - 1 }}
+ }} }}
+''')
+
+FAIL_TASK_W_5LN = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: five-level wild nesting
+ set_fact:
+ var_three_wld: "{{ {{ {{ {{ {{ 234 }} }} }} }} }}"
+''')
+
+FAIL_TASK_W_5LN_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: five-level wild multiline nesting
+ set_fact:
+ var_three_wld_ml: >
+ {{
+ {{
+ {{
+ {{
+ {{ 234 }}
+ }}
+ }}
+ }}
+ }}
+''')
+
+SUCCESS_TASK_P = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: non-nested example
+ set_fact:
+ var_one: "number for 'one' is {{ 2 * 1 }}"
+''')
+
+SUCCESS_TASK_P_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: multiline non-nested example
+ set_fact:
+ var_one_ml: >
+ number for 'one' is {{
+ 2 * 1 }}
+''')
+
+SUCCESS_TASK_2P = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: nesting far from each other
+ set_fact:
+ var_two: "number for 'two' is {{ 2 * 1 }} and number for 'three' is {{ 4 - 1 }}"
+''')
+
+SUCCESS_TASK_2P_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: multiline nesting far from each other
+ set_fact:
+ var_two_ml: >
+ number for 'two' is {{ 2 * 1
+ }} and number for 'three' is {{
+ 4 - 1 }}
+''')
+
+SUCCESS_TASK_C_2P = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: nesting close to each other
+ set_fact:
+ var_three: "number for 'ten' is {{ 2 - 1 }}{{ 3 - 3 }}"
+''')
+
+SUCCESS_TASK_C_2P_M = PlayFile('playbook.yml', '''
+- hosts: all
+ tasks:
+ - name: multiline nesting close to each other
+ set_fact:
+ var_three_ml: >
+ number for 'ten' is {{
+ 2 - 1
+ }}{{ 3 - 3 }}
+''')
+
+
+@pytest.fixture
+def runner(tmp_path, default_rules_collection):
+ return Runner(
+ default_rules_collection,
+ str(tmp_path / 'playbook.yml'),
+ [], [], [],
+ )
+
+
+@pytest.fixture
+def _playbook_file(tmp_path, request):
+ if request.param is None:
+ return
+ for play_file in request.param:
+ p = tmp_path / play_file.name
+ p.write_text(play_file.content)
+
+
+@pytest.mark.parametrize(
+ '_playbook_file',
+ (
+ pytest.param([FAIL_TASK_1LN], id='file includes one-level nesting'),
+ pytest.param([FAIL_TASK_1LN_M], id='file includes one-level multiline nesting'),
+ pytest.param([FAIL_TASK_2LN], id='file includes two-level nesting'),
+ pytest.param([FAIL_TASK_2LN_M], id='file includes two-level multiline nesting'),
+ pytest.param([FAIL_TASK_W_5LN], id='file includes five-level wild nesting'),
+ pytest.param([FAIL_TASK_W_5LN_M], id='file includes five-level wild multiline nesting'),
+ ),
+ indirect=['_playbook_file'],
+)
+@pytest.mark.usefixtures('_playbook_file')
+def test_including_wrong_nested_jinja(runner):
+ rule_violations = runner.run()
+ assert rule_violations[0].rule.id == '207'
+
+
+@pytest.mark.parametrize(
+ '_playbook_file',
+ (
+ pytest.param([SUCCESS_TASK_P], id='file includes non-nested example'),
+ pytest.param([SUCCESS_TASK_P_M], id='file includes multiline non-nested example'),
+ pytest.param([SUCCESS_TASK_2P], id='file includes nesting far from each other'),
+ pytest.param([SUCCESS_TASK_2P_M], id='file includes multiline nesting far from each other'),
+ pytest.param([SUCCESS_TASK_C_2P], id='file includes nesting close to each other'),
+ pytest.param(
+ [SUCCESS_TASK_C_2P_M],
+ id='file includes multiline nesting close to each other',
+ ),
+ ),
+ indirect=['_playbook_file'],
+)
+@pytest.mark.usefixtures('_playbook_file')
+def test_including_proper_nested_jinja(runner):
+ rule_violations = runner.run()
+ assert not rule_violations
diff --git a/test/TestNoFormattingInWhenRule.py b/test/TestNoFormattingInWhenRule.py
new file mode 100644
index 0000000..f9fde4a
--- /dev/null
+++ b/test/TestNoFormattingInWhenRule.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.NoFormattingInWhenRule import NoFormattingInWhenRule
+from ansiblelint.runner import Runner
+
+
+class TestNoFormattingInWhenRule(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(NoFormattingInWhenRule())
+
+ def test_file_positive(self):
+ success = 'test/jinja2-when-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/jinja2-when-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(2, len(errs))
diff --git a/test/TestOctalPermissions.py b/test/TestOctalPermissions.py
new file mode 100644
index 0000000..ed8c79c
--- /dev/null
+++ b/test/TestOctalPermissions.py
@@ -0,0 +1,112 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.OctalPermissionsRule import OctalPermissionsRule
+from ansiblelint.testing import RunFromText
+
+SUCCESS_TASKS = '''
+---
+- hosts: hosts
+ vars:
+ varset: varset
+ tasks:
+ - name: octal permissions test success (0600)
+ file:
+ path: foo
+ mode: 0600
+
+ - name: octal permissions test success (0000)
+ file:
+ path: foo
+ mode: 0000
+
+ - name: octal permissions test success (02000)
+ file:
+ path: bar
+ mode: 02000
+
+ - name: octal permissions test success (02751)
+ file:
+ path: bar
+ mode: 02751
+
+ - name: octal permissions test success (0777)
+ file: path=baz mode=0777
+
+ - name: octal permissions test success (0711)
+ file: path=baz mode=0711
+
+ - name: permissions test success (0777)
+ file: path=baz mode=u+rwx
+
+ - name: octal permissions test success (777)
+ file: path=baz mode=777
+
+ - name: octal permissions test success (733)
+ file: path=baz mode=733
+'''
+
+FAIL_TASKS = '''
+---
+- hosts: hosts
+ vars:
+ varset: varset
+ tasks:
+ - name: octal permissions test fail (600)
+ file:
+ path: foo
+ mode: 600
+
+ - name: octal permissions test fail (710)
+ file:
+ path: foo
+ mode: 710
+
+ - name: octal permissions test fail (123)
+ file:
+ path: foo
+ mode: 123
+
+ - name: octal permissions test fail (2000)
+ file:
+ path: bar
+ mode: 2000
+'''
+
+
+class TestOctalPermissionsRuleWithFile(unittest.TestCase):
+
+ collection = RulesCollection()
+ VALID_MODES = [0o777, 0o775, 0o770, 0o755, 0o750, 0o711, 0o710, 0o700,
+ 0o666, 0o664, 0o660, 0o644, 0o640, 0o600,
+ 0o555, 0o551, 0o550, 0o511, 0o510, 0o500,
+ 0o444, 0o440, 0o400]
+
+ INVALID_MODES = [777, 775, 770, 755, 750, 711, 710, 700,
+ 666, 664, 660, 644, 640, 622, 620, 600,
+ 555, 551, 550, # 511 == 0o777, 510 == 0o776, 500 == 0o764
+ 444, 440, 400]
+
+ def setUp(self):
+ self.rule = OctalPermissionsRule()
+ self.collection.register(self.rule)
+ self.runner = RunFromText(self.collection)
+
+ def test_success(self):
+ results = self.runner.run_playbook(SUCCESS_TASKS)
+ self.assertEqual(0, len(results))
+
+ def test_fail(self):
+ results = self.runner.run_playbook(FAIL_TASKS)
+ self.assertEqual(4, len(results))
+
+ def test_valid_modes(self):
+ for mode in self.VALID_MODES:
+ self.assertFalse(self.rule.is_invalid_permission(mode),
+ msg="0o%o should be a valid mode" % mode)
+
+ def test_invalid_modes(self):
+ for mode in self.INVALID_MODES:
+ self.assertTrue(self.rule.is_invalid_permission(mode),
+ msg="%d should be an invalid mode" % mode)
diff --git a/test/TestPackageIsNotLatest.py b/test/TestPackageIsNotLatest.py
new file mode 100644
index 0000000..91bf24c
--- /dev/null
+++ b/test/TestPackageIsNotLatest.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.PackageIsNotLatestRule import PackageIsNotLatestRule
+from ansiblelint.runner import Runner
+
+
+class TestPackageIsNotLatestRule(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(PackageIsNotLatestRule())
+
+ def test_package_not_latest_positive(self):
+ success = 'test/package-check-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_package_not_latest_negative(self):
+ failure = 'test/package-check-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(3, len(errs))
diff --git a/test/TestRoleHandlers.py b/test/TestRoleHandlers.py
new file mode 100644
index 0000000..9f38320
--- /dev/null
+++ b/test/TestRoleHandlers.py
@@ -0,0 +1,20 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.UseHandlerRatherThanWhenChangedRule import (
+ UseHandlerRatherThanWhenChangedRule,
+)
+from ansiblelint.runner import Runner
+
+
+class TestRoleHandlers(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(UseHandlerRatherThanWhenChangedRule())
+
+ def test_role_handler_positive(self):
+ success = 'test/role-with-handler/main.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
diff --git a/test/TestRoleNames.py b/test/TestRoleNames.py
new file mode 100644
index 0000000..9441a50
--- /dev/null
+++ b/test/TestRoleNames.py
@@ -0,0 +1,82 @@
+"""Test the RoleNames rule."""
+
+import pytest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.RoleNames import RoleNames
+from ansiblelint.runner import Runner
+
+ROLE_NAME_VALID = 'test_role'
+
+TASK_MINIMAL = """
+- name: Some task
+ ping:
+"""
+
+ROLE_MINIMAL = {
+ 'tasks': {
+ 'main.yml': TASK_MINIMAL
+ }
+}
+ROLE_META_EMPTY = {
+ 'meta': {
+ 'main.yml': ''
+ }
+}
+
+ROLE_WITH_EMPTY_META = {**ROLE_MINIMAL, **ROLE_META_EMPTY}
+
+PLAY_INCLUDE_ROLE = f"""
+- hosts: all
+ roles:
+ - {ROLE_NAME_VALID}
+"""
+
+
+@pytest.fixture
+def test_rules_collection():
+ """Instantiate a roles collection for tests."""
+ collection = RulesCollection()
+ collection.register(RoleNames())
+ return collection
+
+
+def dict_to_files(parent_dir, file_dict):
+ """Write a nested dict to a file and directory structure below parent_dir."""
+ for file, content in file_dict.items():
+ if isinstance(content, dict):
+ directory = parent_dir / file
+ directory.mkdir()
+ dict_to_files(directory, content)
+ else:
+ (parent_dir / file).write_text(content)
+
+
+@pytest.fixture
+def playbook_path(request, tmp_path):
+ """Create a playbook with a role in a temporary directory."""
+ playbook_text = request.param[0]
+ role_name = request.param[1]
+ role_layout = request.param[2]
+ role_path = tmp_path / role_name
+ role_path.mkdir()
+ dict_to_files(role_path, role_layout)
+ play_path = tmp_path / 'playbook.yml'
+ play_path.write_text(playbook_text)
+ return str(play_path)
+
+
+@pytest.mark.parametrize(('playbook_path', 'messages'), (
+ pytest.param((PLAY_INCLUDE_ROLE, ROLE_NAME_VALID, ROLE_WITH_EMPTY_META),
+ [],
+ id='ROLE_EMPTY_META',
+ ),
+), indirect=('playbook_path',))
+def test_role_name(test_rules_collection, playbook_path, messages):
+ """Lint a playbook and compare the expected messages with the actual messages."""
+ runner = Runner(test_rules_collection, playbook_path, [], [], [])
+ results = runner.run()
+ assert len(results) == len(messages)
+ results_text = str(results)
+ for message in messages:
+ assert message in results_text
diff --git a/test/TestRoleRelativePath.py b/test/TestRoleRelativePath.py
new file mode 100644
index 0000000..6c3163b
--- /dev/null
+++ b/test/TestRoleRelativePath.py
@@ -0,0 +1,52 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.RoleRelativePath import RoleRelativePath
+from ansiblelint.testing import RunFromText
+
+FAIL_TASKS = '''
+- name: template example
+ template:
+ src: ../templates/foo.j2
+ dest: /etc/file.conf
+- name: copy example
+ copy:
+ src: ../files/foo.conf
+ dest: /etc/foo.conf
+- name: win_template example
+ win_template:
+ src: ../win_templates/file.conf.j2
+ dest: file.conf
+- name: win_copy example
+ win_copy:
+ src: ../files/foo.conf
+ dest: renamed-foo.conf
+'''
+
+SUCCESS_TASKS = '''
+- name: content example with no src
+ copy:
+ content: '# This file was moved to /etc/other.conf'
+ dest: /etc/mine.conf
+- name: content example with no src
+ win_copy:
+ content: '# This file was moved to /etc/other.conf'
+ dest: /etc/mine.conf
+'''
+
+
+class TestRoleRelativePath(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(RoleRelativePath())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_fail(self):
+ results = self.runner.run_role_tasks_main(FAIL_TASKS)
+ self.assertEqual(4, len(results))
+
+ def test_success(self):
+ results = self.runner.run_role_tasks_main(SUCCESS_TASKS)
+ self.assertEqual(0, len(results))
diff --git a/test/TestRuleProperties.py b/test/TestRuleProperties.py
new file mode 100644
index 0000000..41f40e0
--- /dev/null
+++ b/test/TestRuleProperties.py
@@ -0,0 +1,11 @@
+def test_serverity_valid(default_rules_collection):
+ valid_severity_values = [
+ 'VERY_HIGH',
+ 'HIGH',
+ 'MEDIUM',
+ 'LOW',
+ 'VERY_LOW',
+ 'INFO',
+ ]
+ for rule in default_rules_collection:
+ assert rule.severity in valid_severity_values
diff --git a/test/TestRulesCollection.py b/test/TestRulesCollection.py
new file mode 100644
index 0000000..52d7dc6
--- /dev/null
+++ b/test/TestRulesCollection.py
@@ -0,0 +1,112 @@
+# 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 collections
+import os
+
+import pytest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.testing import run_ansible_lint
+
+
+@pytest.fixture
+def test_rules_collection():
+ return RulesCollection([os.path.abspath('./test/rules')])
+
+
+@pytest.fixture
+def ematchtestfile():
+ return dict(path='test/ematchtest.yml', type='playbook')
+
+
+@pytest.fixture
+def bracketsmatchtestfile():
+ return dict(path='test/bracketsmatchtest.yml', type='playbook')
+
+
+def test_load_collection_from_directory(test_rules_collection):
+ assert len(test_rules_collection) == 2
+
+
+def test_run_collection(test_rules_collection, ematchtestfile):
+ matches = test_rules_collection.run(ematchtestfile)
+ assert len(matches) == 3
+
+
+def test_tags(test_rules_collection, ematchtestfile, bracketsmatchtestfile):
+ matches = test_rules_collection.run(ematchtestfile, tags=['test1'])
+ assert len(matches) == 3
+ matches = test_rules_collection.run(ematchtestfile, tags=['test2'])
+ assert len(matches) == 0
+ matches = test_rules_collection.run(bracketsmatchtestfile, tags=['test1'])
+ assert len(matches) == 1
+ matches = test_rules_collection.run(bracketsmatchtestfile, tags=['test2'])
+ assert len(matches) == 2
+
+
+def test_skip_tags(test_rules_collection, ematchtestfile, bracketsmatchtestfile):
+ matches = test_rules_collection.run(ematchtestfile, skip_list=['test1'])
+ assert len(matches) == 0
+ matches = test_rules_collection.run(ematchtestfile, skip_list=['test2'])
+ assert len(matches) == 3
+ matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=['test1'])
+ assert len(matches) == 2
+ matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=['test2'])
+ assert len(matches) == 1
+
+
+def test_skip_id(test_rules_collection, ematchtestfile, bracketsmatchtestfile):
+ matches = test_rules_collection.run(ematchtestfile, skip_list=['TEST0001'])
+ assert len(matches) == 0
+ matches = test_rules_collection.run(ematchtestfile, skip_list=['TEST0002'])
+ assert len(matches) == 3
+ matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=['TEST0001'])
+ assert len(matches) == 2
+ matches = test_rules_collection.run(bracketsmatchtestfile, skip_list=['TEST0002'])
+ assert len(matches) == 1
+
+
+def test_skip_non_existent_id(test_rules_collection, ematchtestfile):
+ matches = test_rules_collection.run(ematchtestfile, skip_list=['DOESNOTEXIST'])
+ assert len(matches) == 3
+
+
+def test_no_duplicate_rule_ids(test_rules_collection):
+ real_rules = RulesCollection([os.path.abspath('./lib/ansiblelint/rules')])
+ rule_ids = [rule.id for rule in real_rules]
+ assert not any(y > 1 for y in collections.Counter(rule_ids).values())
+
+
+def test_rich_rule_listing():
+ """Test that rich list format output is rendered as a table.
+
+ This check also offers the contract of having rule id, short and long
+ descriptions in the console output.
+ """
+ rules_path = os.path.abspath('./test/rules')
+ result = run_ansible_lint("-r", rules_path, "-f", "rich", "-L")
+ assert result.returncode == 0
+
+ for rule in RulesCollection([rules_path]):
+ assert rule.id in result.stdout
+ assert rule.shortdesc in result.stdout
+ # description could wrap inside table, so we do not check full length
+ assert rule.description[:30] in result.stdout
diff --git a/test/TestRunner.py b/test/TestRunner.py
new file mode 100644
index 0000000..59740bd
--- /dev/null
+++ b/test/TestRunner.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
+
+import pytest
+
+from ansiblelint import formatters
+from ansiblelint.cli import abspath
+from ansiblelint.runner import Runner
+
+LOTS_OF_WARNINGS_PLAYBOOK = abspath('examples/lots_of_warnings.yml', os.getcwd())
+
+
+@pytest.mark.parametrize(('playbook', 'exclude', 'length'), (
+ ('test/nomatchestest.yml', [], 0),
+ ('test/unicode.yml', [], 1),
+ (LOTS_OF_WARNINGS_PLAYBOOK, [LOTS_OF_WARNINGS_PLAYBOOK], 0),
+ ('test/block.yml', [], 0),
+ ('test/become.yml', [], 0),
+ ('test/emptytags.yml', [], 0),
+ ('test/contains_secrets.yml', [], 0),
+))
+def test_runner(default_rules_collection, playbook, exclude, length):
+ runner = Runner(default_rules_collection, playbook, [], [], exclude)
+
+ matches = runner.run()
+
+ assert len(matches) == length
+
+
+@pytest.mark.parametrize(('formatter_cls', 'format_kwargs'), (
+ pytest.param(formatters.Formatter, {}, id='Formatter-plain'),
+ pytest.param(formatters.ParseableFormatter,
+ {'colored': True},
+ id='ParseableFormatter-colored'),
+ pytest.param(formatters.QuietFormatter,
+ {'colored': True},
+ id='QuietFormatter-colored'),
+ pytest.param(formatters.Formatter,
+ {'colored': True},
+ id='Formatter-colored'),
+))
+def test_runner_unicode_format(default_rules_collection, formatter_cls, format_kwargs):
+ formatter = formatter_cls(os.getcwd(), True)
+ runner = Runner(default_rules_collection, 'test/unicode.yml', [], [], [])
+
+ matches = runner.run()
+
+ formatter.format(matches[0], **format_kwargs)
+
+
+@pytest.mark.parametrize('directory_name', ('test/', os.path.abspath('test')))
+def test_runner_with_directory(default_rules_collection, directory_name):
+ runner = Runner(default_rules_collection, directory_name, [], [], [])
+ assert list(runner.playbooks)[0][1] == 'role'
+
+
+def test_files_not_scanned_twice(default_rules_collection):
+ checked_files = set()
+
+ filename = os.path.abspath('test/common-include-1.yml')
+ runner = Runner(default_rules_collection, filename, [], [], [], 0, checked_files)
+ run1 = runner.run()
+
+ filename = os.path.abspath('test/common-include-2.yml')
+ runner = Runner(default_rules_collection, filename, [], [], [], 0, checked_files)
+ run2 = runner.run()
+
+ assert (len(run1) + len(run2)) == 1
diff --git a/test/TestShellWithoutPipefail.py b/test/TestShellWithoutPipefail.py
new file mode 100644
index 0000000..c0c8545
--- /dev/null
+++ b/test/TestShellWithoutPipefail.py
@@ -0,0 +1,84 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.ShellWithoutPipefail import ShellWithoutPipefail
+from ansiblelint.testing import RunFromText
+
+FAIL_TASKS = '''
+---
+- hosts: localhost
+ become: no
+ tasks:
+ - name: pipeline without pipefail
+ shell: false | cat
+
+ - name: pipeline with or and pipe, no pipefail
+ shell: false || true | cat
+
+ - shell: |
+ df | grep '/dev'
+'''
+
+SUCCESS_TASKS = '''
+---
+- hosts: localhost
+ become: no
+ tasks:
+ - name: pipeline with pipefail
+ shell: set -o pipefail && false | cat
+
+ - name: pipeline with pipefail, multi-line
+ shell: |
+ set -o pipefail
+ false | cat
+
+ - name: pipeline with pipefail, complex set
+ shell: |
+ set -e -x -o pipefail
+ false | cat
+
+ - name: pipeline with pipefail, complex set
+ shell: |
+ set -e -x -o pipefail
+ false | cat
+
+ - name: pipeline with pipefail, complex set
+ shell: |
+ set -eo pipefail
+ false | cat
+
+ - name: pipeline without pipefail, ignoring errors
+ shell: false | cat
+ ignore_errors: true
+
+ - name: non-pipeline without pipefail
+ shell: "true"
+
+ - name: command without pipefail
+ command: "true"
+
+ - name: shell with or
+ shell:
+ false || true
+
+ - shell: |
+ set -o pipefail
+ df | grep '/dev'
+'''
+
+
+class TestShellWithoutPipeFail(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(ShellWithoutPipefail())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_fail(self):
+ results = self.runner.run_playbook(FAIL_TASKS)
+ self.assertEqual(3, len(results))
+
+ def test_success(self):
+ results = self.runner.run_playbook(SUCCESS_TASKS)
+ self.assertEqual(0, len(results))
diff --git a/test/TestSkipImportPlaybook.py b/test/TestSkipImportPlaybook.py
new file mode 100644
index 0000000..66e8520
--- /dev/null
+++ b/test/TestSkipImportPlaybook.py
@@ -0,0 +1,35 @@
+import pytest
+
+from ansiblelint.runner import Runner
+
+IMPORTED_PLAYBOOK = '''
+- hosts: all
+ tasks:
+ - name: success
+ fail: msg="fail"
+ when: False
+'''
+
+MAIN_PLAYBOOK = '''
+- hosts: all
+
+ tasks:
+ - name: should be shell # noqa 305 301
+ shell: echo lol
+
+- import_playbook: imported_playbook.yml
+'''
+
+
+@pytest.fixture
+def playbook(tmp_path):
+ playbook_path = tmp_path / 'playbook.yml'
+ playbook_path.write_text(MAIN_PLAYBOOK)
+ (tmp_path / 'imported_playbook.yml').write_text(IMPORTED_PLAYBOOK)
+ return str(playbook_path)
+
+
+def test_skip_import_playbook(default_rules_collection, playbook):
+ runner = Runner(default_rules_collection, playbook, [], [], [])
+ results = runner.run()
+ assert len(results) == 0
diff --git a/test/TestSkipInsideYaml.py b/test/TestSkipInsideYaml.py
new file mode 100644
index 0000000..a2abde2
--- /dev/null
+++ b/test/TestSkipInsideYaml.py
@@ -0,0 +1,122 @@
+import pytest
+
+ROLE_TASKS = '''
+---
+- name: test 303
+ command: git log
+ changed_when: False
+- name: test 303 (skipped)
+ command: git log # noqa 303
+ changed_when: False
+'''
+
+ROLE_TASKS_WITH_BLOCK = '''
+---
+- name: bad git 1 # noqa 401
+ action: git a=b c=d
+- name: bad git 2
+ action: git a=b c=d
+- name: Block with rescue and always section
+ block:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+ rescue:
+ - name: bad git 5 # noqa 401
+ action: git a=b c=d
+ - name: bad git 6
+ action: git a=b c=d
+ always:
+ - name: bad git 7 # noqa 401
+ action: git a=b c=d
+ - name: bad git 8
+ action: git a=b c=d
+'''
+
+PLAYBOOK = '''
+- hosts: all
+ tasks:
+ - name: test 402
+ action: hg
+ - name: test 402 (skipped) # noqa 402
+ action: hg
+
+ - name: test 401 and 501
+ become_user: alice
+ action: git
+ - name: test 401 and 501 (skipped) # noqa 401 501
+ become_user: alice
+ action: git
+
+ - name: test 204 and 206
+ get_url:
+ url: http://example.com/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/file.conf
+ dest: "{{dest_proj_path}}/foo.conf"
+ - name: test 204 and 206 (skipped)
+ get_url:
+ url: http://example.com/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/really_long_path/file.conf # noqa 204
+ dest: "{{dest_proj_path}}/foo.conf" # noqa 206
+
+ - name: test 302
+ command: creates=B chmod 644 A
+ - name: test 302
+ command: warn=yes creates=B chmod 644 A
+ - name: test 302 (skipped via no warn)
+ command: warn=no creates=B chmod 644 A
+ - name: test 302 (skipped via skip_ansible_lint)
+ command: creates=B chmod 644 A
+ tags:
+ - skip_ansible_lint
+
+ - name: test invalid action (skipped)
+ foo: bar
+ tags:
+ - skip_ansible_lint
+'''
+
+ROLE_META = '''
+galaxy_info: # noqa 701
+ author: your name # noqa 703
+ description: missing min_ansible_version and platforms. author default not changed
+ license: MIT
+'''
+
+ROLE_TASKS_WITH_BLOCK_BECOME = '''
+- hosts:
+ tasks:
+ - name: foo
+ become: true
+ block:
+ - name: bar
+ become_user: jonhdaa
+ command: "/etc/test.sh"
+'''
+
+
+def test_role_tasks(default_text_runner):
+ results = default_text_runner.run_role_tasks_main(ROLE_TASKS)
+ assert len(results) == 1
+
+
+def test_role_tasks_with_block(default_text_runner):
+ results = default_text_runner.run_role_tasks_main(ROLE_TASKS_WITH_BLOCK)
+ assert len(results) == 4
+
+
+@pytest.mark.parametrize(
+ ('playbook_src', 'results_num'),
+ (
+ (PLAYBOOK, 7),
+ (ROLE_TASKS_WITH_BLOCK_BECOME, 0),
+ ),
+ ids=('generic', 'with block become inheritance'),
+)
+def test_playbook(default_text_runner, playbook_src, results_num):
+ results = default_text_runner.run_playbook(playbook_src)
+ assert len(results) == results_num
+
+
+def test_role_meta(default_text_runner):
+ results = default_text_runner.run_role_meta_main(ROLE_META)
+ assert len(results) == 0
diff --git a/test/TestSkipPlaybookItems.py b/test/TestSkipPlaybookItems.py
new file mode 100644
index 0000000..5564ff2
--- /dev/null
+++ b/test/TestSkipPlaybookItems.py
@@ -0,0 +1,99 @@
+import pytest
+
+PLAYBOOK_PRE_TASKS = '''
+- hosts: all
+ tasks:
+ - name: bad git 1 # noqa 401
+ action: git a=b c=d
+ - name: bad git 2
+ action: git a=b c=d
+ pre_tasks:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+'''
+
+PLAYBOOK_POST_TASKS = '''
+- hosts: all
+ tasks:
+ - name: bad git 1 # noqa 401
+ action: git a=b c=d
+ - name: bad git 2
+ action: git a=b c=d
+ post_tasks:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+'''
+
+PLAYBOOK_HANDLERS = '''
+- hosts: all
+ tasks:
+ - name: bad git 1 # noqa 401
+ action: git a=b c=d
+ - name: bad git 2
+ action: git a=b c=d
+ handlers:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+'''
+
+PLAYBOOK_TWO_PLAYS = '''
+- hosts: all
+ tasks:
+ - name: bad git 1 # noqa 401
+ action: git a=b c=d
+ - name: bad git 2
+ action: git a=b c=d
+
+- hosts: all
+ tasks:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+'''
+
+PLAYBOOK_WITH_BLOCK = '''
+- hosts: all
+ tasks:
+ - name: bad git 1 # noqa 401
+ action: git a=b c=d
+ - name: bad git 2
+ action: git a=b c=d
+ - name: Block with rescue and always section
+ block:
+ - name: bad git 3 # noqa 401
+ action: git a=b c=d
+ - name: bad git 4
+ action: git a=b c=d
+ rescue:
+ - name: bad git 5 # noqa 401
+ action: git a=b c=d
+ - name: bad git 6
+ action: git a=b c=d
+ always:
+ - name: bad git 7 # noqa 401
+ action: git a=b c=d
+ - name: bad git 8
+ action: git a=b c=d
+'''
+
+
+@pytest.mark.parametrize(('playbook', 'length'), (
+ pytest.param(PLAYBOOK_PRE_TASKS, 2, id='PRE_TASKS'),
+ pytest.param(PLAYBOOK_POST_TASKS, 2, id='POST_TASKS'),
+ pytest.param(PLAYBOOK_HANDLERS, 2, id='HANDLERS'),
+ pytest.param(PLAYBOOK_TWO_PLAYS, 2, id='TWO_PLAYS'),
+ pytest.param(PLAYBOOK_WITH_BLOCK, 4, id='WITH_BLOCK'),
+))
+def test_pre_tasks(default_text_runner, playbook, length):
+ # When
+ results = default_text_runner.run_playbook(playbook)
+
+ # Then
+ assert len(results) == length
diff --git a/test/TestSudoRule.py b/test/TestSudoRule.py
new file mode 100644
index 0000000..a105854
--- /dev/null
+++ b/test/TestSudoRule.py
@@ -0,0 +1,67 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.SudoRule import SudoRule
+from ansiblelint.testing import RunFromText
+
+ROLE_2_ERRORS = '''
+- name: test
+ debug:
+ msg: 'test message'
+ sudo: yes
+ sudo_user: nobody
+'''
+
+ROLE_0_ERRORS = '''
+- name: test
+ debug:
+ msg: 'test message'
+ become: yes
+ become_user: somebody
+'''
+
+PLAY_4_ERRORS = '''
+- hosts: all
+ sudo: yes
+ sudo_user: somebody
+ tasks:
+ - name: test
+ debug:
+ msg: 'test message'
+ sudo: yes
+ sudo_user: nobody
+'''
+
+PLAY_1_ERROR = '''
+- hosts: all
+ tasks:
+ - name: test
+ debug:
+ msg: 'test message'
+ sudo: yes
+'''
+
+
+class TestSudoRule(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(SudoRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_run_role_fail(self):
+ results = self.runner.run_role_tasks_main(ROLE_2_ERRORS)
+ self.assertEqual(2, len(results))
+
+ def test_run_role_pass(self):
+ results = self.runner.run_role_tasks_main(ROLE_0_ERRORS)
+ self.assertEqual(0, len(results))
+
+ def test_play_root_and_task_fail(self):
+ results = self.runner.run_playbook(PLAY_4_ERRORS)
+ self.assertEqual(4, len(results))
+
+ def test_play_task_fail(self):
+ results = self.runner.run_playbook(PLAY_1_ERROR)
+ self.assertEqual(1, len(results))
diff --git a/test/TestTaskHasName.py b/test/TestTaskHasName.py
new file mode 100644
index 0000000..3a35f29
--- /dev/null
+++ b/test/TestTaskHasName.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.TaskHasNameRule import TaskHasNameRule
+from ansiblelint.runner import Runner
+
+
+class TestTaskHasNameRule(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(TaskHasNameRule())
+
+ def test_file_positive(self):
+ success = 'test/task-has-name-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/task-has-name-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(2, len(errs))
diff --git a/test/TestTaskIncludes.py b/test/TestTaskIncludes.py
new file mode 100644
index 0000000..a3506bd
--- /dev/null
+++ b/test/TestTaskIncludes.py
@@ -0,0 +1,34 @@
+import pytest
+
+from ansiblelint.runner import Runner
+
+
+@pytest.mark.parametrize(
+ ('filename', 'playbooks_count'),
+ (
+ pytest.param('blockincludes', 4, id='block included tasks'),
+ pytest.param(
+ 'blockincludes2', 4,
+ id='block included tasks with rescue and always',
+ ),
+ pytest.param('taskincludes', 4, id='included tasks'),
+ pytest.param(
+ 'taskincludes_2_4_style', 4,
+ id='include tasks 2.4 style',
+ ),
+ pytest.param('taskimports', 4, id='import tasks 2 4 style'),
+ pytest.param(
+ 'include-in-block', 3,
+ id='include tasks with block include',
+ ),
+ pytest.param(
+ 'include-import-tasks-in-role', 4,
+ id='include tasks in role',
+ ),
+ ),
+)
+def test_included_tasks(default_rules_collection, filename, playbooks_count):
+ playbook_path = 'test/{filename}.yml'.format(**locals())
+ runner = Runner(default_rules_collection, playbook_path, [], [], [])
+ runner.run()
+ assert len(runner.playbooks) == playbooks_count
diff --git a/test/TestTaskNoLocalAction.py b/test/TestTaskNoLocalAction.py
new file mode 100644
index 0000000..74be24c
--- /dev/null
+++ b/test/TestTaskNoLocalAction.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.TaskNoLocalAction import TaskNoLocalAction
+from ansiblelint.testing import RunFromText
+
+TASK_LOCAL_ACTION = '''
+- name: task example
+ local_action:
+ module: boto3_facts
+'''
+
+
+class TestTaskNoLocalAction(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(TaskNoLocalAction())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_local_action(self):
+ results = self.runner.run_role_tasks_main(TASK_LOCAL_ACTION)
+ self.assertEqual(1, len(results))
diff --git a/test/TestUseCommandInsteadOfShell.py b/test/TestUseCommandInsteadOfShell.py
new file mode 100644
index 0000000..d7e44a2
--- /dev/null
+++ b/test/TestUseCommandInsteadOfShell.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.UseCommandInsteadOfShellRule import UseCommandInsteadOfShellRule
+from ansiblelint.runner import Runner
+
+
+class TestUseCommandInsteadOfShell(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(UseCommandInsteadOfShellRule())
+
+ def test_file_positive(self):
+ success = 'test/command-instead-of-shell-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/command-instead-of-shell-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(2, len(errs))
diff --git a/test/TestUseHandlerRatherThanWhenChanged.py b/test/TestUseHandlerRatherThanWhenChanged.py
new file mode 100644
index 0000000..5306e2f
--- /dev/null
+++ b/test/TestUseHandlerRatherThanWhenChanged.py
@@ -0,0 +1,88 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.UseHandlerRatherThanWhenChangedRule import (
+ UseHandlerRatherThanWhenChangedRule,
+)
+from ansiblelint.testing import RunFromText
+
+SUCCESS_TASKS = '''
+- name: print helpful error message
+ debug:
+ var: result
+ when: result.failed
+
+- name: do something when hello is output
+ debug:
+ msg: why isn't this a handler
+ when: result.stdout == "hello"
+
+- name: never actually debug
+ debug:
+ var: result
+ when: False
+
+- name: Dont execute this step
+ debug:
+ msg: "debug message"
+ when:
+ - false
+
+- name: check when with a list
+ debug:
+ var: result
+ when:
+ - conditionA
+ - conditionB
+'''
+
+
+FAIL_TASKS = '''
+- name: execute command
+ command: echo hello
+ register: result
+
+- name: this should be a handler
+ debug:
+ msg: why isn't this a handler
+ when: result.changed
+
+- name: this should be a handler 2
+ debug:
+ msg: why isn't this a handler
+ when: result|changed
+
+- name: this should be a handler 3
+ debug:
+ msg: why isn't this a handler
+ when: result.changed == true
+
+- name: this should be a handler 4
+ debug:
+ msg: why isn't this a handler
+ when: result['changed'] == true
+
+- name: this should be a handler 5
+ debug:
+ msg: why isn't this a handler
+ when:
+ - result['changed'] == true
+ - another_condition
+'''
+
+
+class TestUseHandlerRatherThanWhenChanged(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(UseHandlerRatherThanWhenChangedRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_success(self):
+ results = self.runner.run_role_tasks_main(SUCCESS_TASKS)
+ self.assertEqual(0, len(results))
+
+ def test_fail(self):
+ results = self.runner.run_role_tasks_main(FAIL_TASKS)
+ self.assertEqual(5, len(results))
diff --git a/test/TestUsingBareVariablesIsDeprecated.py b/test/TestUsingBareVariablesIsDeprecated.py
new file mode 100644
index 0000000..42c3b4d
--- /dev/null
+++ b/test/TestUsingBareVariablesIsDeprecated.py
@@ -0,0 +1,24 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.UsingBareVariablesIsDeprecatedRule import UsingBareVariablesIsDeprecatedRule
+from ansiblelint.runner import Runner
+
+
+class TestUsingBareVariablesIsDeprecated(unittest.TestCase):
+ collection = RulesCollection()
+
+ def setUp(self):
+ self.collection.register(UsingBareVariablesIsDeprecatedRule())
+
+ def test_file_positive(self):
+ success = 'test/using-bare-variables-success.yml'
+ good_runner = Runner(self.collection, success, [], [], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_file_negative(self):
+ failure = 'test/using-bare-variables-failure.yml'
+ bad_runner = Runner(self.collection, failure, [], [], [])
+ errs = bad_runner.run()
+ self.assertEqual(14, len(errs))
diff --git a/test/TestUtils.py b/test/TestUtils.py
new file mode 100644
index 0000000..58824b0
--- /dev/null
+++ b/test/TestUtils.py
@@ -0,0 +1,317 @@
+# -*- coding: utf-8 -*-
+
+# 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.
+"""Tests for generic utilitary functions."""
+
+import logging
+import os
+import os.path
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+from ansiblelint import cli, constants, utils
+from ansiblelint.__main__ import initialize_logger
+from ansiblelint.file_utils import normpath
+
+
+@pytest.mark.parametrize(('string', 'expected_cmd', 'expected_args', 'expected_kwargs'), (
+ pytest.param('', '', [], {}, id='blank'),
+ pytest.param('vars:', 'vars', [], {}, id='single_word'),
+ pytest.param('hello: a=1', 'hello', [], {'a': '1'}, id='string_module_and_arg'),
+ pytest.param('action: hello a=1', 'hello', [], {'a': '1'}, id='strips_action'),
+ pytest.param('action: whatever bobbins x=y z=x c=3',
+ 'whatever',
+ ['bobbins', 'x=y', 'z=x', 'c=3'],
+ {},
+ id='more_than_one_arg'),
+ pytest.param('action: command chdir=wxy creates=zyx tar xzf zyx.tgz',
+ 'command',
+ ['tar', 'xzf', 'zyx.tgz'],
+ {'chdir': 'wxy', 'creates': 'zyx'},
+ id='command_with_args'),
+))
+def test_tokenize(string, expected_cmd, expected_args, expected_kwargs):
+ """Test that tokenize works for different input types."""
+ (cmd, args, kwargs) = utils.tokenize(string)
+ assert cmd == expected_cmd
+ assert args == expected_args
+ assert kwargs == expected_kwargs
+
+
+@pytest.mark.parametrize(('reference_form', 'alternate_forms'), (
+ pytest.param(dict(name='hello', action='command chdir=abc echo hello world'),
+ (dict(name="hello", command="chdir=abc echo hello world"), ),
+ id='simple_command'),
+ pytest.param({'git': {'version': 'abc'}, 'args': {'repo': 'blah', 'dest': 'xyz'}},
+ ({'git': {'version': 'abc', 'repo': 'blah', 'dest': 'xyz'}},
+ {"git": 'version=abc repo=blah dest=xyz'},
+ {"git": None, "args": {'repo': 'blah', 'dest': 'xyz', 'version': 'abc'}},
+ ),
+ id='args')
+))
+def test_normalize(reference_form, alternate_forms):
+ """Test that tasks specified differently are normalized same way."""
+ normal_form = utils.normalize_task(reference_form, 'tasks.yml')
+
+ for form in alternate_forms:
+ assert normal_form == utils.normalize_task(form, 'tasks.yml')
+
+
+def test_normalize_complex_command():
+ """Test that tasks specified differently are normalized same way."""
+ task1 = dict(name="hello", action={'module': 'pip',
+ 'name': 'df',
+ 'editable': 'false'})
+ task2 = dict(name="hello", pip={'name': 'df',
+ 'editable': 'false'})
+ task3 = dict(name="hello", pip="name=df editable=false")
+ task4 = dict(name="hello", action="pip name=df editable=false")
+ assert utils.normalize_task(task1, 'tasks.yml') == utils.normalize_task(task2, 'tasks.yml')
+ assert utils.normalize_task(task2, 'tasks.yml') == utils.normalize_task(task3, 'tasks.yml')
+ assert utils.normalize_task(task3, 'tasks.yml') == utils.normalize_task(task4, 'tasks.yml')
+
+
+def test_extract_from_list():
+ """Check that tasks get extracted from blocks if present."""
+ block = {
+ 'block': [{'tasks': {'name': 'hello', 'command': 'whoami'}}],
+ 'test_none': None,
+ 'test_string': 'foo',
+ }
+ blocks = [block]
+
+ test_list = utils.extract_from_list(blocks, ['block'])
+ test_none = utils.extract_from_list(blocks, ['test_none'])
+
+ assert list(block['block']) == test_list
+ assert list() == test_none
+ with pytest.raises(RuntimeError):
+ utils.extract_from_list(blocks, ['test_string'])
+
+
+@pytest.mark.parametrize(('template', 'output'), (
+ pytest.param('{{ playbook_dir }}', '/a/b/c', id='simple'),
+ pytest.param("{{ 'hello' | doesnotexist }}",
+ "{{ 'hello' | doesnotexist }}",
+ id='unknown_filter'),
+ pytest.param('{{ hello | to_json }}',
+ '{{ hello | to_json }}',
+ id='to_json_filter_on_undefined_variable'),
+ pytest.param('{{ hello | to_nice_yaml }}',
+ '{{ hello | to_nice_yaml }}',
+ id='to_nice_yaml_filter_on_undefined_variable'),
+))
+def test_template(template, output):
+ """Verify that resolvable template vars and filters get rendered."""
+ result = utils.template('/base/dir', template, dict(playbook_dir='/a/b/c'))
+ assert result == output
+
+
+def test_task_to_str_unicode():
+ """Ensure that extracting messages from tasks preserves Unicode."""
+ task = dict(fail=dict(msg=u"unicode é ô à"))
+ result = utils.task_to_str(utils.normalize_task(task, 'filename.yml'))
+ assert result == u"fail msg=unicode é ô à"
+
+
+@pytest.mark.parametrize('path', (
+ pytest.param(Path('a/b/../'), id='pathlib.Path'),
+ pytest.param('a/b/../', id='str'),
+))
+def test_normpath_with_path_object(path):
+ """Ensure that relative parent dirs are normalized in paths."""
+ assert normpath(path) == "a"
+
+
+def test_expand_path_vars(monkeypatch):
+ """Ensure that tilde and env vars are expanded in paths."""
+ test_path = '/test/path'
+ monkeypatch.setenv('TEST_PATH', test_path)
+ assert utils.expand_path_vars('~') == os.path.expanduser('~')
+ assert utils.expand_path_vars('$TEST_PATH') == test_path
+
+
+@pytest.mark.parametrize(('test_path', 'expected'), (
+ pytest.param(Path('$TEST_PATH'), "/test/path", id='pathlib.Path'),
+ pytest.param('$TEST_PATH', "/test/path", id='str'),
+ pytest.param(' $TEST_PATH ', "/test/path", id='stripped-str'),
+ pytest.param('~', os.path.expanduser('~'), id='home'),
+))
+def test_expand_paths_vars(test_path, expected, monkeypatch):
+ """Ensure that tilde and env vars are expanded in paths lists."""
+ monkeypatch.setenv('TEST_PATH', '/test/path')
+ assert utils.expand_paths_vars([test_path]) == [expected]
+
+
+@pytest.mark.parametrize(
+ ('reset_env_var', 'message_prefix'),
+ (
+ ('PATH',
+ "Failed to locate command: "),
+ ('GIT_DIR',
+ "Failed to discover yaml files to lint using git: ")
+ ),
+ ids=('no Git installed', 'outside Git repository'),
+)
+def test_get_yaml_files_git_verbose(
+ reset_env_var,
+ message_prefix,
+ monkeypatch,
+ caplog
+):
+ """Ensure that autodiscovery lookup failures are logged."""
+ options = cli.get_config(['-v'])
+ initialize_logger(options.verbosity)
+ monkeypatch.setenv(reset_env_var, '')
+ utils.get_yaml_files(options)
+
+ expected_info = (
+ "ansiblelint.utils",
+ logging.INFO,
+ 'Discovering files to lint: git ls-files *.yaml *.yml')
+
+ assert expected_info in caplog.record_tuples
+ assert any(m.startswith(message_prefix) for m in caplog.messages)
+
+
+@pytest.mark.parametrize(
+ 'is_in_git',
+ (True, False),
+ ids=('in Git', 'outside Git'),
+)
+def test_get_yaml_files_silent(is_in_git, monkeypatch, capsys):
+ """Verify that no stderr output is displayed while discovering yaml files.
+
+ (when the verbosity is off, regardless of the Git or Git-repo presence)
+
+ Also checks expected number of files are detected.
+ """
+ options = cli.get_config([])
+ test_dir = Path(__file__).resolve().parent
+ lint_path = test_dir / 'roles' / 'test-role'
+ if not is_in_git:
+ monkeypatch.setenv('GIT_DIR', '')
+
+ yaml_count = (
+ len(list(lint_path.glob('**/*.yml'))) + len(list(lint_path.glob('**/*.yaml')))
+ )
+
+ monkeypatch.chdir(str(lint_path))
+ files = utils.get_yaml_files(options)
+ stderr = capsys.readouterr().err
+ assert not stderr, 'No stderr output is expected when the verbosity is off'
+ assert len(files) == yaml_count, (
+ "Expected to find {yaml_count} yaml files in {lint_path}".format_map(
+ locals(),
+ )
+ )
+
+
+def test_logger_debug(caplog):
+ """Test that the double verbosity arg causes logger to be DEBUG."""
+ options = cli.get_config(['-vv'])
+ initialize_logger(options.verbosity)
+
+ expected_info = (
+ "ansiblelint.__main__",
+ logging.DEBUG,
+ 'Logging initialized to level 10',
+ )
+
+ assert expected_info in caplog.record_tuples
+
+
+@pytest.mark.xfail
+def test_cli_auto_detect(capfd):
+ """Test that run without arguments it will detect and lint the entire repository."""
+ cmd = sys.executable, "-m", "ansiblelint", "-v", "-p", "--nocolor"
+ result = subprocess.run(cmd, check=False).returncode
+
+ # We de expect to fail on our own repo due to test examples we have
+ # TODO(ssbarnea) replace it with exact return code once we document them
+ assert result != 0
+
+ out, err = capfd.readouterr()
+
+ # Confirmation that it runs in auto-detect mode
+ assert "Discovering files to lint: git ls-files *.yaml *.yml" in err
+ # Expected failure to detect file type"
+ assert "Unknown file type: test/fixtures/unknown-type.yml" in err
+ # An expected rule match from our examples
+ assert "examples/roles/bobbins/tasks/main.yml:2: " \
+ "[E401] Git checkouts must contain explicit version" in out
+ # assures that our .ansible-lint exclude was effective in excluding github files
+ assert "Unknown file type: .github/" not in out
+ # assures that we can parse playbooks as playbooks
+ assert "Unknown file type: test/test/always-run-success.yml" not in err
+
+
+@pytest.mark.xfail
+def test_is_playbook():
+ """Verify that we can detect a playbook as a playbook."""
+ assert utils.is_playbook("test/test/always-run-success.yml")
+
+
+def test_auto_detect_exclude(monkeypatch):
+ """Verify that exclude option can be used to narrow down detection."""
+ options = cli.get_config(['--exclude', 'foo'])
+
+ def mockreturn(options):
+ return ['foo/playbook.yml', 'bar/playbook.yml']
+
+ monkeypatch.setattr(utils, 'get_yaml_files', mockreturn)
+ result = utils.get_playbooks_and_roles(options)
+ assert result == ['bar/playbook.yml']
+
+
+_DEFAULT_RULEDIRS = [constants.DEFAULT_RULESDIR]
+_CUSTOM_RULESDIR = Path(__file__).parent / "custom_rules"
+_CUSTOM_RULEDIRS = [
+ str(_CUSTOM_RULESDIR / "example_inc"),
+ str(_CUSTOM_RULESDIR / "example_com")
+]
+
+
+@pytest.mark.parametrize(("user_ruledirs", "use_default", "expected"), (
+ ([], True, _DEFAULT_RULEDIRS),
+ ([], False, _DEFAULT_RULEDIRS),
+ (_CUSTOM_RULEDIRS, True, _CUSTOM_RULEDIRS + _DEFAULT_RULEDIRS),
+ (_CUSTOM_RULEDIRS, False, _CUSTOM_RULEDIRS)
+))
+def test_get_rules_dirs(user_ruledirs, use_default, expected):
+ """Test it returns expected dir lists."""
+ assert utils.get_rules_dirs(user_ruledirs, use_default) == expected
+
+
+@pytest.mark.parametrize(("user_ruledirs", "use_default", "expected"), (
+ ([], True, sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS),
+ ([], False, sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS),
+ (_CUSTOM_RULEDIRS, True,
+ _CUSTOM_RULEDIRS + sorted(_CUSTOM_RULEDIRS) + _DEFAULT_RULEDIRS),
+ (_CUSTOM_RULEDIRS, False, _CUSTOM_RULEDIRS)
+))
+def test_get_rules_dirs_with_custom_rules(user_ruledirs, use_default, expected, monkeypatch):
+ """Test it returns expected dir lists when custom rules exist."""
+ monkeypatch.setenv(constants.CUSTOM_RULESDIR_ENVVAR, str(_CUSTOM_RULESDIR))
+ assert utils.get_rules_dirs(user_ruledirs, use_default) == expected
diff --git a/test/TestVariableHasSpaces.py b/test/TestVariableHasSpaces.py
new file mode 100644
index 0000000..01b12c9
--- /dev/null
+++ b/test/TestVariableHasSpaces.py
@@ -0,0 +1,54 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.VariableHasSpacesRule import VariableHasSpacesRule
+from ansiblelint.testing import RunFromText
+
+TASK_VARIABLES = '''
+- name: good variable format
+ debug:
+ msg: "{{ good_format }}"
+- name: good variable format
+ debug:
+ msg: "Value: {{ good_format }}"
+- name: jinja escaping allowed
+ debug:
+ msg: "{{ '{{' }}"
+- name: jinja escaping allowed
+ shell: docker info --format '{{ '{{' }}json .Swarm.LocalNodeState{{ '}}' }}' | tr -d '"'
+- name: jinja whitespace control allowed
+ debug:
+ msg: |
+ {{ good_format }}/
+ {{- good_format }}
+ {{- good_format -}}
+- name: bad variable format
+ debug:
+ msg: "{{bad_format}}"
+- name: bad variable format
+ debug:
+ msg: "Value: {{ bad_format}}"
+- name: bad variable format
+ debug:
+ msg: "{{bad_format }}"
+- name: not a jinja variable
+ debug:
+ msg: "test"
+ example: "data = ${lookup{$local_part}lsearch{/etc/aliases}}"
+- name: JSON inside jinja is valid
+ debug:
+ msg: "{{ {'test': {'subtest': variable}} }}"
+'''
+
+
+class TestVariableHasSpaces(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(VariableHasSpacesRule())
+
+ def setUp(self):
+ self.runner = RunFromText(self.collection)
+
+ def test_variable_has_spaces(self):
+ results = self.runner.run_role_tasks_main(TASK_VARIABLES)
+ self.assertEqual(3, len(results))
diff --git a/test/TestWithSkipTagId.py b/test/TestWithSkipTagId.py
new file mode 100644
index 0000000..129a00d
--- /dev/null
+++ b/test/TestWithSkipTagId.py
@@ -0,0 +1,39 @@
+# pylint: disable=preferred-module # FIXME: remove once migrated per GH-725
+import unittest
+
+from ansiblelint.rules import RulesCollection
+from ansiblelint.rules.TrailingWhitespaceRule import TrailingWhitespaceRule
+from ansiblelint.runner import Runner
+
+
+class TestWithSkipTagId(unittest.TestCase):
+ collection = RulesCollection()
+ collection.register(TrailingWhitespaceRule())
+ file = 'test/with-skip-tag-id.yml'
+
+ def test_negative_no_param(self):
+ bad_runner = Runner(self.collection, self.file, [], [], [])
+ errs = bad_runner.run()
+ self.assertGreater(len(errs), 0)
+
+ def test_negative_with_id(self):
+ with_id = '201'
+ bad_runner = Runner(self.collection, self.file, [with_id], [], [])
+ errs = bad_runner.run()
+ self.assertGreater(len(errs), 0)
+
+ def test_negative_with_tag(self):
+ with_tag = 'ANSIBLE0002'
+ bad_runner = Runner(self.collection, self.file, [with_tag], [], [])
+ errs = bad_runner.run()
+ self.assertGreater(len(errs), 0)
+
+ def test_positive_skip_id(self):
+ skip_id = '201'
+ good_runner = Runner(self.collection, self.file, [], [skip_id], [])
+ self.assertEqual([], good_runner.run())
+
+ def test_positive_skip_tag(self):
+ skip_tag = 'ANSIBLE0002'
+ good_runner = Runner(self.collection, self.file, [], [skip_tag], [])
+ self.assertEqual([], good_runner.run())
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..124a757
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1 @@
+"""Use ansiblelint.testing instead for test reusables."""
diff --git a/test/always-run-failure.yml b/test/always-run-failure.yml
new file mode 100644
index 0000000..c9ff225
--- /dev/null
+++ b/test/always-run-failure.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+
+ tasks:
+ - name: always_run is deprecated
+ debug: msg="always_run is deprecated"
+ always_run: yes
diff --git a/test/always-run-success.yml b/test/always-run-success.yml
new file mode 100644
index 0000000..30559e0
--- /dev/null
+++ b/test/always-run-success.yml
@@ -0,0 +1,6 @@
+- hosts: localhost
+
+ tasks:
+ - name: always_run is deprecated
+ debug: msg="always_run is deprecated"
+ check_mode: yes
diff --git a/test/bar.txt b/test/bar.txt
new file mode 100644
index 0000000..e22f90b
--- /dev/null
+++ b/test/bar.txt
@@ -0,0 +1 @@
+Bar file
diff --git a/test/become-user-without-become-failure.yml b/test/become-user-without-become-failure.yml
new file mode 100644
index 0000000..a4051f0
--- /dev/null
+++ b/test/become-user-without-become-failure.yml
@@ -0,0 +1,26 @@
+- hosts: localhost
+ name: become_user without become play
+ become_user: root
+
+ tasks:
+ - debug:
+ msg: hello
+
+- hosts: localhost
+
+ tasks:
+ - name: become_user without become task
+ command: whoami
+ become_user: postgres
+
+- hosts: localhost
+
+ tasks:
+ - name: a block with become and become_user on different tasks
+ block:
+ - name: become
+ become: true
+ command: whoami
+ - name: become_user
+ become_user: postgres
+ command: whoami
diff --git a/test/become-user-without-become-success.yml b/test/become-user-without-become-success.yml
new file mode 100644
index 0000000..a8d64da
--- /dev/null
+++ b/test/become-user-without-become-success.yml
@@ -0,0 +1,30 @@
+- hosts: localhost
+ become_user: root
+ become: true
+
+ tasks:
+ - debug:
+ msg: hello
+
+- hosts: localhost
+
+ tasks:
+ - command: whoami
+ become_user: postgres
+ become: true
+
+- hosts: localhost
+ become: true
+
+ tasks:
+ - name: Accepts a become from higher scope
+ command: whoami
+ become_user: postgres
+
+- hosts: localhost
+ become_user: postgres
+
+ tasks:
+ - name: Accepts a become from a lower scope
+ command: whoami
+ become: true
diff --git a/test/become.yml b/test/become.yml
new file mode 100644
index 0000000..f2ea524
--- /dev/null
+++ b/test/become.yml
@@ -0,0 +1,14 @@
+- hosts: all
+
+ tasks:
+ - name: clone content repository
+ git:
+ repo: '{{ archive_services_repo_url }}'
+ dest: '/home/www'
+ accept_hostkey: yes
+ version: master
+ update: no
+ become: yes
+ become_user: nobody
+ notify:
+ - restart apache2
diff --git a/test/block.yml b/test/block.yml
new file mode 100644
index 0000000..1aac26e
--- /dev/null
+++ b/test/block.yml
@@ -0,0 +1,26 @@
+---
+- hosts: all
+
+ pre_tasks:
+ - { include: 'doesnotexist.yml' }
+
+ tasks:
+ - block:
+ - name: successful debug message
+ debug: msg='i execute normally'
+ - name: failure command
+ command: /bin/false
+ changed_when: False
+ - name: never reached debug message
+ debug: msg='i never execute, cause ERROR!'
+ rescue:
+ - name: exception debug message
+ debug: msg='I caught an error'
+ - name: another failure command
+ command: /bin/false
+ changed_when: False
+ - name: another missed debug message
+ debug: msg='I also never execute :-('
+ always:
+ - name: always reached debug message
+ debug: msg="this always executes"
diff --git a/test/blockincludes.yml b/test/blockincludes.yml
new file mode 100644
index 0000000..5bea9be
--- /dev/null
+++ b/test/blockincludes.yml
@@ -0,0 +1,13 @@
+---
+- hosts: webservers
+ vars:
+ varset: varset
+ tasks:
+ - block:
+ - include: nestedincludes.yml tags=nested
+ - block:
+ - include: "{{ varnotset }}.yml"
+ - block:
+ - include: "{{ varset }}.yml"
+ - block:
+ - include: "directory with spaces/main.yml"
diff --git a/test/blockincludes2.yml b/test/blockincludes2.yml
new file mode 100644
index 0000000..03d7a75
--- /dev/null
+++ b/test/blockincludes2.yml
@@ -0,0 +1,13 @@
+---
+- hosts: webservers
+ vars:
+ varset: varset
+ tasks:
+ - block:
+ - include: nestedincludes.yml tags=nested
+ - block:
+ - include: "{{ varnotset }}.yml"
+ rescue:
+ - include: "{{ varset }}.yml"
+ always:
+ - include: "directory with spaces/main.yml"
diff --git a/test/brackets-do-not-match-test.yml b/test/brackets-do-not-match-test.yml
new file mode 100644
index 0000000..dfa5ff8
--- /dev/null
+++ b/test/brackets-do-not-match-test.yml
@@ -0,0 +1,22 @@
+---
+- hosts: foo
+ roles:
+ - ../../../roles/base_os
+ - ../../../roles/repos
+ - {
+ role: ../../../roles/openshift_master,
+ oo_minion_ips: "{ hostvars['localhost'].oo_minion_ips | default(['']) }}",
+ oo_bind_ip: "{{ hostvars[inventory_hostname].ansible_eth0.ipv4.address | default(['']) }}"
+ }
+ - ../../../roles/pods
+
+- name: "Set Origin specific facts on localhost (for later use)"
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Setting oo_minion_ips fact on localhost
+ set_fact:
+ oo_minion_ips: "{{ hostvars
+ | oo_select_keys(groups['tag_env-host-type-' + oo_env + '-openshift-minion'])
+ | oo_collect(attribute='ansible_eth0.ipv4.address') }"
+ when: groups['tag_env-host-type-' + oo_env + '-openshift-minion'] is defined
diff --git a/test/bracketsmatchtest.yml b/test/bracketsmatchtest.yml
new file mode 100644
index 0000000..46b213d
--- /dev/null
+++ b/test/bracketsmatchtest.yml
@@ -0,0 +1,3 @@
+val1: "{{dest}}"
+val2: worry
+val3: "{{victory}}"
diff --git a/test/command-check-failure.yml b/test/command-check-failure.yml
new file mode 100644
index 0000000..d58c09d
--- /dev/null
+++ b/test/command-check-failure.yml
@@ -0,0 +1,11 @@
+- hosts: localhost
+ tasks:
+ - name: command without checks
+ command: echo blah
+ args:
+ chdir: X
+
+ - name: shell without checks
+ shell: echo blah
+ args:
+ chdir: X
diff --git a/test/command-check-success.yml b/test/command-check-success.yml
new file mode 100644
index 0000000..24266ff
--- /dev/null
+++ b/test/command-check-success.yml
@@ -0,0 +1,61 @@
+- hosts: localhost
+ tasks:
+ - name: command with creates check
+ command: echo blah
+ args:
+ creates: Z
+
+ - name: command with removes check
+ command: echo blah
+ args:
+ removes: Z
+
+ - name: command with changed_when
+ command: echo blah
+ changed_when: False
+
+ - name: command with inline creates
+ command: creates=Z echo blah
+
+ - 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:
+ creates: Z
+
+ - name: shell with removes check
+ shell: echo blah
+ args:
+ removes: Z
+
+ - name: shell with changed_when
+ shell: echo blah
+ changed_when: False
+
+ - name: shell with inline creates
+ shell: creates=Z echo blah
+
+ - name: shell with inline removes
+ shell: removes=Z echo blah
+
+ - name: shell with cmd
+ shell:
+ cmd:
+ echo blah
+ args:
+ creates: Z
+
+- hosts: localhost
+ handlers:
+ - name: restart something
+ command: do something
+ - include: included-handlers.yml
diff --git a/test/command-instead-of-shell-failure.yml b/test/command-instead-of-shell-failure.yml
new file mode 100644
index 0000000..7b8d829
--- /dev/null
+++ b/test/command-instead-of-shell-failure.yml
@@ -0,0 +1,8 @@
+---
+- hosts: localhost
+ tasks:
+ - name: shell no pipe
+ shell: echo hello
+
+ - name: shell with jinja filter
+ shell: echo {{"hello"|upper}}
diff --git a/test/command-instead-of-shell-success.yml b/test/command-instead-of-shell-success.yml
new file mode 100644
index 0000000..410ff97
--- /dev/null
+++ b/test/command-instead-of-shell-success.yml
@@ -0,0 +1,37 @@
+- hosts: localhost
+ tasks:
+ - name: shell with pipe
+ shell: echo hello | true
+
+ - name: shell with redirect
+ shell: echo hello > /tmp/hello
+
+ - name: chain two shell commands
+ shell: echo hello && echo goodbye
+
+ - name: run commands in succession
+ shell: echo hello ; echo goodbye
+
+ - name: use variables
+ shell: echo $HOME $USER
+
+ - name: use * for globbing
+ shell: ls foo*
+
+ - name: use ? for globbing
+ shell: ls foo?
+
+ - name: use [] for globbing
+ shell: ls foo[1,2,3]
+
+ - name: use shell generator
+ shell: ls foo{.txt,.xml}
+
+ - name: use backticks
+ shell: ls `ls foo*`
+
+ - name: use shell with cmd
+ shell:
+ cmd: |
+ set -x
+ ls foo?
diff --git a/test/common-include-1.yml b/test/common-include-1.yml
new file mode 100644
index 0000000..287df85
--- /dev/null
+++ b/test/common-include-1.yml
@@ -0,0 +1,4 @@
+---
+- hosts: webservers
+ tasks:
+ - include: included-with-lint.yml
diff --git a/test/common-include-2.yml b/test/common-include-2.yml
new file mode 100644
index 0000000..287df85
--- /dev/null
+++ b/test/common-include-2.yml
@@ -0,0 +1,4 @@
+---
+- hosts: webservers
+ tasks:
+ - include: included-with-lint.yml
diff --git a/test/contains_secrets.yml b/test/contains_secrets.yml
new file mode 100644
index 0000000..aed5a0e
--- /dev/null
+++ b/test/contains_secrets.yml
@@ -0,0 +1,14 @@
+- hosts: localhost
+ vars:
+ plain: hello123
+ # just 'hello123' encrypted with 'letmein' for test purposes
+ secret: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ 63346434613163653866303630313238626164313961613935373137323639636333393338386232
+ 3735313061316666343839343665383036623237353263310a623639336530383433343833653138
+ 30393032393534316164613834393864616566646164363830316664623636643731383164376163
+ 3736653037356435310a303533383533353739323834343637366438633766666163656330343631
+ 3066
+ tasks:
+ - name: just a debug task
+ debug: msg="hello world"
diff --git a/test/custom_rules/__init__.py b/test/custom_rules/__init__.py
new file mode 100644
index 0000000..09a0f04
--- /dev/null
+++ b/test/custom_rules/__init__.py
@@ -0,0 +1 @@
+"""Dummy test module."""
diff --git a/test/custom_rules/example_com/ExampleComRule.py b/test/custom_rules/example_com/ExampleComRule.py
new file mode 100644
index 0000000..0ce9c1c
--- /dev/null
+++ b/test/custom_rules/example_com/ExampleComRule.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2020, Ansible Project
+#
+# 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.
+"""A dummy custom rule module #2."""
+
+import ansiblelint.rules.AlwaysRunRule
+
+
+class ExampleComRule(ansiblelint.rules.AlwaysRunRule.AlwaysRunRule):
+ """A dummy custom rule class."""
+
+ id = '100002'
diff --git a/test/custom_rules/example_com/__init__.py b/test/custom_rules/example_com/__init__.py
new file mode 100644
index 0000000..a633c75
--- /dev/null
+++ b/test/custom_rules/example_com/__init__.py
@@ -0,0 +1 @@
+"""A dummy test module #2."""
diff --git a/test/custom_rules/example_inc/CustomAlwaysRunRule.py b/test/custom_rules/example_inc/CustomAlwaysRunRule.py
new file mode 100644
index 0000000..1bff62d
--- /dev/null
+++ b/test/custom_rules/example_inc/CustomAlwaysRunRule.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2020, Ansible Project
+#
+# 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.
+"""Dummy custom rule module."""
+
+import ansiblelint.rules.AlwaysRunRule
+
+
+class CustomAlwaysRunRule(ansiblelint.rules.AlwaysRunRule.AlwaysRunRule):
+ """Dummy custom rule class."""
+
+ id = '100001'
diff --git a/test/custom_rules/example_inc/__init__.py b/test/custom_rules/example_inc/__init__.py
new file mode 100644
index 0000000..09a0f04
--- /dev/null
+++ b/test/custom_rules/example_inc/__init__.py
@@ -0,0 +1 @@
+"""Dummy test module."""
diff --git a/test/dependency-in-meta/bitbucket.yml b/test/dependency-in-meta/bitbucket.yml
new file mode 100644
index 0000000..b696809
--- /dev/null
+++ b/test/dependency-in-meta/bitbucket.yml
@@ -0,0 +1,10 @@
+---
+
+dependencies:
+ # from Bitbucket
+ - src: git+http://bitbucket.org/willthames/git-ansible-galaxy
+ version: v1.4
+
+ # from Bitbucket, alternative syntax and caveats
+ - src: http://bitbucket.org/willthames/hg-ansible-galaxy
+ scm: hg
diff --git a/test/dependency-in-meta/galaxy.yml b/test/dependency-in-meta/galaxy.yml
new file mode 100644
index 0000000..7f2c343
--- /dev/null
+++ b/test/dependency-in-meta/galaxy.yml
@@ -0,0 +1,5 @@
+---
+
+dependencies:
+ # from galaxy
+ - src: yatesr.timezone
diff --git a/test/dependency-in-meta/github.yml b/test/dependency-in-meta/github.yml
new file mode 100644
index 0000000..234c85a
--- /dev/null
+++ b/test/dependency-in-meta/github.yml
@@ -0,0 +1,10 @@
+---
+
+dependencies:
+ # from GitHub
+ - src: https://github.com/bennojoy/nginx
+
+ # from GitHub, overriding the name and specifying a specific tag
+ - src: https://github.com/bennojoy/nginx
+ version: master
+ name: nginx_role
diff --git a/test/dependency-in-meta/gitlab.yml b/test/dependency-in-meta/gitlab.yml
new file mode 100644
index 0000000..03b741e
--- /dev/null
+++ b/test/dependency-in-meta/gitlab.yml
@@ -0,0 +1,7 @@
+---
+
+dependencies:
+ # from GitLab or other git-based scm
+ - src: git@gitlab.company.com:mygroup/ansible-base.git
+ scm: git
+ version: "0.1" # quoted, so YAML doesn't parse this as a floating-point value
diff --git a/test/dependency-in-meta/webserver.yml b/test/dependency-in-meta/webserver.yml
new file mode 100644
index 0000000..2209ee2
--- /dev/null
+++ b/test/dependency-in-meta/webserver.yml
@@ -0,0 +1,6 @@
+---
+
+dependencies:
+ # from a webserver, where the role is packaged in a tar.gz
+ - src: https://some.webserver.example.com/files/master.tar.gz
+ name: http-role
diff --git a/test/directory with spaces/main.yml b/test/directory with spaces/main.yml
new file mode 100644
index 0000000..1969c6e
--- /dev/null
+++ b/test/directory with spaces/main.yml
@@ -0,0 +1 @@
+- debug: msg="tasks in directory with spaces included"
diff --git a/test/ematchtest.yml b/test/ematchtest.yml
new file mode 100644
index 0000000..333526c
--- /dev/null
+++ b/test/ematchtest.yml
@@ -0,0 +1,5 @@
+hello
+nothing
+exciting
+is
+happening
diff --git a/test/emptytags.yml b/test/emptytags.yml
new file mode 100644
index 0000000..26758dc
--- /dev/null
+++ b/test/emptytags.yml
@@ -0,0 +1,7 @@
+---
+- hosts: all
+
+ tasks:
+ - name: hello world
+ debug: msg="hello world"
+ tags:
diff --git a/test/fixtures/ansible-config-invalid.yml b/test/fixtures/ansible-config-invalid.yml
new file mode 100644
index 0000000..ca8c431
--- /dev/null
+++ b/test/fixtures/ansible-config-invalid.yml
@@ -0,0 +1,3 @@
+# invalid .ansible-lint config file
+- foo
+- bar
diff --git a/test/fixtures/ansible-config.yml b/test/fixtures/ansible-config.yml
new file mode 100644
index 0000000..4c94267
--- /dev/null
+++ b/test/fixtures/ansible-config.yml
@@ -0,0 +1,4 @@
+---
+verbosity: 1
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/config-with-relative-path.yml b/test/fixtures/config-with-relative-path.yml
new file mode 100644
index 0000000..51ac404
--- /dev/null
+++ b/test/fixtures/config-with-relative-path.yml
@@ -0,0 +1,5 @@
+---
+exclude_paths:
+- ../test-role/
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/exclude-paths-with-expands.yml b/test/fixtures/exclude-paths-with-expands.yml
new file mode 100644
index 0000000..20c742d
--- /dev/null
+++ b/test/fixtures/exclude-paths-with-expands.yml
@@ -0,0 +1,6 @@
+---
+exclude_paths:
+- ~/.ansible/roles
+- $HOME/.ansible/roles
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/exclude-paths.yml b/test/fixtures/exclude-paths.yml
new file mode 100644
index 0000000..a8bb938
--- /dev/null
+++ b/test/fixtures/exclude-paths.yml
@@ -0,0 +1,5 @@
+---
+exclude_paths:
+- ../
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/parseable.yml b/test/fixtures/parseable.yml
new file mode 100644
index 0000000..4603267
--- /dev/null
+++ b/test/fixtures/parseable.yml
@@ -0,0 +1,4 @@
+---
+parseable: true
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/quiet.yml b/test/fixtures/quiet.yml
new file mode 100644
index 0000000..583556f
--- /dev/null
+++ b/test/fixtures/quiet.yml
@@ -0,0 +1,4 @@
+---
+quiet: true
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/rulesdir-defaults.yml b/test/fixtures/rulesdir-defaults.yml
new file mode 100644
index 0000000..9eb30c6
--- /dev/null
+++ b/test/fixtures/rulesdir-defaults.yml
@@ -0,0 +1,6 @@
+---
+rulesdir:
+- ./rules
+use_default_rules: true
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/rulesdir.yml b/test/fixtures/rulesdir.yml
new file mode 100644
index 0000000..6ecd43d
--- /dev/null
+++ b/test/fixtures/rulesdir.yml
@@ -0,0 +1,5 @@
+---
+rulesdir:
+- ./rules
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/show-abspath.yml b/test/fixtures/show-abspath.yml
new file mode 100644
index 0000000..1945d2e
--- /dev/null
+++ b/test/fixtures/show-abspath.yml
@@ -0,0 +1,4 @@
+---
+display_relative_path: false
+
+# vim: et:sw=2:syntax=2:ts=2:
diff --git a/test/fixtures/show-relpath.yml b/test/fixtures/show-relpath.yml
new file mode 100644
index 0000000..7e7c3e6
--- /dev/null
+++ b/test/fixtures/show-relpath.yml
@@ -0,0 +1,4 @@
+---
+display_relative_path: true
+
+# vim: et:sw=2:syntax=2:ts=2:
diff --git a/test/fixtures/skip-tags.yml b/test/fixtures/skip-tags.yml
new file mode 100644
index 0000000..1f64b00
--- /dev/null
+++ b/test/fixtures/skip-tags.yml
@@ -0,0 +1,5 @@
+---
+skip_list:
+- "bad_tag"
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml
new file mode 100644
index 0000000..39f444a
--- /dev/null
+++ b/test/fixtures/tags.yml
@@ -0,0 +1,5 @@
+---
+tags:
+ - skip_ansible_lint
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/fixtures/unknown-type.yml b/test/fixtures/unknown-type.yml
new file mode 100644
index 0000000..54c6d2b
--- /dev/null
+++ b/test/fixtures/unknown-type.yml
@@ -0,0 +1,2 @@
+---
+some: map
diff --git a/test/fixtures/verbosity.yml b/test/fixtures/verbosity.yml
new file mode 100644
index 0000000..4c94267
--- /dev/null
+++ b/test/fixtures/verbosity.yml
@@ -0,0 +1,4 @@
+---
+verbosity: 1
+
+# vim: et:sw=2:syntax=yaml:ts=2:
diff --git a/test/foo.txt b/test/foo.txt
new file mode 100644
index 0000000..94f8f1a
--- /dev/null
+++ b/test/foo.txt
@@ -0,0 +1 @@
+Foo file
diff --git a/test/include-import-role.yml b/test/include-import-role.yml
new file mode 100644
index 0000000..7050ed1
--- /dev/null
+++ b/test/include-import-role.yml
@@ -0,0 +1,17 @@
+- hosts: all
+ vars:
+ var_is_set: no
+
+ tasks:
+ - import_role:
+ name: test-role
+
+- hosts: all
+ vars:
+ var_is_set: no
+
+ tasks:
+ - include_role:
+ name: test-role
+ tasks_from: world
+ when: "{{ var_is_set }}"
diff --git a/test/include-import-tasks-in-role.yml b/test/include-import-tasks-in-role.yml
new file mode 100644
index 0000000..313fc28
--- /dev/null
+++ b/test/include-import-tasks-in-role.yml
@@ -0,0 +1,3 @@
+- hosts: all
+ roles:
+ - role-with-included-imported-tasks
diff --git a/test/include-in-block-inner.yml b/test/include-in-block-inner.yml
new file mode 100644
index 0000000..be491a1
--- /dev/null
+++ b/test/include-in-block-inner.yml
@@ -0,0 +1,5 @@
+---
+
+- block:
+ - include: simpletask.yml
+ tags: ['foo']
diff --git a/test/include-in-block.yml b/test/include-in-block.yml
new file mode 100644
index 0000000..74f1f99
--- /dev/null
+++ b/test/include-in-block.yml
@@ -0,0 +1,5 @@
+---
+- hosts: all
+
+ tasks:
+ - include: include-in-block-inner.yml
diff --git a/test/included-handlers.yml b/test/included-handlers.yml
new file mode 100644
index 0000000..322b347
--- /dev/null
+++ b/test/included-handlers.yml
@@ -0,0 +1,6 @@
+---
+- name: restart xyz
+ service: name=xyz state=restarted
+# see Issue #165
+- name: command handler issue 165
+ command: do something
diff --git a/test/included-with-lint.yml b/test/included-with-lint.yml
new file mode 100644
index 0000000..d92af1a
--- /dev/null
+++ b/test/included-with-lint.yml
@@ -0,0 +1,4 @@
+# missing a task name
+- yum:
+ name: ansible
+ until: result|success
diff --git a/test/includedoesnotexist.yml b/test/includedoesnotexist.yml
new file mode 100644
index 0000000..b123339
--- /dev/null
+++ b/test/includedoesnotexist.yml
@@ -0,0 +1,3 @@
+---
+- pre_tasks:
+ - include: "doesnotexist.yml"
diff --git a/test/jinja2-when-failure.yml b/test/jinja2-when-failure.yml
new file mode 100644
index 0000000..aec7269
--- /dev/null
+++ b/test/jinja2-when-failure.yml
@@ -0,0 +1,10 @@
+- hosts: all
+ tasks:
+ - name: test when with jinja2
+ debug: msg=text
+ when: "{{ false }}"
+
+- hosts: all
+ roles:
+ - role: test
+ when: "{{ '1' = '1' }}"
diff --git a/test/jinja2-when-success.yml b/test/jinja2-when-success.yml
new file mode 100644
index 0000000..20d3db9
--- /dev/null
+++ b/test/jinja2-when-success.yml
@@ -0,0 +1,8 @@
+- hosts: all
+ tasks:
+ - name: test when
+ debug: msg=text
+ when: true
+ - name: test when 2
+ debug: msg=text2
+ when: 1 = 1
diff --git a/test/local-content/README.md b/test/local-content/README.md
new file mode 100644
index 0000000..2b6322a
--- /dev/null
+++ b/test/local-content/README.md
@@ -0,0 +1,6 @@
+The reason that every roles test gets its own directory is that while they
+use the same three roles, the way the tests work makes sure that when the
+second one runs, the roles and their local plugins from the first test are
+still known to Ansible. For that reason, their names reflect the directory
+they are in to make sure that tests don't use modules/plugins found by
+other tests.
diff --git a/test/local-content/collections/ansible_collections/testns/testcoll/galaxy.yml b/test/local-content/collections/ansible_collections/testns/testcoll/galaxy.yml
new file mode 100644
index 0000000..43dd2e9
--- /dev/null
+++ b/test/local-content/collections/ansible_collections/testns/testcoll/galaxy.yml
@@ -0,0 +1,3 @@
+namespace: testns
+name: testcoll
+version: 0.1.0
diff --git a/test/local-content/collections/ansible_collections/testns/testcoll/plugins/filter/test_filter.py b/test/local-content/collections/ansible_collections/testns/testcoll/plugins/filter/test_filter.py
new file mode 100644
index 0000000..ac9e854
--- /dev/null
+++ b/test/local-content/collections/ansible_collections/testns/testcoll/plugins/filter/test_filter.py
@@ -0,0 +1,16 @@
+"""A filter plugin."""
+
+
+def a_test_filter(a, b):
+ """Return a string containing both a and b."""
+ return '{0}:{1}'.format(a, b)
+
+
+class FilterModule(object):
+ """Filter plugin."""
+
+ def filters(self):
+ """Return filters."""
+ return {
+ 'test_filter': a_test_filter
+ }
diff --git a/test/local-content/collections/ansible_collections/testns/testcoll/plugins/modules/test_module_2.py b/test/local-content/collections/ansible_collections/testns/testcoll/plugins/modules/test_module_2.py
new file mode 100644
index 0000000..cae1a26
--- /dev/null
+++ b/test/local-content/collections/ansible_collections/testns/testcoll/plugins/modules/test_module_2.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 2!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-collection.yml b/test/local-content/test-collection.yml
new file mode 100644
index 0000000..bc3ed1b
--- /dev/null
+++ b/test/local-content/test-collection.yml
@@ -0,0 +1,10 @@
+---
+- name: Use module and filter plugin from local collection
+ hosts: localhost
+ tasks:
+ - name: Use module from local collection
+ testns.testcoll.test_module_2:
+ - name: Use filter from local collection
+ assert:
+ that:
+ - 1 | testns.testcoll.test_filter(2) == '1:2'
diff --git a/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py
new file mode 100644
index 0000000..1c63fdd
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role1/library/test_module_1_failed_complete.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 1!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml
new file mode 100644
index 0000000..680dcab
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role1/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 1
+ test_module_1_failed_complete:
diff --git a/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml
new file mode 100644
index 0000000..8646f6b
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role2/tasks/main.yml
@@ -0,0 +1,11 @@
+---
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_1_failed_complete:
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_3_failed_complete:
+- name: Use local test plugin
+ assert:
+ that:
+ - "'2' is b_test_failed_complete '12345'"
diff --git a/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py
new file mode 100644
index 0000000..abc1049
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role2/test_plugins/b_failed_complete.py
@@ -0,0 +1,16 @@
+"""A test plugin."""
+
+
+def compatibility_in_test(a, b):
+ """Return True when a is contained in b."""
+ return a in b
+
+
+class TestModule:
+ """Test plugin."""
+
+ def tests(self):
+ """Return tests."""
+ return {
+ 'b_test_failed_complete': compatibility_in_test,
+ }
diff --git a/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py b/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py
new file mode 100644
index 0000000..c7296be
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role3/library/test_module_3_failed_complete.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 3!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml b/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml
new file mode 100644
index 0000000..7a36734
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/roles/role3/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 3
+ test_module_3_failed_complete:
diff --git a/test/local-content/test-roles-failed-complete/test.yml b/test/local-content/test-roles-failed-complete/test.yml
new file mode 100644
index 0000000..1160bb5
--- /dev/null
+++ b/test/local-content/test-roles-failed-complete/test.yml
@@ -0,0 +1,5 @@
+---
+- name: Include role which expects module that is local to other role which is not loaded
+ hosts: localhost
+ roles:
+ - role2
diff --git a/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py b/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py
new file mode 100644
index 0000000..1c63fdd
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role1/library/test_module_1_failed.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 1!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-failed/roles/role1/tasks/main.yml b/test/local-content/test-roles-failed/roles/role1/tasks/main.yml
new file mode 100644
index 0000000..257493a
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role1/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 1
+ test_module_1_failed:
diff --git a/test/local-content/test-roles-failed/roles/role2/tasks/main.yml b/test/local-content/test-roles-failed/roles/role2/tasks/main.yml
new file mode 100644
index 0000000..48daca6
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role2/tasks/main.yml
@@ -0,0 +1,11 @@
+---
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_1_failed:
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_3_failed:
+- name: Use local test plugin
+ assert:
+ that:
+ - "'2' is b_test_failed '12345'"
diff --git a/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py b/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py
new file mode 100644
index 0000000..09a02a3
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role2/test_plugins/b_failed.py
@@ -0,0 +1,16 @@
+"""A test plugin."""
+
+
+def compatibility_in_test(a, b):
+ """Return True when a is contained in b."""
+ return a in b
+
+
+class TestModule:
+ """Test plugin."""
+
+ def tests(self):
+ """Return tests."""
+ return {
+ 'b_test_failed': compatibility_in_test,
+ }
diff --git a/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py b/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py
new file mode 100644
index 0000000..c7296be
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role3/library/test_module_3_failed.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 3!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-failed/roles/role3/tasks/main.yml b/test/local-content/test-roles-failed/roles/role3/tasks/main.yml
new file mode 100644
index 0000000..ad17eb0
--- /dev/null
+++ b/test/local-content/test-roles-failed/roles/role3/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 3
+ test_module_3_failed:
diff --git a/test/local-content/test-roles-failed/test.yml b/test/local-content/test-roles-failed/test.yml
new file mode 100644
index 0000000..08ff0f6
--- /dev/null
+++ b/test/local-content/test-roles-failed/test.yml
@@ -0,0 +1,7 @@
+---
+- name: Use roles with local module in wrong order, so that Ansible fails
+ hosts: localhost
+ roles:
+ - role2
+ - role3
+ - role1
diff --git a/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py b/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py
new file mode 100644
index 0000000..1c63fdd
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role1/library/test_module_1_success.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 1!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-success/roles/role1/tasks/main.yml b/test/local-content/test-roles-success/roles/role1/tasks/main.yml
new file mode 100644
index 0000000..ba920af
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role1/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 1
+ test_module_1_success:
diff --git a/test/local-content/test-roles-success/roles/role2/tasks/main.yml b/test/local-content/test-roles-success/roles/role2/tasks/main.yml
new file mode 100644
index 0000000..a540cf1
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role2/tasks/main.yml
@@ -0,0 +1,11 @@
+---
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_1_success:
+- name: Use local module from other role that has been included before this one
+ # If it has not been included before, loading this role fails!
+ test_module_3_success:
+- name: Use local test plugin
+ assert:
+ that:
+ - "'2' is b_test_success '12345'"
diff --git a/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py b/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py
new file mode 100644
index 0000000..bcef377
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role2/test_plugins/b_success.py
@@ -0,0 +1,16 @@
+"""A test plugin."""
+
+
+def compatibility_in_test(a, b):
+ """Return True when a is contained in b."""
+ return a in b
+
+
+class TestModule:
+ """Test plugin."""
+
+ def tests(self):
+ """Return tests."""
+ return {
+ 'b_test_success': compatibility_in_test,
+ }
diff --git a/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py b/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py
new file mode 100644
index 0000000..c7296be
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role3/library/test_module_3_success.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+"""A module."""
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+def main() -> None:
+ """Execute module."""
+ module = AnsibleModule(dict())
+ module.exit_json(msg="Hello 3!")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/local-content/test-roles-success/roles/role3/tasks/main.yml b/test/local-content/test-roles-success/roles/role3/tasks/main.yml
new file mode 100644
index 0000000..c77a7c8
--- /dev/null
+++ b/test/local-content/test-roles-success/roles/role3/tasks/main.yml
@@ -0,0 +1,3 @@
+---
+- name: Use local module 3
+ test_module_3_success:
diff --git a/test/local-content/test-roles-success/test.yml b/test/local-content/test-roles-success/test.yml
new file mode 100644
index 0000000..df17c7d
--- /dev/null
+++ b/test/local-content/test-roles-success/test.yml
@@ -0,0 +1,7 @@
+---
+- name: Use roles with local modules and test plugins
+ hosts: localhost
+ roles:
+ - role1
+ - role3
+ - role2
diff --git a/test/multiline-brackets-do-not-match-test.yml b/test/multiline-brackets-do-not-match-test.yml
new file mode 100644
index 0000000..dfa5ff8
--- /dev/null
+++ b/test/multiline-brackets-do-not-match-test.yml
@@ -0,0 +1,22 @@
+---
+- hosts: foo
+ roles:
+ - ../../../roles/base_os
+ - ../../../roles/repos
+ - {
+ role: ../../../roles/openshift_master,
+ oo_minion_ips: "{ hostvars['localhost'].oo_minion_ips | default(['']) }}",
+ oo_bind_ip: "{{ hostvars[inventory_hostname].ansible_eth0.ipv4.address | default(['']) }}"
+ }
+ - ../../../roles/pods
+
+- name: "Set Origin specific facts on localhost (for later use)"
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Setting oo_minion_ips fact on localhost
+ set_fact:
+ oo_minion_ips: "{{ hostvars
+ | oo_select_keys(groups['tag_env-host-type-' + oo_env + '-openshift-minion'])
+ | oo_collect(attribute='ansible_eth0.ipv4.address') }"
+ when: groups['tag_env-host-type-' + oo_env + '-openshift-minion'] is defined
diff --git a/test/multiline-bracketsmatchtest.yml b/test/multiline-bracketsmatchtest.yml
new file mode 100644
index 0000000..703f225
--- /dev/null
+++ b/test/multiline-bracketsmatchtest.yml
@@ -0,0 +1,22 @@
+---
+- hosts: foo
+ roles:
+ - ../../../roles/base_os
+ - ../../../roles/repos
+ - {
+ role: ../../../roles/openshift_master,
+ oo_minion_ips: "{{ hostvars['localhost'].oo_minion_ips | default(['']) }}",
+ oo_bind_ip: "{{ hostvars[inventory_hostname].ansible_eth0.ipv4.address | default(['']) }}"
+ }
+ - ../../../roles/pods
+
+- name: "Set Origin specific facts on localhost (for later use)"
+ hosts: localhost
+ gather_facts: no
+ tasks:
+ - name: Setting oo_minion_ips fact on localhost
+ set_fact:
+ oo_minion_ips: "{{ hostvars
+ | oo_select_keys(groups['tag_env-host-type-' + oo_env + '-openshift-minion'])
+ | oo_collect(attribute='ansible_eth0.ipv4.address') }}"
+ when: groups['tag_env-host-type-' + oo_env + '-openshift-minion'] is defined
diff --git a/test/nestedincludes.yml b/test/nestedincludes.yml
new file mode 100644
index 0000000..5985b54
--- /dev/null
+++ b/test/nestedincludes.yml
@@ -0,0 +1,2 @@
+---
+- include: simpletask.yml tags=nginx
diff --git a/test/nomatchestest.yml b/test/nomatchestest.yml
new file mode 100644
index 0000000..e48ef92
--- /dev/null
+++ b/test/nomatchestest.yml
@@ -0,0 +1,9 @@
+---
+- hosts: whatever
+
+ tasks:
+ - name: hello world
+ action: debug msg="Hello!"
+
+ - name: this should be fine too
+ action: file state=touch dest=./wherever mode=0600
diff --git a/test/norole.yml b/test/norole.yml
new file mode 100644
index 0000000..3a18e94
--- /dev/null
+++ b/test/norole.yml
@@ -0,0 +1,5 @@
+---
+- hosts:
+ - localhost
+ roles:
+ - name: node
diff --git a/test/norole2.yml b/test/norole2.yml
new file mode 100644
index 0000000..2ee7a83
--- /dev/null
+++ b/test/norole2.yml
@@ -0,0 +1,5 @@
+---
+- hosts:
+ - localhost
+ roles:
+ - name: node
diff --git a/test/package-check-failure.yml b/test/package-check-failure.yml
new file mode 100644
index 0000000..8c25f3c
--- /dev/null
+++ b/test/package-check-failure.yml
@@ -0,0 +1,14 @@
+- hosts: localhost
+ tasks:
+ - name: install ansible
+ yum: name=ansible state=latest
+
+ - name: install ansible-lint
+ pip: name=ansible-lint
+ args:
+ state: latest
+
+ - name: install some-package
+ package:
+ name: some-package
+ state: latest
diff --git a/test/package-check-success.yml b/test/package-check-success.yml
new file mode 100644
index 0000000..d649dc7
--- /dev/null
+++ b/test/package-check-success.yml
@@ -0,0 +1,15 @@
+- hosts: localhost
+ tasks:
+ - name: install ansible
+ yum: name=ansible-2.1.0.0 state=present
+
+ - name: install ansible-lint
+ pip: name=ansible-lint
+ args:
+ state: present
+ version: 3.1.2
+
+ - name: install some-package
+ package:
+ name: some-package
+ state: present
diff --git a/test/playbook-import/playbook_imported.yml b/test/playbook-import/playbook_imported.yml
new file mode 100644
index 0000000..4dc646a
--- /dev/null
+++ b/test/playbook-import/playbook_imported.yml
@@ -0,0 +1,9 @@
+---
+- hosts: localhost
+ connection: local
+ gather_facts: no
+ tasks:
+ - command: echo "no name" # should generate 502
+ - name: Another task
+ debug:
+ msg: debug message
diff --git a/test/playbook-import/playbook_parent.yml b/test/playbook-import/playbook_parent.yml
new file mode 100644
index 0000000..7e8b524
--- /dev/null
+++ b/test/playbook-import/playbook_parent.yml
@@ -0,0 +1,3 @@
+---
+- name: Importing another playbook
+ import_playbook: playbook_imported.yml
diff --git a/test/role-with-handler/a-role/handlers/main.yml b/test/role-with-handler/a-role/handlers/main.yml
new file mode 100644
index 0000000..59ae800
--- /dev/null
+++ b/test/role-with-handler/a-role/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+- name: do anything
+ shell: echo merp | cat
+ when:
+ - something.changed
diff --git a/test/role-with-handler/main.yml b/test/role-with-handler/main.yml
new file mode 100644
index 0000000..9b37597
--- /dev/null
+++ b/test/role-with-handler/main.yml
@@ -0,0 +1,4 @@
+- hosts: localhost
+ name: foo
+ roles:
+ - { role: a-role }
diff --git a/test/role-with-included-imported-tasks/tasks/imported_tasks.yml b/test/role-with-included-imported-tasks/tasks/imported_tasks.yml
new file mode 100644
index 0000000..32a2b23
--- /dev/null
+++ b/test/role-with-included-imported-tasks/tasks/imported_tasks.yml
@@ -0,0 +1,2 @@
+- name: This is a task that should be imported
+ ping:
diff --git a/test/role-with-included-imported-tasks/tasks/included_tasks.yml b/test/role-with-included-imported-tasks/tasks/included_tasks.yml
new file mode 100644
index 0000000..37b59d2
--- /dev/null
+++ b/test/role-with-included-imported-tasks/tasks/included_tasks.yml
@@ -0,0 +1,2 @@
+- name: This is a task that should be included
+ ping:
diff --git a/test/role-with-included-imported-tasks/tasks/main.yml b/test/role-with-included-imported-tasks/tasks/main.yml
new file mode 100644
index 0000000..81de7cb
--- /dev/null
+++ b/test/role-with-included-imported-tasks/tasks/main.yml
@@ -0,0 +1,6 @@
+- include_tasks: included_tasks.yml
+- import_tasks: imported_tasks.yml
+- include_tasks:
+ file: included_tasks.yml
+ apply:
+ tags: sometag
diff --git a/test/roles/ansible-role-foo/tasks/main.yaml b/test/roles/ansible-role-foo/tasks/main.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/roles/ansible-role-foo/tasks/main.yaml
diff --git a/test/roles/invalid-name/tasks/main.yaml b/test/roles/invalid-name/tasks/main.yaml
new file mode 100644
index 0000000..1270837
--- /dev/null
+++ b/test/roles/invalid-name/tasks/main.yaml
@@ -0,0 +1,4 @@
+---
+- name: foo
+ debug:
+ msg: foo
diff --git a/test/roles/invalid_due_to_meta/meta/main.yml b/test/roles/invalid_due_to_meta/meta/main.yml
new file mode 100644
index 0000000..0d4b0b2
--- /dev/null
+++ b/test/roles/invalid_due_to_meta/meta/main.yml
@@ -0,0 +1,8 @@
+galaxy_info:
+ role_name: invalid-due-to-meta
+ author: foo
+ description: foo
+ license: MIT
+ platforms:
+ - name: foo
+ min_ansible_version: 2.7
diff --git a/test/roles/invalid_due_to_meta/tasks/main.yaml b/test/roles/invalid_due_to_meta/tasks/main.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/roles/invalid_due_to_meta/tasks/main.yaml
diff --git a/test/roles/test-role/molecule/default/include-import-role.yml b/test/roles/test-role/molecule/default/include-import-role.yml
new file mode 100644
index 0000000..0e1d166
--- /dev/null
+++ b/test/roles/test-role/molecule/default/include-import-role.yml
@@ -0,0 +1,6 @@
+---
+- name: test
+ gather_facts: no
+ hosts: all
+ roles:
+ - role: test-role
diff --git a/test/roles/test-role/tasks/main.yml b/test/roles/test-role/tasks/main.yml
new file mode 100644
index 0000000..53b968b
--- /dev/null
+++ b/test/roles/test-role/tasks/main.yml
@@ -0,0 +1,2 @@
+- name: shell instead of command
+ shell: echo hello world
diff --git a/test/roles/valid-due-to-meta/meta/main.yml b/test/roles/valid-due-to-meta/meta/main.yml
new file mode 100644
index 0000000..8b8566b
--- /dev/null
+++ b/test/roles/valid-due-to-meta/meta/main.yml
@@ -0,0 +1,8 @@
+galaxy_info:
+ role_name: valid_due_to_meta
+ author: foo
+ description: foo
+ license: MIT
+ platforms:
+ - name: foo
+ min_ansible_version: 2.7
diff --git a/test/roles/valid-due-to-meta/tasks/debian/main.yml b/test/roles/valid-due-to-meta/tasks/debian/main.yml
new file mode 100644
index 0000000..6fa48c2
--- /dev/null
+++ b/test/roles/valid-due-to-meta/tasks/debian/main.yml
@@ -0,0 +1,2 @@
+# This empty task file is here to test that roles with tasks organized in subdirectories
+# are handled correctly by ansible-lint.
diff --git a/test/roles/valid-due-to-meta/tasks/main.yaml b/test/roles/valid-due-to-meta/tasks/main.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/roles/valid-due-to-meta/tasks/main.yaml
diff --git a/test/rules/EMatcherRule.py b/test/rules/EMatcherRule.py
new file mode 100644
index 0000000..ed65883
--- /dev/null
+++ b/test/rules/EMatcherRule.py
@@ -0,0 +1,12 @@
+from ansiblelint.rules import AnsibleLintRule
+
+
+class EMatcherRule(AnsibleLintRule):
+ id = 'TEST0001'
+ description = 'This is a test rule that looks for lines ' + \
+ 'containing the letter e'
+ shortdesc = 'The letter "e" is present'
+ tags = ['fake', 'dummy', 'test1']
+
+ def match(self, filename, line):
+ return "e" in line
diff --git a/test/rules/UnsetVariableMatcherRule.py b/test/rules/UnsetVariableMatcherRule.py
new file mode 100644
index 0000000..b4c1756
--- /dev/null
+++ b/test/rules/UnsetVariableMatcherRule.py
@@ -0,0 +1,12 @@
+from ansiblelint.rules import AnsibleLintRule
+
+
+class UnsetVariableMatcherRule(AnsibleLintRule):
+ id = 'TEST0002'
+ shortdesc = 'Line contains untemplated variable'
+ description = 'This is a test rule that looks for lines ' + \
+ 'post templating that still contain {{'
+ tags = ['fake', 'dummy', 'test2']
+
+ def match(self, filename, line):
+ return "{{" in line
diff --git a/test/rules/__init__.py b/test/rules/__init__.py
new file mode 100644
index 0000000..4a6e23f
--- /dev/null
+++ b/test/rules/__init__.py
@@ -0,0 +1,3 @@
+"""Test rules resources."""
+
+__all__ = ['UnsetVariableMatcherRule', 'EMatcherRule']
diff --git a/test/simpletask.yml b/test/simpletask.yml
new file mode 100644
index 0000000..0aba042
--- /dev/null
+++ b/test/simpletask.yml
@@ -0,0 +1,3 @@
+---
+- name: hello world
+ debug: msg="Hello!"
diff --git a/test/skiptasks.yml b/test/skiptasks.yml
new file mode 100644
index 0000000..c6a130b
--- /dev/null
+++ b/test/skiptasks.yml
@@ -0,0 +1,70 @@
+---
+- hosts: all
+
+ tasks:
+
+ - name: test 401
+ action: git
+
+ - name: test 402
+ action: hg
+
+ - name: test 303
+ command: git log
+ changed_when: False
+
+ - name: test 302
+ command: creates=B chmod 644 A
+
+ - name: test invalid action (skip)
+ foo: bar
+ tags:
+ - skip_ansible_lint
+
+ - name: test 401 (skip)
+ action: git
+ tags:
+ - skip_ansible_lint
+
+ - name: test 402 (skip)
+ action: hg
+ tags:
+ - skip_ansible_lint
+
+ - name: test 303 (skip)
+ command: git log
+ tags:
+ - skip_ansible_lint
+
+ - name: test 302 (skip)
+ command: chmod 644 A
+ tags:
+ - skip_ansible_lint
+
+ - name: test 401 (don't warn)
+ command: git log
+ args:
+ warn: False
+ changed_when: False
+
+ - name: test 402 (don't warn)
+ command: chmod 644 A
+ args:
+ warn: False
+ creates: B
+
+ - name: test 402 (warn)
+ command: chmod 644 A
+ args:
+ warn: yes
+ creates: B
+
+ - name: test 401 (don't warn single line)
+ command: warn=False chdir=/tmp/blah git log
+ changed_when: False
+
+ - name: test 402 (don't warn single line)
+ command: warn=no creates=B chmod 644 A
+
+ - name: test 402 (warn single line)
+ command: warn=yes creates=B chmod 644 A
diff --git a/test/task-has-name-failure.yml b/test/task-has-name-failure.yml
new file mode 100644
index 0000000..ce947f3
--- /dev/null
+++ b/test/task-has-name-failure.yml
@@ -0,0 +1,7 @@
+---
+
+- hosts: all
+ tasks:
+ - command: echo "no name"
+ - name:
+ command: echo "empty name"
diff --git a/test/task-has-name-success.yml b/test/task-has-name-success.yml
new file mode 100644
index 0000000..b708f5a
--- /dev/null
+++ b/test/task-has-name-success.yml
@@ -0,0 +1,9 @@
+---
+
+- hosts: all
+ tasks:
+ - name: This task has a name
+ command: echo "Hello World"
+ - debug:
+ msg: "Hello World"
+ - meta: flush_handlers
diff --git a/test/taskimports.yml b/test/taskimports.yml
new file mode 100644
index 0000000..f21b678
--- /dev/null
+++ b/test/taskimports.yml
@@ -0,0 +1,9 @@
+---
+- 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"
diff --git a/test/taskincludes.yml b/test/taskincludes.yml
new file mode 100644
index 0000000..cba7909
--- /dev/null
+++ b/test/taskincludes.yml
@@ -0,0 +1,9 @@
+---
+- hosts: webservers
+ vars:
+ varset: varset
+ tasks:
+ - include: nestedincludes.yml tags=nested
+ - include: "{{ varnotset }}.yml"
+ - include: "{{ varset }}.yml"
+ - include: "directory with spaces/main.yml"
diff --git a/test/taskincludes_2_4_style.yml b/test/taskincludes_2_4_style.yml
new file mode 100644
index 0000000..f1ae9f4
--- /dev/null
+++ b/test/taskincludes_2_4_style.yml
@@ -0,0 +1,9 @@
+---
+- 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"
diff --git a/test/test-role/tasks/main.yml b/test/test-role/tasks/main.yml
new file mode 100644
index 0000000..53b968b
--- /dev/null
+++ b/test/test-role/tasks/main.yml
@@ -0,0 +1,2 @@
+- name: shell instead of command
+ shell: echo hello world
diff --git a/test/test-role/tasks/world.yml b/test/test-role/tasks/world.yml
new file mode 100644
index 0000000..69ae661
--- /dev/null
+++ b/test/test-role/tasks/world.yml
@@ -0,0 +1 @@
+- command: echo this is a task without a name
diff --git a/test/test/always-run-success.yml b/test/test/always-run-success.yml
new file mode 100644
index 0000000..468a17c
--- /dev/null
+++ b/test/test/always-run-success.yml
@@ -0,0 +1 @@
+- hosts: localhost
diff --git a/test/testproject/roles/test-role/tasks/main.yml b/test/testproject/roles/test-role/tasks/main.yml
new file mode 100644
index 0000000..53b968b
--- /dev/null
+++ b/test/testproject/roles/test-role/tasks/main.yml
@@ -0,0 +1,2 @@
+- name: shell instead of command
+ shell: echo hello world
diff --git a/test/unicode.yml b/test/unicode.yml
new file mode 100644
index 0000000..c9204e4
--- /dev/null
+++ b/test/unicode.yml
@@ -0,0 +1,9 @@
+---
+- hosts: localhost
+ connection: local
+ vars:
+ unicode_var: a_b_cö
+
+ tasks:
+ - name: bonjour, ça va?
+ file: state=touch dest=/tmp/naïve.yml mode=0600
diff --git a/test/using-bare-variables-failure.yml b/test/using-bare-variables-failure.yml
new file mode 100644
index 0000000..865381b
--- /dev/null
+++ b/test/using-bare-variables-failure.yml
@@ -0,0 +1,108 @@
+---
+- hosts: localhost
+ become: no
+ vars:
+ my_list:
+ - foo
+ - bar
+
+ my_list2:
+ - 1
+ - 2
+
+ my_list_of_dicts:
+ - foo: 1
+ bar: 2
+ - foo: 3
+ bar: 4
+
+ my_list_of_lists:
+ - "{{ my_list }}"
+ - "{{ my_list2 }}"
+
+ my_filenames:
+ - foo.txt
+ - bar.txt
+
+ my_dict:
+ foo: bar
+
+ tasks:
+ - name: with_items loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_items: my_list
+
+ - name: with_dict loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_dict: my_dict
+
+ ### Testing with_dict with a default empty dictionary
+ - name: with_dict loop using variable and default
+ debug:
+ msg: "{{ item.key }} - {{ item.value }}"
+ with_dict: uwsgi_ini | default({})
+
+ - name: with_nested loop using bare variable
+ debug:
+ msg: "{{ item.0 }} {{ item.1 }}"
+ with_nested:
+ - my_list
+ - "{{ my_list2 }}"
+
+ - name: with_file loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_file: my_list
+
+ - name: with_fileglob loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_fileglob: my_list
+
+ - name: with_filetree loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_filetree: my_list
+
+ - name: with_together loop using bare variable
+ debug:
+ msg: "{{ item.0 }} {{ item.1 }}"
+ with_together:
+ - my_list
+ - "{{ my_list2 }}"
+
+ - name: with_subelements loop using bare variable
+ debug:
+ msg: "{{ item.0 }}"
+ with_subelements:
+ - my_list_of_dicts
+ - bar
+
+ - name: with_random_choice loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_random_choice: my_list
+
+ - name: with_first_found loop using bare variable
+ debug:
+ msg: "{{ item }}"
+ with_first_found: my_filenames
+
+ - name: with_indexed_items loop
+ debug:
+ msg: "{{ item.0 }} {{ item.1 }}"
+ with_indexed_items: my_list
+
+ - name: with_flattened loop
+ debug:
+ msg: "{{ item }}"
+ with_flattened:
+ - my_list
+ - my_list2
+
+ - name: with_flattened loop with a variable
+ debug:
+ msg: "{{ item }}"
+ with_flattened: my_list_of_lists
diff --git a/test/using-bare-variables-success.yml b/test/using-bare-variables-success.yml
new file mode 100644
index 0000000..c8f0f3f
--- /dev/null
+++ b/test/using-bare-variables-success.yml
@@ -0,0 +1,200 @@
+---
+- hosts: localhost
+ become: no
+ vars:
+ my_list:
+ - foo
+ - bar
+
+ my_list2:
+ - 1
+ - 2
+
+ my_list_of_dicts:
+ - foo: 1
+ bar: 2
+ - foo: 3
+ bar: 4
+
+ my_list_of_lists:
+ - "{{ my_list }}"
+ - "{{ my_list2 }}"
+
+ my_filenames:
+ - foo.txt
+ - bar.txt
+
+ my_dict:
+ foo: bar
+
+ tasks:
+ ### Testing with_items loops
+ - name: with_items loop using static list
+ debug:
+ msg: "{{ item }}"
+ with_items:
+ - foo
+ - bar
+
+ - name: with_items using a static hash
+ debug:
+ msg: "{{ item.key }} - {{ item.value }}"
+ with_items:
+ - { key: foo, value: 1 }
+ - { key: bar, value: 2 }
+
+ - name: with_items loop using variable
+ debug:
+ msg: "{{ item }}"
+ with_items: "{{ my_list }}"
+
+ ### Testing with_nested loops
+ - name: with_nested loop using static lists
+ debug:
+ msg: "{{ item[0] }} - {{ item[1] }}"
+ with_nested:
+ - [ 'foo', 'bar' ]
+ - [ '1', '2', '3' ]
+
+ - name: with_nested loop using variable list and static
+ debug:
+ msg: "{{ item[0] }} - {{ item[1] }}"
+ with_nested:
+ - "{{ my_list }}"
+ - [ '1', '2', '3' ]
+
+ ### Testing with_dict
+ - name: with_dict loop using variable
+ debug:
+ msg: "{{ item.key }} - {{ item.value }}"
+ with_dict: "{{ my_dict }}"
+
+ ### Testing with_dict with a default empty dictionary
+ - name: with_dict loop using variable and default
+ debug:
+ msg: "{{ item.key }} - {{ item.value }}"
+ with_dict: "{{ uwsgi_ini | default({}) }}"
+
+ ### Testing with_file
+ - name: with_file loop using static files list
+ debug:
+ msg: "{{ item }}"
+ with_file:
+ - foo.txt
+ - bar.txt
+
+ - name: with_file loop using list of filenames
+ debug:
+ msg: "{{ item }}"
+ with_file: "{{ my_filenames }}"
+
+ ### Testing with_fileglob
+ - name: with_fileglob loop using list of *.txt
+ debug:
+ msg: "{{ item }}"
+ with_fileglob:
+ - '*.txt'
+
+ ### Testing non-list form of with_fileglob
+ - name: with_fileglob loop using single value *.txt
+ debug:
+ msg: "{{ item }}"
+ with_fileglob: '*.txt'
+
+ ### Testing non-list form of with_fileglob with trailing templated pattern
+ - name: with_fileglob loop using templated pattern
+ debug:
+ msg: "{{ item }}"
+ with_fileglob: 'foo{{glob}}'
+
+ ### Testing with_filetree
+ - name: with_filetree loop using list of path
+ debug:
+ msg: "{{ item }}"
+ with_filetree:
+ - path/to/dir1/
+ - path/to/dir2/
+
+ ### Testing non-list form of with_filetree
+ - name: with_filetree loop using single path
+ debug:
+ msg: "{{ item }}"
+ with_filetree: path/to/dir/
+
+ ### Testing non-list form of with_filetree with trailing templated pattern
+ - name: with_filetree loop using templated pattern
+ debug:
+ msg: "{{ item }}"
+ with_filetree: 'path/to/{{ directory }}'
+
+ ### Testing with_together
+ - name: with_together loop using variable lists
+ debug:
+ msg: "{{ item.0 }} - {{ item.1 }}"
+ with_together:
+ - "{{ my_list }}"
+ - "{{ my_list2 }}"
+
+ - name: with_subelements loop
+ debug:
+ msg: "{{ item }}"
+ with_subelements:
+ - "{{ my_list_of_dicts }}"
+ - bar
+
+ - name: with_sequence loop
+ debug:
+ msg: "{{ item }}"
+ with_sequence: count=2
+
+ - name: with_random_choice loop
+ debug:
+ msg: "{{ item }}"
+ with_random_choice: "{{ my_list }}"
+
+ - name: with_first_found loop with static files list
+ debug:
+ msg: "{{ item }}"
+ with_first_found:
+ - foo.txt
+ - bar.txt
+
+ - name: with_first_found loop with list of filenames
+ debug:
+ msg: "{{ item }}"
+ with_first_found: "{{ my_filenames }}"
+
+ - name: with_indexed_items loop
+ debug:
+ msg: "{{ item.0 }} {{ item.1 }}"
+ with_indexed_items: "{{ my_list }}"
+
+ - name: with_ini loop
+ debug:
+ msg: "{{ item }}"
+ with_ini: value[1-2] section=section1 file=foo.ini re=true
+
+ - name: with_flattened loop
+ debug:
+ msg: "{{ item }}"
+ with_flattened:
+ - "{{ my_list }}"
+ - "{{ my_list2 }}"
+
+ - name: with_flattened loop with a variable
+ debug:
+ msg: "{{ item }}"
+ with_flattened: "{{ my_list_of_lists }}"
+
+ - name: with_flattened loop with a multiline template
+ debug:
+ msg: "{{ item }}"
+ with_flattened: >
+ {{ my_list
+ | union(my_list2)
+ | list }}
+
+ - name: with_inventory_hostnames loop
+ debug:
+ msg: "{{ item }}"
+ with_inventory_hostnames: all
diff --git a/test/varset.yml b/test/varset.yml
new file mode 100644
index 0000000..fa28db9
--- /dev/null
+++ b/test/varset.yml
@@ -0,0 +1,3 @@
+- debug: msg="var was set"
+
+- git: repo=hello.git
diff --git a/test/varunset.yml b/test/varunset.yml
new file mode 100644
index 0000000..9e8a891
--- /dev/null
+++ b/test/varunset.yml
@@ -0,0 +1 @@
+- debug: msg="var was not set"
diff --git a/test/with-skip-tag-id.yml b/test/with-skip-tag-id.yml
new file mode 100644
index 0000000..12d2fb7
--- /dev/null
+++ b/test/with-skip-tag-id.yml
@@ -0,0 +1,6 @@
+- hosts: all
+ tasks:
+ - name: trailing whitespace on this line
+ git:
+ repo: '{{ archive_services_repo_url }}'
+ dest: '/home/www'
diff --git a/tools/test-setup.sh b/tools/test-setup.sh
new file mode 100755
index 0000000..170cd99
--- /dev/null
+++ b/tools/test-setup.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+set -euxo pipefail
+# Used by Zuul CI to perform extra bootstrapping
+
+# sudo used only because currently zuul default tox_executable=tox instead of
+# "python3 -m tox"
+# https://zuul-ci.org/docs/zuul-jobs/python-roles.html#rolevar-ensure-tox.tox_executable
+
+# Install pip if not already install on the system
+python3 -m pip --version || {
+ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
+ sudo python3 get-pip.py
+}
+
+# Workaround until ensure-tox will allow upgrades
+# https://review.opendev.org/#/c/690057/
+sudo python3 -m pip install -U tox
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..9599055
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,134 @@
+[tox]
+minversion = 3.16.1
+envlist = lint,py{38,37,36}-ansible{29,28,210,devel}
+isolated_build = true
+requires =
+ setuptools >= 41.4.0
+ pip >= 19.3.0
+skip_missing_interpreters = True
+# `usedevelop = true` overrides `skip_install` instruction, it's unwanted
+usedevelop = false
+
+[testenv]
+description =
+ Run the tests with pytest under {basepython}
+deps =
+ ansible28: ansible>=2.8,<2.9
+ ansible29: ansible>=2.9,<2.10
+ ansible210: ansible>=2.10.0a1,<2.11
+ # Be sure we do not install old ansible from default deps
+ # https://github.com/ansible/ansible/issues/70705
+ ansibledevel: ansible>=2.10.0a1,<2.11
+ ansibledevel: ansible-base @ git+https://github.com/ansible/ansible.git
+ -r test-requirements.in
+ -c test-requirements.txt
+commands =
+ # safety measure to assure we do not accidentaly run tests with broken dependencies
+ python -m pip check
+ {envpython} -m pytest \
+ --cov "{envsitepackagesdir}/ansiblelint" \
+ --junitxml "{toxworkdir}/junit.{envname}.xml" \
+ {posargs:}
+install_command =
+ {envpython} -m \
+ pip install \
+ {opts} \
+ {packages}
+passenv =
+ CURL_CA_BUNDLE # https proxies, https://github.com/tox-dev/tox/issues/1437
+ HOME
+ REQUESTS_CA_BUNDLE # https proxies
+ SSL_CERT_FILE # https proxies
+# recreate = True
+setenv =
+ ANSIBLE_COLLECTIONS_PATHS = {envtmpdir}
+ COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}}
+ PIP_DISABLE_PIP_VERSION_CHECK = 1
+whitelist_externals =
+ ansibledevel: sh
+
+[testenv:.dev-env]
+#basepython = python3
+basepython = /home/wk/.pyenv/versions/ansible-lint-py3.8.0-pyenv-venv/bin/python3
+#{[testenv]deps}
+deps =
+# virtualenv >= 16
+# setuptools >= 45.0.0
+isolated_build = false
+skip_install = true
+recreate = false
+usedevelop = false
+
+[testenv:build-dists]
+basepython = python3
+description =
+ Build dists with PEP 517 and save them in the dist/ dir
+skip_install = true
+deps =
+ pep517 >= 0.7.0
+commands =
+ {envpython} -c 'import os.path, shutil, sys; \
+ dist_dir = os.path.join("{toxinidir}", "dist"); \
+ os.path.isdir(dist_dir) or sys.exit(0); \
+ print("Removing \{!s\} contents...".format(dist_dir), file=sys.stderr); \
+ shutil.rmtree(dist_dir)'
+ {envpython} -m pep517.build \
+ --source \
+ --binary \
+ --out-dir {toxinidir}/dist/ \
+ {toxinidir}
+
+# deprecated: use more generic 'lint' instead
+[testenv:flake8]
+deps = {[testenv:lint]deps}
+envdir = {toxworkdir}/lint
+skip_install = true
+commands =
+ python -m pre_commit run --all-files flake8
+
+[testenv:lint]
+basepython = python3
+deps =
+ pre-commit>=2.6.0
+skip_install = true
+commands =
+ python -m pre_commit run {posargs:--all-files --hook-stage manual -v}
+passenv =
+ {[testenv]passenv}
+ PRE_COMMIT_HOME
+
+[testenv:docs]
+basepython = python3
+deps =
+ -r{toxinidir}/docs/requirements.in
+ -c{toxinidir}/docs/requirements.txt
+commands =
+ # Build the html docs with Sphinx:
+ {envpython} -m sphinx \
+ -j auto \
+ -b html \
+ --color \
+ -a \
+ -n \
+ -W \
+ -d "{temp_dir}/.doctrees" \
+ . \
+ "{envdir}/html"
+
+ # Print out the output docs dir and a way to serve html:
+ -{envpython} -c \
+ 'import pathlib; docs_dir = pathlib.Path(r"{envdir}") / "html"; index_file = docs_dir / "index.html"; '\
+ 'print("\n" + "=" * 120 + f"\n\nDocumentation available under `file://\{index_file\}`\n\nTo serve docs, use `python3 -m http.server --directory \{docs_dir\} 0`\n\n" + "=" * 120)'
+changedir = {toxinidir}/docs
+
+[testenv:metadata-validation]
+basepython = python3
+description =
+ Verify that dists under the dist/ dir have valid metadata
+depends = build-dists
+deps =
+ twine
+skip_install = true
+# Ref: https://twitter.com/di_codes/status/1044358639081975813
+commands =
+ twine check {toxinidir}/dist/*