Ansible Lint#

Ansible Lint – это утилита командной строки, предназначенная для проверки качества и стиля кода в сценариях, ролях и коллекциях Ansible. Она помогает следовать лучшим практикам при написании кода, выявлять распространенные ошибки и потенциальные проблемы, а также поддерживать чистоту и читаемость кода.

Основные возможности Ansible Lint#

Основные возможности Ansible Lint:

  • Проверка на соответствие стандартам.

    Ansible Lint содержит правила написания кода из лучших практик, что позволяет избежать ошибок, ухудшающих качество и поддерживаемость кода.

  • Автоматическое выявление ошибок.

    Утилита анализирует контент Ansible, указывая на места, которые могут быть потенциально проблемными или нестандартными.

  • Адаптация к новым версиям Ansible.

    Ansible Lint помогает адаптировать код к новым версиям Ansible, что упрощает его обновление и предотвращает возможные несовместимости.

Установка#

Для установки Ansible Lint выполните следующие действия:

  1. Подключите репозиторий Astra Automation.

    Инструкция по подключению репозитория
    1. В каталоге /etc/apt/sources.list.d/ создайте файл astra-automation.list со ссылкой на репозиторий Astra Automation:

      deb https://dl.astralinux.ru/aa/aa-debs-for-alse-1.7 <version> main
      

      Вместо <version> необходимо подставить версию устанавливаемой платформы, например, 1.1-upd1.

      Доступные версии продукта опубликованы в таблице История обновлений.

    2. Обновите список доступных пакетов:

      sudo apt update
      
  2. Выполните команду:

    sudo apt-get install ansible-lint --yes
    

Установка при отсутствии доступа к интернету описана в документе Инструменты.

Настройка Ansible Lint#

Ansible Lint предоставляет различные возможности для настройки в проектах:

  • игнорирование определенных правил;

  • включение дополнительных проверок;

  • настройка профилей под конкретные нужды.

Ansible Lint можно настроить с помощью следующих компонентов:

При вычислении действующих значений аргументов используется следующий порядок (значения, указанные на более поздних этапах, заменяют значения, указанные ранее):

  1. Переменные окружения.

  2. Файл настроек.

  3. Аргументы команды.

По умолчанию Ansible Lint начинает поиск файла настроек с текущего каталога. Возможные названия и расположения файлов:

  • .ansible-lint;

  • .config/ansible-lint.yml;

  • .config/ansible-lint.yaml.

Если файл настроек не найден в текущем каталоге, Ansible Lint попытается найти его в родительских каталогах. Если используется Git, то поиск осуществляется только в пределах текущего репозитория. Путь к файлу настроек можно указать явно с помощью аргумента -c, например:

ansible-lint -c ansible-lint-config.yml

Настройки в конфигурационном файле задаются следующим образом:

Пример файла настроек
.ansible-lint#
---
# .ansible-lint

profile: null # min, basic, moderate, safety, shared, production

# Allows dumping of results in SARIF format
# sarif_file: result.sarif

# exclude_paths included in this file are parsed relative to this file's location
# and not relative to the CWD of execution. CLI arguments passed to the --exclude
# option are parsed relative to the CWD of execution.
exclude_paths:
  - .cache/ # implicit unless exclude_paths is defined in config
  - test/fixtures/formatting-before/
  - test/fixtures/formatting-prettier/
# parseable: true
# quiet: true
# strict: true
# verbosity: 1

# Mock modules or roles in order to pass ansible-playbook --syntax-check
mock_modules:
  - zuul_return
  # note the foo.bar is invalid as being neither a module or a collection
  - fake_namespace.fake_collection.fake_module
  - fake_namespace.fake_collection.fake_module.fake_submodule
mock_roles:
  - mocked_role
  - author.role_name # old standalone galaxy role
  - fake_namespace.fake_collection.fake_role # role within a collection

# Enable checking of loop variable prefixes in roles
loop_var_prefix: "^(__|{role}_)"

# Enforce variable names to follow pattern below, in addition to Ansible own
# requirements, like avoiding python identifiers. To disable add `var-naming`
# to skip_list.
var_naming_pattern: "^[a-z_][a-z0-9_]*$"

use_default_rules: true
# Load custom rules from this specific folder
# rulesdir:
#   - ./rule/directory/

# Ansible-lint is able to recognize and load skip rules stored inside
# `.ansible-lint-ignore` (or `.config/ansible-lint-ignore.txt`) files.
# To skip a rule just enter filename and tag, like "playbook.yml package-latest"
# on a new line.
# Optionally you can add comments after the tag, prefixed by "#". We discourage
# the use of skip_list below because that will hide violations from the output.
# When putting ignores inside the ignore file, they are marked as ignored, but
# still visible, making it easier to address later.
skip_list:
  - skip_this_tag

# Ansible-lint does not automatically load rules that have the 'opt-in' tag.
# You must enable opt-in rules by listing each rule 'id' below.
enable_list:
  - args
  - empty-string-compare # opt-in
  - no-log-password # opt-in
  - no-same-owner # opt-in
  - name[prefix] # opt-in
  - galaxy-version-incorrect # opt-in
  # add yaml here if you want to avoid ignoring yaml checks when yamllint
  # library is missing. Normally its absence just skips using that rule.
  - yaml
# Report only a subset of tags and fully ignore any others
# tags:
#   - jinja[spacing]

# Ansible-lint does not fail on warnings from the rules or tags listed below
warn_list:
  - skip_this_tag
  - experimental # experimental is included in the implicit list
# - role-name
# - yaml[document-start]  # you can also use sub-rule matches

# Some rules can transform files to fix (or make it easier to fix) identified
# errors. `ansible-lint --fix` will reformat YAML files and run these transforms.
# By default it will run all transforms (effectively `write_list: ["all"]`).
# You can disable running transforms by setting `write_list: ["none"]`.
# Or only enable a subset of rule transforms by listing rules/tags here.
# write_list:
#   - all

# Offline mode disables installation of requirements.yml and schema refreshing
offline: true

# Define required Ansible's variables to satisfy syntax check
extra_vars:
foo: bar
multiline_string_variable: |
  line1
  line2
complex_variable: ":{;\t$()"
# Uncomment to enforce action validation with tasks, usually is not
# needed as Ansible syntax check also covers it.
# skip_action_validation: false

# List of additional kind:pattern to be added at the top of the default
# match list, first match determines the file kind.
kinds:
  # - playbook: "**/examples/*.{yml,yaml}"
  # - galaxy: "**/folder/galaxy.yml"
  # - tasks: "**/tasks/*.yml"
  # - vars: "**/vars/*.yml"
  # - meta: "**/meta/main.yml"
  - yaml: "**/*.yaml-too"

# List of additional collections to allow in only-builtins rule.
# only_builtins_allow_collections:
#   - example_ns.example_collection

# List of additions modules to allow in only-builtins rule.
# only_builtins_allow_modules:
#   - example_module

# Allow setting custom prefix for name[prefix] rule
task_name_prefix: "{stem} | "
# Complexity related settings

# Limit the depth of the nested blocks:
# max_block_depth: 20

# Also recognize these versions of Ansible as supported:
# supported_ansible_also:
#   - "2.14"

Подробное описание параметров см. в справочнике.

Игнорирование правил для определенных файлов#

Чтобы при проверке файлов Ansible Lint не использовал определенные правила, создайте файл .ansible-lint-ignore или .config/ansible-lint-ignore.txt. Созданный файл должен быть размещен в том же каталоге, что и файл основных настроек Ansible Lint. Каждая строка содержит путь к файлу и через пробел идентификатор правила.

Пример заполнения:

# Игнорирование правила package-latest для файла playbook.yml
playbook.yml package-latest

# Игнорирование правила deprecated-module для playbook.yml
playbook.yml deprecated-module

# Игнорирование правила no-tabs для роли my_role
roles/my_role/tasks/main.yml no-tabs

Создание правил#

Ansible Lint позволяет создавать правила, содержащие собственные уникальные проверки, для поддержки стиля кодирования, принятого в вашей организации. Каждое правило необходимо создавать в отдельном файле Python и описывать в виде класса, который наследует AnsibleLintRule. Файл с правилом следует именовать согласно цели правила, например, DeprecatedVariableRule.py. Внутри каждого класса должны быть следующие поля:

  • id – уникальный идентификатор правила;

  • description – краткое описание правила (какой тип ошибок оно выявляет);

  • tags – список тегов для группировки правил, близких по смыслу.

Для проверки кода по заданному правилу используют следующие методы:

  • match;

  • matchtask.

Метод match работает с каждой строкой и возвращает результат проверки. Если строка не соответствует критерию, метод возвращает None или False. Если строка соответствует критерию, метод возвращает True или настраиваемое сообщение.

Пример правила, реализующего метод match:

class DeprecatedVariableRule(AnsibleLintRule):
    """Deprecated variable declarations."""
    id = 'DEPRECATED_VAR'
    description = 'Check for deprecated variable syntax ${var}'
    tags = ['deprecations']

    def match(self, line: str) -> Union[bool, str]:
        return '${' in line

Метод matchtask обрабатывает каждую задачу или обработчик (handler). Все задачи стандартизируются, чтобы включать ключи module и module_arguments. Если в задаче присутствуют модификаторы when, with_items, tags и другие, они также доступны для проверки.

Пример правила, реализующего метод matchtask:

from ansiblelint.rules import AnsibleLintRule
from typing import Union

class TaskHasTag(AnsibleLintRule):
    """Ensure every task has a tag for better organization."""
    id = 'TASK_TAG'
    description = 'Tasks must have a tag to improve organization and filtering'
    tags = ['department', 'team']

    def matchtask(self, task, file=None) -> Union[bool, str]:
        if 'tags' not in task:
           return "Task missing a tag"
        return False

Примечание

Для создания более сложных правил рекомендуется изучить методы и свойства класса AnsibleLintRule и его родительских классов.

Пример#

Для изучения Ansible Lint на практике, сделайте клон репозитория, содержащего примеры сценариев:

git clone https://github.com/ansible/ansible-lint.git

Проверка сценария#

Для проверки сценария example.yml выполните команду:

ansible-lint --offline -p ansible-lint/examples/playbooks/example.yml

Пример вывода:

ansible-lint/examples/playbooks/example.yml:1: schema[playbook]: $[0].tasks[13] None is not of type 'object'
ansible-lint/examples/playbooks/example.yml:9: jinja[spacing]: Jinja2 spacing could be improved: echo {{this_variable}} is not set in this playbook -> echo {{ this_variable }} is not set in this playbook (warning)
ansible-lint/examples/playbooks/example.yml:9: no-changed-when: Commands should not change things if nothing needs doing.
ansible-lint/examples/playbooks/example.yml:12: no-changed-when: Commands should not change things if nothing needs doing.
ansible-lint/examples/playbooks/example.yml:13: yaml[trailing-spaces]: Trailing spaces
ansible-lint/examples/playbooks/example.yml:15: args[module]: missing required arguments: repo (warning)
ansible-lint/examples/playbooks/example.yml:15: latest[git]: Result of the command may vary on subsequent runs.
ansible-lint/examples/playbooks/example.yml:18: args[module]: missing required arguments: repo (warning)
ansible-lint/examples/playbooks/example.yml:18: latest[git]: Result of the command may vary on subsequent runs.
ansible-lint/examples/playbooks/example.yml:21: args[module]: Unsupported parameters for (basic.py) module: bobbins. Supported parameters include: accept_hostkey, accept_newhostkey, archive, archive_prefix, bare, clone, depth, dest, executable, force, gpg_whitelist, key_file, recursive, reference, refspec, remote, repo, separate_git_dir, single_branch, ssh_opts, track_submodules, umask, update, verify_commit, version (name). (warning)
ansible-lint/examples/playbooks/example.yml:21: no-free-form: Avoid using free-form when calling module actions. (ansible.builtin.git)
ansible-lint/examples/playbooks/example.yml:24: command-instead-of-module: git used in place of git module
ansible-lint/examples/playbooks/example.yml:24: no-changed-when: Commands should not change things if nothing needs doing.
ansible-lint/examples/playbooks/example.yml:27: command-instead-of-module: git used in place of git module
ansible-lint/examples/playbooks/example.yml:30: latest[git]: Result of the command may vary on subsequent runs.
ansible-lint/examples/playbooks/example.yml:33: jinja[spacing]: Jinja2 spacing could be improved: {{item}} -> {{ item }} (warning)
ansible-lint/examples/playbooks/example.yml:36: yaml[indentation]: Wrong indentation: expected 8 but found 6
ansible-lint/examples/playbooks/example.yml:39: no-free-form: Avoid using free-form when calling module actions. (ansible.builtin.dnf)
ansible-lint/examples/playbooks/example.yml:39: package-latest: Package installs should not use latest.
ansible-lint/examples/playbooks/example.yml:42: name[missing]: All tasks should be named.
ansible-lint/examples/playbooks/example.yml:42: no-free-form: Avoid using free-form when calling module actions. (ansible.builtin.debug)
ansible-lint/examples/playbooks/example.yml:44: no-free-form: Avoid using free-form when calling module actions. (ansible.builtin.apt)
ansible-lint/examples/playbooks/example.yml:44: package-latest: Package installs should not use latest.
ansible-lint/examples/playbooks/example.yml:47: name[missing]: All tasks should be named.
Read documentation for instructions on how to ignore specific rule violations.

                       Rule Violation Summary
count tag                       profile rule associated tags
    2 command-instead-of-module basic   command-shell, idiom
    2 jinja[spacing]            basic   formatting (warning)
    4 no-free-form              basic   syntax, risk
    1 schema[playbook]          basic   core
    2 name[missing]             basic   idiom
    1 yaml[indentation]         basic   formatting, yaml
    1 yaml[trailing-spaces]     basic   formatting, yaml
    3 latest[git]               safety  idempotency
    2 package-latest            safety  idempotency
    3 no-changed-when           shared  command-shell, idempotency
    3 args[module]                      syntax, experimental (warning)

Failed: 19 failure(s), 5 warning(s) on 1 files. Last profile that met the validation criteria was 'min'.

Пример создания правила#

Чтобы создать правила на основе методов match и matchtask и проверить с помощью этих правил сценарий example.com, выполните следующие действия:

  1. Создайте каталог для правил и перейдите в него, например:

    mkdir my_custom_rules && cd my_custom_rules
    
  2. Создайте файл TaskHasTag.py, содержащий правило для проверки наличия тега:

    from __future__ import annotations
    from typing import TYPE_CHECKING, Union
    
    from ansiblelint.rules import AnsibleLintRule
    
    if TYPE_CHECKING:
        from ansiblelint.file_utils import Lintable
        from ansiblelint.utils import Task
    
    
    class TaskHasTag(AnsibleLintRule):
        """Tasks must have tag."""
    
        id = 'EXAMPLE001'
        description = 'Tasks must have tag'
        tags = ['custom', 'core']
    
        def matchtask(self,
                     task: Task,
                     file: Lintable | None = None
                     ) -> Union[bool, str]:
            # Эта проверка исключает задачи include и fail из требований наличия тегов,
            # так как для них теги не обязательны:
            # Если задача включает другие задачи (include) или намеренно завершает выполнение (fail),
            # она исключается из проверки на наличие тегов, возвращая False.
            if not set(task.keys()).isdisjoint(['include', 'fail']):
                return False
    
            if not task.get("tags"):
                return True
            return False
    
  3. Создайте файл TestRule.py, содержащий правило для проверки наличия определенного слова:

    from typing import Union
    from ansiblelint.rules import AnsibleLintRule
    
    
    class TestRule(AnsibleLintRule):
        """Deprecated variable declarations."""
    
        id = 'EXAMPLE002'
        description = 'This rule triggers on tasks containing the word "task".'
        tags = ['custom', 'core']
    
        def match(self, line: str) -> Union[bool, str]:
            # Срабатывает только для строк, содержащих "task" — для тестирования
            return 'task' in line
    
  4. Запустите проверку сценария example.com:

    ansible-lint \
      --offline \
      -r my_custom_rules/ \
      -t custom ~/ansible-lint/examples/playbooks/example.yml
    

    Пример вывода:

    EXAMPLE002: Deprecated variable declarations.
    ansible-lint/examples/playbooks/example.yml:8   tasks:
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:9 Task/Handler: Unset variable
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:12 Task/Handler: Trailing whitespace
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:15 Task/Handler: Run git check
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:18 Task/Handler: Run git check 2
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:21 Task/Handler: Run git check 3
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:24 Task/Handler: Executing git through command
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:27 Task/Handler: Executing git through command
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:30 Task/Handler: Using git module
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:33 Task/Handler: Passing git as an argument to another task
    
    EXAMPLE002: Deprecated variable declarations.
    ansible-lint/examples/playbooks/example.yml:33     - name: Passing git as an argument to another task
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:39 Task/Handler: Dnf latest
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:42 Task/Handler: debug msg=debug task without a name
    
    EXAMPLE002: Deprecated variable declarations.
    ansible-lint/examples/playbooks/example.yml:42     - ansible.builtin.debug: msg="debug task without a name"
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:44 Task/Handler: Run apt latest
    
    EXAMPLE001: Tasks must have tag.
    ansible-lint/examples/playbooks/example.yml:47 Task/Handler: meta flush_handlers
    
    Read documentation for instructions on how to ignore specific rule violations.
    
                Rule Violation Summary
    count tag        profile rule associated tags
       13 EXAMPLE001         custom, core
       3 EXAMPLE002         custom, core
    
    Failed: 16 failure(s), 0 warning(s) on 1 files. Last profile that met the validation criteria was 'production'. Rating: 5/5 star