ansible/ansible
Role and handler
A role bundles tasks, handlers, defaults, vars, files, templates, and meta into a reusable unit. It's both a directory structure on disk and a Python class in lib/ansible/playbook/role/. Handlers are tasks that run only on notify:.
Role on disk
roles/myrole/
├── defaults/
│ └── main.yml # Lowest-precedence variables
├── files/ # Files copyable via copy/template
├── handlers/
│ └── main.yml # Handler tasks
├── meta/
│ └── main.yml # Role dependencies, supported platforms, argument_specs
├── tasks/
│ └── main.yml # The role's task list
├── templates/ # Jinja templates
├── tests/
└── vars/
└── main.yml # Higher-precedence variables (vs defaults)Roles live in:
- A play's
roles:list (relative to the playbook'sroles/directory orANSIBLE_ROLES_PATH). - An installed collection:
<collection>/roles/<name>/.
Role classes
| File | Class | Lines |
|---|---|---|
lib/ansible/playbook/role/__init__.py |
Role |
674 |
lib/ansible/playbook/role/include.py |
RoleInclude |
90 |
lib/ansible/playbook/role/definition.py |
RoleDefinition |
230 |
lib/ansible/playbook/role/metadata.py |
RoleMetadata |
130 |
lib/ansible/playbook/role/requirement.py |
RoleRequirement |
70 |
lib/ansible/playbook/role_include.py |
IncludeRole |
240 |
The class hierarchy is unusually deep because roles support many usage patterns:
- Static
roles:list at play level (Role, applied at play compile time). - Dynamic
include_role:(IncludeRole, applied at task time). - Static
import_role:(compiled in early but as a task, not a play-level role). - Role dependencies declared in another role's
meta/main.yml.
RoleDefinition is the parser; it reads the YAML entry (string short-form or dict long-form), looks up the role on disk, and produces a Role instance.
How a role gets compiled into the play
graph TD
PLAY[play.roles list] --> RD[RoleDefinition.load]
RD --> FIND[Locate role on disk]
FIND --> META[Read meta/main.yml]
META --> DEPS[Recursively load dependencies]
DEPS --> ROLE[Role instance]
ROLE --> COMPILE[Role.compile]
COMPILE --> TASKS[Tasks list with role context]
TASKS --> SPLICE[Splice into play's compiled task list]Role.compile() returns the role's tasks as a list of Block objects with the role context attached:
- Each task gets a
_rolereference back to the role. - The role's
vars/main.ymlanddefaults/main.ymlare made available as low-precedence vars on the host scope. - Handlers from
handlers/main.ymlare added to the play's handler list.
The _role reference matters because lookup('file', 'foo.txt') from inside a role searches role-relative paths first (<role>/files/foo.txt).
Role dependencies
meta/main.yml can declare role dependencies:
dependencies:
- role: shared.base
- role: webserver
vars:
webserver_port: 8080When loading a role, Role._load_dependencies() recurses through these and produces a flattened, deduplicated list of all role tasks in dependency order. Cycles are detected and rejected.
role.allow_duplicates: yes in meta/main.yml lets a role be applied multiple times (with different vars per application) — useful for "create a user" or "open a port" roles.
Argument specs for roles
Modern roles can declare an argument spec for include_role/import_role invocations:
# meta/argument_specs.yml
argument_specs:
main:
short_description: Configure the webserver
options:
port:
type: int
required: true
hostname:
type: str
default: localhostThe validation runs via the validate_argument_spec action plugin (lib/ansible/plugins/action/validate_argument_spec.py). When you include_role: name=foo, the argument spec for main is checked first; if invalid, the role doesn't run.
This is the same machinery as module argument validation, but applied to role parameters.
Handlers
Handler is a Task subclass:
class Handler(Task):
listen = FieldAttribute(isa='list', default=list)The only addition is listen:. Handlers go into the play's handlers list (separate from regular tasks). They run when:
- A regular task
notify:s them by name or bylisten:topic. - The strategy reaches an explicit
meta: flush_handlerstask. - The play ends (all queued handlers fire).
Handler ordering
Handler order is the order they're declared in handlers: (or in roles[*].handlers/main.yml), not the order they were notified. If you have:
handlers:
- name: handler-a
debug: msg=A
- name: handler-b
debug: msg=Bthen notify: [handler-b, handler-a] runs handler-a first.
This is sometimes surprising but consistent: handler ordering is your responsibility at definition time.
Handler triggering
Strategy.process_notifications() (lib/ansible/plugins/strategy/__init__.py) tracks per-host notification lists. When a regular task fires notify:, the handler name is added to the host's pending list. At flush time, the strategy iterates the play's handler list, and for each handler that any host has pending, it dispatches that handler against those hosts.
listen: for cross-handler triggering
handlers:
- name: reload nginx
listen: web-changed
...
- name: reload haproxy
listen: web-changed
...notify: web-changed triggers both handlers. The listen: value is also a topic name; multiple handlers can listen on the same topic, and one task can notify: either a handler name or a listen: topic.
force_handlers
By default, if a play has any failed hosts, handlers don't fire on those hosts (the host's task stream stopped). Setting force_handlers: true on the play (or globally with ANSIBLE_FORCE_HANDLERS) makes handlers fire even after a failure — useful for "make sure cleanup happens regardless".
meta: flush_handlers
Forces all pending notifications to fire immediately rather than waiting for play-end. Used to ensure config changes are applied before subsequent tasks depend on them.
include_role and import_role
Two ways to use a role inside a task list rather than at play level:
| Keyword | Static/dynamic | Equivalent of |
|---|---|---|
import_role |
static | Inserting the role's tasks at the import location, at parse time |
include_role |
dynamic | Resolving the role at task execution time, with the current vars |
Use include_role when the role name itself is templated (name: "{{ chosen_role }}") or when you want the role to be re-evaluated per loop iteration. Use import_role when you want consistent tag-application and parse-time behavior.
IncludeRole (the action plugin equivalent) lives in lib/ansible/playbook/role_include.py and the action plugin lib/ansible/plugins/action/include_role.py (actually, this dispatches through the strategy plugin's IncludedFile machinery rather than a regular action). Same for import_role — it's parsed as a task at parse time but resolved by lib/ansible/playbook/helpers.py.
Integration points
- Imported by:
lib/ansible/playbook/play.py:Play._load_roles(), the strategy plugins (forIncludeRole), thevalidate_argument_specaction. - Imports:
lib/ansible/parsing/dataloader.py,lib/ansible/plugins/loader.py,lib/ansible/playbook/role/{definition,metadata,include,requirement}.py.
Entry points for modification
- A new role-level keyword — declare it in
RoleorRoleMetadataas appropriate. Update the role schema docs. - Argument-spec semantics —
lib/ansible/plugins/action/validate_argument_spec.pyand the role'smeta/argument_specs.ymlparser. - Handler firing logic —
lib/ansible/plugins/strategy/__init__.py:StrategyBase. This code is intricate; existing tests intest/units/plugins/strategy/are the best reference.
Cross-links
- Playbook and play — the role caller.
- Task and block — what a role compiles into.
- Strategy — what runs handlers.
- Features → Collections — where collections-shipped roles live.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.