Commit 078e6d46 authored by Gregory Colpart's avatar Gregory Colpart

Import upstream from Git (commit 9548752d389a85d42545f2fc1d498ff0a5c3e1a9)

parents
# Byte-compiled / optimized / DLL files
__pycache__
*.py[co]
*$py.class
# Packages
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.tox
---
# 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)$
language: python
python:
- "2.7"
install:
- pip install -r test-deps.txt
script: tox
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:
```
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
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 rules 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 don't feel able to contribute a code fix.
Standards
=========
ansible-lint is flake8 compliant with `max-line-length` set to 100
(see [setup.cfg](setup.cfg)).
ansible-lint works with both Ansible 1.9 and Ansible 2.0 onwards. This will be
the case for the foreseeable future, so please ensure all contributions work
with both.
Automated tests will be run against all PRs for flake8 compliance and Ansible
compatibility - to check before pushing commits, just use `tox`.
# 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
# Actual Behaviour (Bug report only)
Please give some details of what is actually happening.
Include a [minimum complete verifiable example](http://stackoverflow.com/help/mcve)
with:
- playbook
- output of running ansible-lint
- if you're getting a stack trace, output of
`ansible-playbook --syntax-check playbook`
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.
include LICENSE
Ansible-lint
============
ansible-lint checks playbooks for practices and behaviour that could
potentially be improved
[![PyPI version](https://img.shields.io/pypi/v/ansible-lint.svg)](https://pypi.python.org/pypi/ansible-lint)
[![Build Status](https://travis-ci.org/willthames/ansible-lint.svg?branch=master)](https://travis-ci.org/willthames/ansible-lint)
Setup
-----
Using pip:
```
pip2 install ansible-lint
```
From source:
```
git clone https://github.com/willthames/ansible-lint
export PYTHONPATH=$PYTHONPATH:`pwd`/ansible-lint/lib
export PATH=$PATH:`pwd`/ansible-lint/bin
```
Usage
-----
```
Usage: ansible-lint playbook.yml|roledirectory ...
Options:
--version show program's version number and exit
-h, --help show this help message and exit
-L list all the rules
-q quieter, although not silent output
-p parseable output in the format of pep8
-r RULESDIR specify one or more rules directories using one or
more -r arguments. Any -r flags override the default
rules in ['/path/to/ansible-
lint/lib/ansiblelint/rules'], unless -R is also used.
-R Use default rules ['/path/to/ansible-
lint/lib/ansiblelint/rules'] in addition to any extra
rules directories specified with -r. There is no need
to specify this if no -r flags are used
-t TAGS only check rules whose id/tags match these values
-T list all the tags
-x SKIP_LIST only check rules whose id/tags do not match these
values
--exclude=EXCLUDE_PATHS
path to directories or files to skip. This option is
repeatable.
--force-color Try force colored output (relying on ansible's code)
--nocolor disable colored output
```
False positives
===============
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.
There are two mechanisms for this - one works with all tasks, the other
works with the command checking modules.
Use the `warn` parameter with the command or shell module.
Use `skip_ansible_lint` tag with any task that you want to skip.
I recommend commenting the reasons why you're skipping the check.
Unfortunately ansible-lint is unable to check for such comments
at this time! (patches welcome)
```
- name: this would typically fire CommandsInsteadOfArgumentRule
command: warn=no chmod 644 X
- name: this would typically fire CommandsInsteadOfModuleRule
command: git pull --rebase
args:
warn: False
- name: this would typically fire GitHasVersionRule
git: src=/path/to/git/repo dest=checkout
tags:
- skip_ansible_lint
```
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 (this
allows one rule to test multiple behaviours - see e.g. the
CommandsInsteadOfModulesRule
* `matchblock` that takes the details about the file and a block.
It returns `None` or `False` if the line doesn't match the test
and `True` or a custom message.
* `matchtask` operates on a single task or handler. Such a task
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:
```python
from ansiblelint import AnsibleLintRule
class DeprecatedVariableRule(AnsibleLintRule):
id = 'ANSIBLE0001'
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:
```python
import ansiblelint.utils
from ansiblelint import AnsibleLintRule
class TaskHasTag(AnsibleLintRule):
id = 'ANSIBLE0008'
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__']`
Examples
--------
There are some example playbooks with undesirable features. Running
ansible-lint on them works:
```
$ ansible-lint examples/example.yml
[ANSIBLE0004] Git checkouts must contain explicit version
examples/example.yml:15
Task/Handler: git check
[ANSIBLE0004] Git checkouts must contain explicit version
examples/example.yml:18
Task/Handler: git check 2
[ANSIBLE0004] Git checkouts must contain explicit version
examples/example.yml:30
Task/Handler: using git module
[ANSIBLE0002] Trailing whitespace
examples/example.yml:13
action: do nothing
[ANSIBLE0002] Trailing whitespace
examples/example.yml:35
with_items:
[ANSIBLE0006] git used in place of git module
examples/example.yml:24
Task/Handler: executing git through command
[ANSIBLE0006] git used in place of git module
examples/example.yml:27
Task/Handler: executing git through command
[ANSIBLE0006] git used in place of git module
examples/example.yml:30
Task/Handler: executing git through command
```
If playbooks include other playbooks, or tasks, or handlers or roles, these
are also handled:
```
$ bin/ansible-lint examples/include.yml
[ANSIBLE0004] Checkouts must contain explicit version
/Users/will/src/ansible-lint/examples/roles/bobbins/tasks/main.yml:3
action: git a=b c=d
```
As of version 2.4.0, ansible-lint now works just on roles (this is useful
for CI of roles)
Pre-commit
==========
To use ansible-lint with [pre-commit](http://pre-commit.com/), just
add the following to your local repo's `.pre-commit-config.yaml` file.
Make sure to change `sha:` to be either a git commit sha or tag of
ansible-lint containing `hooks.yaml`.
```yaml
- repo: https://github.com/willthames/ansible-lint.git
sha: v3.3.1
hooks:
- id: ansible-lint
files: \.(yaml|yml)$
```
Contributing
============
Please read
[CONTRIBUTING.md](https://github.com/willthames/ansible-lint/blob/master/CONTRIBUTING.md) if you wish to contribute.
---
- 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
- name: funny handler
action: service name=funny state=started force=true
---
- 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
- tasks:
- git: repo=hello
This diff is collapsed.
---
- hosts: whatever
tasks:
- name: hello world
action: debug msg="Hello!"
- name: this should be fine too
action: file state=touch dest=./wherever
---
- hosts: bobbins
tasks:
- name: a bad play
action: command service blah restart
---
- name: test tasks
action: git a=b c=d
---
dependencies:
- role: bobbins
- name: restart service using command
command: service bar restart
- 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
from ansiblelint import AnsibleLintRule
class TaskHasTag(AnsibleLintRule):
id = 'EXAMPLE001'
shortdesc = 'Tasks must have tag'
description = 'Tasks must have tag'
tags = ['productivity', 'tags']
def matchtask(self, file, task):
# The meta files don't have tags
if file['type'] in ["meta", "playbooks"]:
return False
if isinstance(task, basestring):
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
# Task should have tags
if 'tags' not in task:
return True
return False
- name: test include
action: funny value=clown
args:
key: value
---
# 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)$
# 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 __future__ import print_function
from collections import defaultdict
import os
import sys
import six
import ansiblelint.utils
import codecs
default_rulesdir = os.path.join(os.path.dirname(ansiblelint.utils.__file__), 'rules')
class AnsibleLintRule(object):
def __repr__(self):
return self.id + ": " + self.shortdesc
def verbose(self):
return self.id + ": " + self.shortdesc + "\n " + self.description
match = None
matchtask = None
matchplay = None
def matchlines(self, file, text):
matches = []
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")):
result = self.match(file, line)
if result:
message = None
if isinstance(result, str):
message = result
matches.append(Match(prev_line_no+1, line,
file['path'], self, message))
return matches
def matchtasks(self, file, text):
matches = []
if not self.matchtask:
return matches
yaml = ansiblelint.utils.parse_yaml_linenumbers(text, file['path'])
if yaml:
for task in ansiblelint.utils.get_normalized_tasks(yaml, file):
if 'action' in task:
result = self.matchtask(file, task)
if result:
message = None
if isinstance(result, six.string_types):
message = result
taskstr = "Task/Handler: " + ansiblelint.utils.task_to_str(task)
matches.append(Match(task[ansiblelint.utils.LINE_NUMBER_KEY], taskstr,
file['path'], self, message))
return matches
def matchyaml(self, file, text):
matches = []
if not self.matchplay:
return matches
yaml = ansiblelint.utils.parse_yaml_linenumbers(text, file['path'])
if yaml and hasattr(self, 'matchplay'):
if isinstance(yaml, dict):
yaml = [yaml]
for play in yaml:
result = self.matchplay(file, play)
if result:
if isinstance(result, tuple):
result = [result]
if not isinstance(result, list):
raise Exception("{} is not a list".format(result))
for section, message in result:
matches.append(Match(play[ansiblelint.utils.LINE_NUMBER_KEY],
section, file['path'], self, message))
return matches
class RulesCollection(object):
def __init__(self):
self.rules = []
def register(self, obj):
self.rules.append(obj)
def __iter__(self):
return iter(self.rules)
def __len__(self):
return len(self.rules)
def extend(self, more):
self.rules.extend(more)
def run(self, playbookfile, tags=set(), skip_list=set()):
text = ""
matches = list()
try:
with codecs.open(playbookfile['path'], mode='rb', encoding='utf-8') as f:
text = f.read()
except IOError as e:
print("WARNING: Couldn't open %s - %s" %
(playbookfile['path'], e.strerror),
file=sys.stderr)
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):
return "\n".join([rule.verbose()
for rule in sorted(self.rules, key=lambda x: x.id)])
def listtags(self):
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)
@classmethod
def create_from_directory(cls, rulesdir):
result = cls()
result.rules = ansiblelint.utils.load_plugins(os.path.expanduser(rulesdir))
return result