# ansible-later

[![Build Status](https://cloud.drone.io/api/badges/xoxys/ansible-later/status.svg)](https://cloud.drone.io/xoxys/ansible-later)
[![](https://img.shields.io/pypi/pyversions/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![](https://img.shields.io/pypi/status/ansible-later.svg)](https://pypi.org/project/ansible-later/)
[![](https://img.shields.io/pypi/v/ansible-later.svg)](https://pypi.org/project/ansible-later/)

This is a fork of Will Thames [ansible-review](https://github.com/willthames/ansible-review) so credits goes to him
for his work on ansible-review and ansible-lint.

ansible-later is an acronym for **L**ovely **A**utomation **TE**sting f**R**mework.

## Table of Content

- [Setup](#setup)
  - [Using pip](#using-pip)
  - [From source](#from-source)
- [Usage](#usage)
  - [Configuration](#configuration)
  - [Review a git repositories](#review-a-git-repositories)
  - [Review a list of files](#review-a-list-of-files)
  - [Buildin rules](#buildin-rules)
- [Build your own](#build-your-own)
  - [The standards file](#the-standards-file)
  - [Candidates](#candidates)
  - [Minimal standards checks](#minimal-standards-checks)
- [License](#license)
- [Maintainers and Contributors](#maintainers-and-contributors)

---

### Setup

#### Using pip

```Shell
# From internal pip repo as user
pip install ansible-later --user

# .. or as root
sudo pip install ansible-later
```

#### From source

```Shell
# Install dependency
git clone https://repourl
export PYTHONPATH=$PYTHONPATH:`pwd`/ansible-later/ansiblelater
export PATH=$PATH:`pwd`/ansible-later/ansiblelater/bin
```

### Usage

```Shell
ansible-later FILES
```

Where FILES is a space delimited list of files to review.
ansible-later is _not_ recursive and won't descend
into child folders; it just processes the list of files you give it.

Passing a folder in with the list of files will elicit a warning:

```Shell
WARN: Couldn't classify file ./foldername
```

ansible-later will review inventory files, role
files, python code (modules, plugins) and playbooks.

- The goal is that each file that changes in a
  changeset should be reviewable simply by passing
  those files as the arguments to ansible-later.
- Roles are slightly harder, and sub-roles are yet
  harder still (currently just using `-R` to process
  roles works very well, but doesn't examine the
  structure of the role)
- Using `{{ playbook_dir }}` in sub roles is so far
  very hard.
- This should work against various repository styles
  - per-role repository
  - roles with sub-roles
  - per-playbook repository
- It should work with roles requirement files and with local roles

#### Configuration

If your standards (and optionally inhouse rules) are set up, create
a configuration file in the appropriate location (this will depend on
your operating system)

The location can be found by using `ansible-later` with no arguments.

You can override the configuration file location with the `-c` flag.

```INI
[rules]
standards = /path/to/your/standards/rules
```

The standards directory can be overridden with the `-d` argument.

#### Review a git repositories

- `git ls-files | xargs ansible-later` works well in
  a roles repo to review the whole role. But it will
  review the whole of other repos too.
- `git ls-files *[^LICENSE,.md] | xargs ansible-later`
  works like the first example but excludes some
  unnecessary files.
- `git diff branch_to_compare | ansible-later` will
  review only the changes between the branches and
  surrounding context.

#### Review a list of files

- `find . -type f | xargs ansible-later` will review
  all files in the current folder (and all subfolders),
  even if they're not checked into git

#### Buildin rules

Reviews are nothing without some rules or standards against which to review. ansible-later
comes with a couple of built-in checks explained in the following table.

| Rule                            | ID          | Description                                                      | Parameter                                                            |
| ------------------------------- | ----------- | ---------------------------------------------------------------- | -------------------------------------------------------------------- |
| check_yaml_empty_lines          | LINT0001    | YAML should not contain unnecessarily empty lines.               | {max: 1, max-start: 0, max-end: 0}                                   |
| check_yaml_indent               | LINT0002    | YAML should be correctly indented.                               | {spaces: 2, check-multi-line-strings: false, indent-sequences: true} |
| check_yaml_hyphens              | LINT0003    | YAML should use consitent number of spaces after hyphens (-).    | {max-spaces-after: 1}                                                |
| check_yaml_document_start       | LINT0004    | YAML should contain document start marker.                       | {document-start: {present: true}}                                    |
| check_yaml_colons               | LINT0005    | YAML should use consitent number of spaces around colons.        | {colons: {max-spaces-before: 0, max-spaces-after: 1}}                |
| check_yaml_file                 | LINT0006    | Roles file should be in yaml format.                             |                                                                      |
| check_yaml_has_content          | LINT0007    | Files should contain useful content.                             |                                                                      |
| check_native_yaml               | LINT0008    | Use YAML format for tasks and handlers rather than key=value.    |                                                                      |
| check_line_between_tasks        | ANSIBLE0001 | Single tasks should be separated by an empty line.               |                                                                      |
| check_meta_main                 | ANSIBLE0002 | Meta file should contain a basic subset of parameters.           | author, description, min_ansible_version, platforms, dependencies    |
| check_unique_named_task         | ANSIBLE0003 | Tasks and handlers must be uniquely named within a file.         |                                                                      |
| check_braces                    | ANSIBLE0004 | YAML should use consitent number of spaces around variables.     |                                                                      |
| check_scm_in_src                | ANSIBLE0005 | Use scm key rather than src: scm+url in requirements file.       |                                                                      |
| check_named_task                | ANSIBLE0006 | Tasks and handlers must be named.                                | excludes: meta, debug, include\_\*, import\_\*, block                |
| check_name_format               | ANSIBLE0007 | Name of tasks and handlers must be formatted.                    | formats: first letter capital                                        |
| check_command_instead_of_module | ANSIBLE0008 | Commands should not be used in place of modules.                 |                                                                      |
| check_install_use_latest        | ANSIBLE0009 | Package managers should not install with state=latest.           |                                                                      |
| check_shell_instead_command     | ANSIBLE0010 | Use Shell only when piping, redirecting or chaining commands.    |                                                                      |
| check_command_has_changes       | ANSIBLE0011 | Commands should be idempotent and only used with some checks.    |                                                                      |
| check_empty_string_compare      | ANSIBLE0012 | Don't compare to "" - use `when: var` or `when: not var`         |                                                                      |
| check_compare_to_literal_bool   | ANSIBLE0013 | Don't compare to True/False - use `when: var` or `when: not var` |                                                                      |

### Build your own

#### The standards file

A standards file comprises a list of standards, and optionally some methods to
check those standards.

Create a file called standards.py (this can import other modules)

```Python
from ansiblelater include Standard, Result

tasks_are_uniquely_named = Standard(dict(
    id="ANSIBLE0003",
    name="Tasks and handlers must be uniquely named within a single file",
    check=check_unique_named_task,
    version="0.1",
    types=["playbook", "task", "handler"],
))

standards = [
  tasks_are_uniquely_named,
  role_must_contain_meta_main,
]
```

When you add new standards, you should increment the version of your standards.
Your playbooks and roles should declare what version of standards you are
using, otherwise ansible-later assumes you're using the latest. The declaration
is done by adding standards version as first line in the file. e.g.

```INI
# Standards: 1.2
```

To add standards that are advisory, don't set the version. These will cause
a message to be displayed but won't constitute a failure.

When a standard version is higher than declared version, a message will be
displayed 'WARN: Future standard' and won't constitute a failure.

An example standards file is available at
[ansiblelater/examples/standards.py](ansiblelater/examples/standards.py)

If you only want to check one or two standards quickly (perhaps you want
to review your entire code base for deprecated bare words), you can use the
`-s` flag with the name of your standard. You can pass `-s` multiple times.

```Shell
git ls-files | xargs ansible-later -s "bare words are deprecated for with_items"
```

You can see the name of the standards being checked for each different file by running
`ansible-later` with the `-v` option.

#### Candidates

Each file passed to `ansible-later` will be classified. The result is a `Candidate` object
which contains some meta informations and is an instance of one of following object types.

| Object type | Description                                                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Task        | all files within the parent dir `tasks`                                                                                      |
| Handler     | all files within the parent dir `handler`                                                                                    |
| RoleVars    | all files within the parent dir `vars` or `default`                                                                          |
| GroupVars   | all files (including subdirs) within the parent dir `group_vars`                                                             |
| HostVars    | all files (including subdirs) within the parent dir `host_vars`                                                              |
| Meta        | all files within the parent dir `meta`                                                                                       |
| Code        | all files within the parent dir `library`, `lookup_plugins`, `callback_plugins` and `filter_plugins` or python files (`.py`) |
| Inventory   | all files within the parent dir `inventory` and `inventory` or `hosts` in filename                                           |
| Rolesfile   | all files with `rolesfile` or `requirements` in filename                                                                     |
| Makefile    | all files with `Makefile` in filename                                                                                        |
| Template    | all files (including subdirs) within the parent dir `templates` or jinja2 files (`.j2`)                                      |
| File        | all files (including subdirs) within the parent dir `files`                                                                  |
| Playbook    | all yaml files (`.yml` or `.yaml`) not maching a previous object type                                                        |
| Doc         | all files with `README` in filename                                                                                          |

#### Minimal standards checks

A typical standards check will look like:

```Python
def check_playbook_for_something(candidate, settings):
    result = Result(candidate.path) # empty result is a success with no output
    with open(candidate.path, 'r') as f:
        for (lineno, line) in enumerate(f):
            if line is dodgy:
                # enumerate is 0-based so add 1 to lineno
                result.errors.append(Error(lineno+1, "Line is dodgy: reasons"))
    return result
```

All standards check take a candidate object, which has a path attribute.
The type can be inferred from the class name (i.e. `type(candidate).__name__`)
or from the table [here](#candidates).

They return a `Result` object, which contains a possibly empty list of `Error`
objects. `Error` objects are formed of a line number and a message. If the
error applies to the whole file being reviewed, set the line number to `None`.
Line numbers are important as `ansible-later` can review just ranges of files
to only review changes (e.g. through piping the output of `git diff` to
`ansible-later`).

### License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

### Maintainers and Contributors

[Robert Kaussow](https://github.com/xoxys)
