Open-Source Wikis

/

Ansible

/

Primitives

/

Playbook and play

ansible/ansible

Playbook and play

A playbook is a list of plays. A play is a unit of "do X to Y under conditions Z". Both are primitive YAML-loadable objects in lib/ansible/playbook/.

Files

File Class Lines
lib/ansible/playbook/__init__.py Playbook 116
lib/ansible/playbook/play.py Play 483
lib/ansible/playbook/playbook_include.py PlaybookInclude 130
lib/ansible/playbook/base.py Base (parent class) 800
lib/ansible/playbook/play_context.py PlayContext 360

Playbook

Playbook is the simplest of the three. Its job is to represent a YAML file containing a list of plays:

class Playbook:
    def __init__(self, loader):
        self._entries = []      # list of Play (and PlaybookInclude)
        self._basedir = ...
        self._loader = loader

    @staticmethod
    def load(file_name, variable_manager=None, loader=None):
        pb = Playbook(loader=loader)
        pb._load_playbook_data(file_name, variable_manager)
        return pb

_load_playbook_data opens the file via DataLoader, asserts it's a list, and iterates. For each entry:

  • If it has import_playbook:, instantiate a PlaybookInclude and splice in the included playbook's plays.
  • Otherwise, instantiate a Play.load(entry, ...) and append.

The loader's basedir is set per-playbook so relative paths in includes resolve correctly.

Play

Play is the workhorse. A play is a YAML dict with a hosts: pattern and optional tasks:, roles:, pre_tasks:, post_tasks:, handlers:, vars:, vars_files:, plus dozens of other keywords.

Play extends Base, plus mixins Taggable, CollectionSearch. Its FieldAttribute declarations enumerate every supported YAML key:

hosts = FieldAttribute(isa='list', required=True, ...)
vars = FieldAttribute(isa='dict', priority=100, default=dict)
gather_facts = FieldAttribute(isa='bool', default=...)
gather_subset = FieldAttribute(isa='list', default=...)
gather_timeout = FieldAttribute(isa='int', default=...)
serial = FieldAttribute(isa='list', default=lambda: [], priority=1, ...)
strategy = FieldAttribute(isa='string', default=C.DEFAULT_STRATEGY)
order = FieldAttribute(isa='string', choices=['inventory', 'reverse_inventory', 'sorted', 'reverse_sorted', 'shuffle'])
roles = FieldAttribute(isa='list', default=list, priority=90, ...)
tasks = FieldAttribute(isa='list', default=list, ...)
pre_tasks = FieldAttribute(isa='list', default=list, ...)
post_tasks = FieldAttribute(isa='list', default=list, ...)
handlers = FieldAttribute(isa='list', default=list, ...)
vars_files = FieldAttribute(isa='list', default=list, ...)
become = FieldAttribute(isa='bool', ...)
become_user = FieldAttribute(isa='string', ...)
become_method = FieldAttribute(isa='string', default=...)
connection = FieldAttribute(isa='string', ...)
remote_user = FieldAttribute(isa='string', ...)
no_log = FieldAttribute(isa='bool', default=False)
force_handlers = FieldAttribute(isa='bool', default=...)
max_fail_percentage = FieldAttribute(isa='percent', default=0)
any_errors_fatal = FieldAttribute(isa='bool', default=...)
fact_path = FieldAttribute(isa='string', ...)

Each FieldAttribute has a type (isa), a default, optional choices, and a priority that affects load ordering when fields depend on each other.

Compilation

Play.compile() produces the flat task list the executor walks. The order is:

  1. meta task: flush_handlers if force_handlers.
  2. Tasks compiled from pre_tasks:.
  3. meta: flush_handlers.
  4. Tasks compiled from roles: (each role's tasks/main.yml, in dependency order).
  5. Tasks compiled from tasks:.
  6. meta: flush_handlers.
  7. Tasks compiled from post_tasks:.
  8. Final meta: flush_handlers.
  9. Handlers from handlers: (only run on notify).

This is what produces the standard "PRE → roles → MAIN → POST → HANDLERS" execution sweep.

Roles vs. tasks

When you list roles in a play, role-level tasks become regular tasks at compile time. The role importer is what makes that work; see Role and handler. Pre-flush, the executor doesn't see roles — it sees a flat task list that already has role tasks merged in.

Hosts patterns and serial

The hosts: field is a host pattern — a string or list of patterns. Play._get_hosts() (called by PlaybookExecutor) resolves the pattern via InventoryManager.list_hosts(), applies any --limit, and returns the working host list.

serial: controls batching. With serial: 5, the play runs against the first 5 hosts, then the next 5, etc. With a list (serial: [1, 5, "30%"]) it does a "canary then ramp" pattern — 1 host, then 5, then 30% of remaining. Implementation in lib/ansible/executor/playbook_executor.py:PlaybookExecutor.run().

gather_facts

gather_facts: true (default in older versions; per-play config in newer) prepends a gather_facts task to the compiled task list. The action plugin dispatches to the configured fact-gathering modules (default: setup).

PlaybookInclude

import_playbook: (the only static-include form for plays — include_playbook was removed) loads another playbook and splices its plays into the current playbook at parse time.

class PlaybookInclude:
    @staticmethod
    def load(data, basedir, variable_manager, loader):
        if not 'import_playbook' in data and not (any of the legacy spelling):
            return None
        # Load the referenced file
        pb = Playbook.load(playbook_path, ...)
        # Apply when:, vars:, tags: from the include
        return pb

PlaybookInclude wraps the included playbook's plays so they inherit vars:, tags:, and when: from the include statement. The conditional and tag system means a single import-playbook line can be skipped or modified per-run.

PlayContext

PlayContext is a derived per-(host, task) object that carries the resolved connection settings: who to connect as, on which port, with what password, with which become method, with which environment variables. It's not serialized to YAML — it's computed at runtime by combining play options, task options, host vars, and CLI flags.

class PlayContext:
    connection: str
    remote_user: str
    port: int
    become: bool
    become_method: str
    become_user: str
    timeout: int
    private_key_file: str
    password: str
    become_password: str
    diff: bool
    check_mode: bool
    ...

Built fresh by TaskQueueManager for each (host, task) pair. The fields are passed into the connection plugin via its constructor and the action plugin via self._play_context.

Lifecycle

graph TD
    YAML[playbook.yml] --> LOAD[Playbook.load]
    LOAD --> PARSE[For each entry: Play.load or PlaybookInclude.load]
    PARSE --> ENTRIES[Playbook._entries]
    ENTRIES --> EXEC[PlaybookExecutor.run]
    EXEC --> COMPILE[Play.compile]
    COMPILE --> TASKS[Flat list of Task]
    TASKS --> TQM[TQM.run]

Integration points

  • Imported by: lib/ansible/cli/playbook.py, lib/ansible/cli/console.py, lib/ansible/cli/pull.py, lib/ansible/executor/playbook_executor.py.
  • Imports: lib/ansible/playbook/base.py, lib/ansible/playbook/block.py, lib/ansible/playbook/role/__init__.py, lib/ansible/playbook/task.py, lib/ansible/parsing/dataloader.py.

Entry points for modification

  • A new top-level play keyword — declare a new FieldAttribute on Play. Keep version_added accurate. The keyword description in lib/ansible/keyword_desc.yml powers ansible-doc --keyword.
  • Changing pre/post/role orderingPlay.compile(). Sensitive; users assume specific execution order.
  • A new include style for plays — extend PlaybookInclude.load. Note that legacy include_playbook was deliberately removed.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Playbook and play – Ansible wiki | Factory