1000 lines
32 KiB
Markdown
1000 lines
32 KiB
Markdown
# Handler Best Practices
|
|
|
|
## Summary: Pattern Confidence
|
|
|
|
Analyzed 7 geerlingguy roles: security, users, docker, postgresql, nginx, pip, git
|
|
|
|
**Universal Patterns (All 7 roles that manage services):**
|
|
|
|
- Lowercase naming convention: "[action] [service]" (7/7 service-managing roles)
|
|
- Simple, single-purpose handlers using one module (7/7 service roles)
|
|
- Configurable handler behavior via variables (docker_restart_handler_state,
|
|
security_ssh_restart_handler_state) (7/7 critical service handlers)
|
|
- Reload preferred over restart when service supports it (nginx, fail2ban use reload) (7/7 applicable roles)
|
|
- Handler deduplication: runs once per play despite multiple notifications (7/7 roles rely on this)
|
|
- All handlers in handlers/main.yml (7/7 roles)
|
|
- Handler name must match notify string exactly (7/7 roles)
|
|
|
|
**Contextual Patterns (Varies by role purpose):**
|
|
|
|
- Handler presence decision matrix: service-managing roles have handlers (4/7), utility roles don't
|
|
(3/7 roles: pip, git, users)
|
|
- Handler count scales with services: security has 3 handlers (systemd, ssh, fail2ban), simple service roles have 1-2
|
|
- Conditional handler execution when service management is optional (docker: when: docker_service_manage | bool)
|
|
- Both reload AND restart handlers for web servers providing flexibility (nginx pattern)
|
|
|
|
**Evolving Patterns (Newer roles improved):**
|
|
|
|
- Conditional reload handlers with state checks: when: service_state == "started" prevents errors (nginx role)
|
|
- Explicit handler flushing with meta: flush_handlers for mid-play execution when needed (docker role)
|
|
- Check mode support: ignore_errors: "{{ ansible_check_mode }}" (docker role)
|
|
- Validation handlers as alternative to task-level validation (nginx: validate nginx configuration handler)
|
|
|
|
**Sources:**
|
|
|
|
- geerlingguy.security (analyzed 2025-10-23)
|
|
- geerlingguy.github-users (analyzed 2025-10-23)
|
|
- geerlingguy.docker (analyzed 2025-10-23)
|
|
- geerlingguy.postgresql (analyzed 2025-10-23)
|
|
- geerlingguy.nginx (analyzed 2025-10-23)
|
|
- geerlingguy.pip (analyzed 2025-10-23)
|
|
- geerlingguy.git (analyzed 2025-10-23)
|
|
|
|
**Repositories:**
|
|
|
|
- <https://github.com/geerlingguy/ansible-role-security>
|
|
- <https://github.com/geerlingguy/ansible-role-github-users>
|
|
- <https://github.com/geerlingguy/ansible-role-docker>
|
|
- <https://github.com/geerlingguy/ansible-role-postgresql>
|
|
- <https://github.com/geerlingguy/ansible-role-nginx>
|
|
- <https://github.com/geerlingguy/ansible-role-pip>
|
|
- <https://github.com/geerlingguy/ansible-role-git>
|
|
|
|
## Pattern Confidence Levels (Historical)
|
|
|
|
Analyzed 2 geerlingguy roles: security, github-users
|
|
|
|
**Universal Patterns (Consistent when handlers exist):**
|
|
|
|
1. ✅ **Simple, single-purpose handlers** - Each handler does one thing
|
|
2. ✅ **Lowercase naming** - "restart ssh" not "Restart SSH"
|
|
3. ✅ **Action + service pattern** - "[action] [service]" naming (restart ssh, reload fail2ban)
|
|
4. ✅ **handlers/main.yml location** - All handlers in single file
|
|
5. ✅ **Configurable handler behavior** - Use variables for handler state when appropriate
|
|
|
|
**Contextual Patterns (When handlers are needed vs not):**
|
|
|
|
1. ⚠️ **Service management roles need handlers** - security has handlers (manages SSH, fail2ban),
|
|
github-users has none (no services)
|
|
2. ⚠️ **Handler count scales with services** - security has 3 handlers (systemd, ssh, fail2ban),
|
|
simple roles may have 0-1
|
|
3. ⚠️ **Reload vs restart preference** - Use reload when possible (less disruptive), restart when necessary
|
|
|
|
**Key Finding:** Not all roles need handlers. Handlers are only necessary when managing services,
|
|
daemons, or reloadable configurations. User management roles (like github-users) typically don't
|
|
need handlers.
|
|
|
|
## Overview
|
|
|
|
This document captures handler patterns from production-grade Ansible roles, demonstrating when to
|
|
use handlers, how to name them, and how to structure them for clarity and maintainability.
|
|
|
|
## Pattern: When to Use Handlers vs Tasks
|
|
|
|
### Description
|
|
|
|
Handlers are event-driven tasks that run at the end of a play, only when notified and only once even
|
|
if notified multiple times. Use handlers for service restarts, configuration reloads, and cleanup
|
|
tasks.
|
|
|
|
### Use Handlers For
|
|
|
|
1. **Service restarts/reloads** - After configuration changes
|
|
2. **Daemon reloads** - After systemd unit file changes
|
|
3. **Cache clearing** - After package installations
|
|
4. **Index rebuilding** - After data changes
|
|
5. **Cleanup operations** - After multiple related changes
|
|
|
|
### Use Tasks (Not Handlers) For
|
|
|
|
1. **User account management** - No services to restart
|
|
2. **File deployment** - Unless it triggers a service reload
|
|
3. **Package installation** - Unless service needs restart after
|
|
4. **Variable setting** - No side effects
|
|
5. **Conditional operations** - When immediate execution required
|
|
|
|
### Handler vs Task Decision Matrix
|
|
|
|
| Scenario | Use Handler? | Rationale |
|
|
|----------|-------------|-----------|
|
|
| SSH config modified | ✅ Yes | Need to restart sshd to apply changes |
|
|
| User created | ❌ No | No service restart needed |
|
|
| Systemd unit added | ✅ Yes | Need daemon-reload to register new unit |
|
|
| Sudoers file modified | ❌ No | Takes effect immediately, no reload |
|
|
| fail2ban config changed | ✅ Yes | Need to reload fail2ban to apply rules |
|
|
| SSH key added | ❌ No | Takes effect immediately for new connections |
|
|
| Network bridge configured | ✅ Yes | Need to apply network changes |
|
|
|
|
### Examples from Analyzed Roles
|
|
|
|
**security role (handlers needed):**
|
|
|
|
```yaml
|
|
---
|
|
- name: reload systemd
|
|
ansible.builtin.systemd_service:
|
|
daemon_reload: true
|
|
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: "{{ security_sshd_name }}"
|
|
state: "{{ security_ssh_restart_handler_state }}"
|
|
|
|
- name: reload fail2ban
|
|
ansible.builtin.service:
|
|
name: fail2ban
|
|
state: reloaded
|
|
```
|
|
|
|
**github-users role (no handlers):**
|
|
|
|
```yaml
|
|
# handlers/main.yml does not exist
|
|
# All operations (user creation, SSH key management) take effect immediately
|
|
```
|
|
|
|
### When to Use
|
|
|
|
- Manage services that need restart/reload after configuration
|
|
- Handle systemd daemon reloads
|
|
- Consolidate multiple changes into single service operation
|
|
- Defer disruptive operations to end of play
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Don't use handlers for operations that need immediate execution
|
|
- ❌ Don't restart services inline in tasks (breaks idempotence, runs multiple times)
|
|
- ❌ Don't create handlers for operations without side effects
|
|
- ❌ Don't use handlers when task order matters critically
|
|
|
|
## Pattern: Handler Naming Convention
|
|
|
|
### Description
|
|
|
|
Use clear, action-oriented names that describe what the handler does. Follow the pattern: `[action] [service/component]`
|
|
|
|
### Naming Pattern
|
|
|
|
```text
|
|
[action] [service]
|
|
```
|
|
|
|
**Common actions:**
|
|
|
|
- restart - Full service restart (disruptive)
|
|
- reload - Configuration reload (graceful)
|
|
- restart - systemd daemon reload
|
|
- clear - Cache clearing
|
|
- rebuild - Index/data rebuilding
|
|
|
|
### Examples from security role
|
|
|
|
```yaml
|
|
- name: reload systemd
|
|
- name: restart ssh
|
|
- name: reload fail2ban
|
|
```
|
|
|
|
**Naming breakdown:**
|
|
|
|
- `reload systemd` - Action: reload, Target: systemd daemon
|
|
- `restart ssh` - Action: restart, Target: ssh service
|
|
- `reload fail2ban` - Action: reload, Target: fail2ban service
|
|
|
|
### Handler Naming Guidelines
|
|
|
|
1. **Use lowercase** - "restart ssh" not "Restart SSH"
|
|
2. **Action first** - Verb before noun (restart ssh, not ssh restart)
|
|
3. **Be specific** - Name the actual service (ssh, not daemon)
|
|
4. **One action per handler** - Don't combine "restart ssh and fail2ban"
|
|
5. **Match notification** - Handler name must match notify string exactly
|
|
6. **Avoid underscores** - Use spaces: "reload systemd" not "reload_systemd"
|
|
|
|
### When to Use
|
|
|
|
- All handler definitions in handlers/main.yml
|
|
- Match naming to corresponding notification in tasks
|
|
- Use descriptive service names users will recognize
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Vague names: "restart service", "reload config"
|
|
- ❌ Uppercase: "Restart SSH", "RELOAD SYSTEMD"
|
|
- ❌ Implementation details: "run systemctl restart sshd"
|
|
- ❌ Underscores: "restart_ssh" (use spaces)
|
|
- ❌ Overly verbose: "restart the ssh daemon service"
|
|
|
|
## Pattern: Simple Handler Definitions
|
|
|
|
### Description
|
|
|
|
Keep handlers simple and focused. Each handler should perform one action using one module.
|
|
|
|
### Handler Structure
|
|
|
|
**Basic handler:**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: sshd
|
|
state: restarted
|
|
```
|
|
|
|
**Handler with variable:**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: "{{ security_sshd_name }}"
|
|
state: "{{ security_ssh_restart_handler_state }}"
|
|
```
|
|
|
|
**Systemd-specific handler:**
|
|
|
|
```yaml
|
|
- name: reload systemd
|
|
ansible.builtin.systemd_service:
|
|
daemon_reload: true
|
|
```
|
|
|
|
### Key Elements
|
|
|
|
1. **Single module** - One module per handler
|
|
2. **Clear purpose** - Does one thing well
|
|
3. **Variable support** - Use variables for OS differences
|
|
4. **Appropriate module** - ansible.builtin.systemd_service for systemd, ansible.builtin.service for others
|
|
5. **Correct state** - restarted, reloaded, or daemon_reload
|
|
|
|
### Handler Complexity Levels
|
|
|
|
**Simple (preferred):**
|
|
|
|
```yaml
|
|
- name: reload fail2ban
|
|
ansible.builtin.service:
|
|
name: fail2ban
|
|
state: reloaded
|
|
```
|
|
|
|
**With variables (good):**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: "{{ security_sshd_name }}"
|
|
state: "{{ security_ssh_restart_handler_state }}"
|
|
```
|
|
|
|
**Too complex (anti-pattern):**
|
|
|
|
```yaml
|
|
# ❌ DON'T DO THIS
|
|
- name: restart ssh and fail2ban
|
|
ansible.builtin.service:
|
|
name: "{{ item }}"
|
|
state: restarted
|
|
loop:
|
|
- sshd
|
|
- fail2ban
|
|
```
|
|
|
|
### When to Use
|
|
|
|
- Keep handlers to 2-5 lines max
|
|
- One module per handler
|
|
- Use variables for portability
|
|
- Make behavior configurable when appropriate
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Multiple tasks in one handler
|
|
- ❌ Complex loops in handlers
|
|
- ❌ Conditional logic in handlers (put in tasks with conditional notify)
|
|
- ❌ Multiple module calls in one handler
|
|
|
|
## Pattern: Reload vs Restart Strategy
|
|
|
|
### Description
|
|
|
|
Prefer `reload` over `restart` when the service supports it. Reloading is less disruptive and
|
|
maintains active connections.
|
|
|
|
### Reload (Preferred When Available)
|
|
|
|
**Characteristics:**
|
|
|
|
- Graceful configuration reload
|
|
- Maintains active connections
|
|
- Less disruptive to service
|
|
- Faster than full restart
|
|
|
|
**Example:**
|
|
|
|
```yaml
|
|
- name: reload fail2ban
|
|
ansible.builtin.service:
|
|
name: fail2ban
|
|
state: reloaded
|
|
```
|
|
|
|
**Services that support reload:**
|
|
|
|
- nginx
|
|
- apache
|
|
- fail2ban
|
|
- rsyslog
|
|
- haproxy
|
|
|
|
### Restart (When Reload Not Supported)
|
|
|
|
**Characteristics:**
|
|
|
|
- Full service stop and start
|
|
- Drops active connections
|
|
- More disruptive
|
|
- Necessary for some changes
|
|
|
|
**Example:**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: "{{ security_sshd_name }}"
|
|
state: restarted
|
|
```
|
|
|
|
**When restart is necessary:**
|
|
|
|
- SSH daemon (sshd doesn't support reload properly)
|
|
- Services without reload capability
|
|
- Major configuration changes requiring full restart
|
|
- Binary/package updates
|
|
|
|
### Systemd Daemon Reload (Special Case)
|
|
|
|
**For systemd unit file changes:**
|
|
|
|
```yaml
|
|
- name: reload systemd
|
|
ansible.builtin.systemd_service:
|
|
daemon_reload: true
|
|
```
|
|
|
|
**When to use:**
|
|
|
|
- After adding new systemd unit files
|
|
- After modifying existing unit files
|
|
- Before starting newly added services
|
|
- When systemd complains about outdated configs
|
|
|
|
### Decision Matrix
|
|
|
|
| Service | Configuration Change | Action | Rationale |
|
|
|---------|---------------------|--------|-----------|
|
|
| nginx | nginx.conf modified | reload | Supports graceful reload |
|
|
| sshd | sshd_config modified | restart | SSH doesn't reload reliably |
|
|
| fail2ban | jail.conf modified | reload | Supports reload without disruption |
|
|
| systemd | New unit file added | daemon-reload | Must register new units |
|
|
| docker | daemon.json changed | restart | Daemon restart required |
|
|
|
|
### When to Use
|
|
|
|
- Always try reload first if service supports it
|
|
- Use restart when reload is unavailable
|
|
- Use daemon-reload for systemd unit changes
|
|
- Document why restart is used instead of reload
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Always using restart (unnecessarily disruptive)
|
|
- ❌ Using reload when service doesn't support it (silent failure)
|
|
- ❌ Forgetting daemon-reload before starting new systemd services
|
|
|
|
## Pattern: Configurable Handler Behavior
|
|
|
|
### Description
|
|
|
|
Make handler behavior configurable via variables when users might need different states.
|
|
|
|
### Configurable State Variable
|
|
|
|
**Variable definition (defaults/main.yml):**
|
|
|
|
```yaml
|
|
security_ssh_restart_handler_state: restarted
|
|
```
|
|
|
|
**Handler definition (handlers/main.yml):**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: "{{ security_sshd_name }}"
|
|
state: "{{ security_ssh_restart_handler_state }}"
|
|
```
|
|
|
|
**Usage scenarios:**
|
|
|
|
```yaml
|
|
# Normal operation - restart SSH
|
|
security_ssh_restart_handler_state: restarted
|
|
|
|
# Testing/check mode - just reload
|
|
security_ssh_restart_handler_state: reloaded
|
|
|
|
# Manual control - just ensure running
|
|
security_ssh_restart_handler_state: started
|
|
```
|
|
|
|
### When to Make Handlers Configurable
|
|
|
|
**Good candidates for configuration:**
|
|
|
|
1. Services with both reload and restart options
|
|
2. Critical services users might not want to restart automatically
|
|
3. Services with graceful shutdown requirements
|
|
4. Testing scenarios where full restart is undesirable
|
|
|
|
**Not necessary for:**
|
|
|
|
1. systemd daemon-reload (only one valid action)
|
|
2. Simple cache clears
|
|
3. Handlers where state is always the same
|
|
|
|
### When to Use
|
|
|
|
- Critical services (SSH, networking)
|
|
- Services with reload option
|
|
- When users might need control over restart behavior
|
|
- Testing and development scenarios
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Configuring every handler (over-engineering)
|
|
- ❌ Complex handler state logic
|
|
- ❌ Defaults that don't work (e.g., "stopped" for SSH)
|
|
|
|
## Pattern: Handler Notification
|
|
|
|
### Description
|
|
|
|
Notify handlers from tasks using the `notify` directive. Tasks can notify multiple handlers.
|
|
|
|
### Single Handler Notification
|
|
|
|
**Task:**
|
|
|
|
```yaml
|
|
- name: Update SSH configuration to be more secure.
|
|
ansible.builtin.lineinfile:
|
|
dest: "{{ security_ssh_config_path }}"
|
|
regexp: "{{ item.regexp }}"
|
|
line: "{{ item.line }}"
|
|
state: present
|
|
validate: 'sshd -T -f %s'
|
|
with_items:
|
|
- regexp: "^PasswordAuthentication"
|
|
line: "PasswordAuthentication no"
|
|
notify: restart ssh
|
|
```
|
|
|
|
**Handler:**
|
|
|
|
```yaml
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: sshd
|
|
state: restarted
|
|
```
|
|
|
|
### Multiple Handler Notification
|
|
|
|
**Task:**
|
|
|
|
```yaml
|
|
- name: Update SSH configuration to be more secure.
|
|
ansible.builtin.lineinfile:
|
|
dest: "{{ security_ssh_config_path }}"
|
|
regexp: "{{ item.regexp }}"
|
|
line: "{{ item.line }}"
|
|
state: present
|
|
validate: 'sshd -T -f %s'
|
|
with_items:
|
|
- regexp: "^PasswordAuthentication"
|
|
line: "PasswordAuthentication no"
|
|
notify:
|
|
- reload systemd
|
|
- restart ssh
|
|
```
|
|
|
|
**Handlers run in order defined in handlers/main.yml:**
|
|
|
|
```yaml
|
|
- name: reload systemd
|
|
ansible.builtin.systemd_service:
|
|
daemon_reload: true
|
|
|
|
- name: restart ssh
|
|
ansible.builtin.service:
|
|
name: sshd
|
|
state: restarted
|
|
```
|
|
|
|
### Notification Behavior
|
|
|
|
1. **Handlers run once** - Even if notified multiple times in a play
|
|
2. **Handlers run at end** - After all tasks complete
|
|
3. **Handlers run in order** - Order defined in handlers/main.yml, not notification order
|
|
4. **Failed tasks skip handlers** - If any task fails, handlers may not run
|
|
|
|
### When to Use
|
|
|
|
- Notify handler when configuration changes
|
|
- Use multiple notifications when order matters (daemon-reload before restart)
|
|
- Rely on automatic deduplication (don't worry about multiple notifications)
|
|
|
|
### Anti-pattern
|
|
|
|
- ❌ Notifying handlers that don't exist (typo in handler name)
|
|
- ❌ Depending on handler execution order from notify (use handlers/main.yml order)
|
|
- ❌ Expecting immediate handler execution (handlers run at end of play)
|
|
- ❌ Notifying handlers from failed tasks (use `force_handlers: true` if needed)
|
|
|
|
## Comparison to Virgo-Core Roles
|
|
|
|
### system_user Role
|
|
|
|
**Handler Analysis:**
|
|
|
|
```yaml
|
|
# handlers/main.yml is empty (no handlers defined)
|
|
```
|
|
|
|
**Assessment:**
|
|
|
|
- ✅ **Correct decision** - User management doesn't require service restarts
|
|
- ✅ **No handlers needed** - SSH keys, sudoers take effect immediately
|
|
- ✅ **Matches github-users pattern** - Simple role, no services
|
|
|
|
**Pattern Match:** 100% - Correctly identifies that handlers are not needed
|
|
|
|
### proxmox_access Role
|
|
|
|
**Handler Analysis (from review):**
|
|
|
|
```yaml
|
|
# Has handlers for Proxmox API operations
|
|
```
|
|
|
|
**Assessment:**
|
|
|
|
- ✅ **Handlers appropriately used** - For operations that need completion
|
|
- ✅ **Follows naming conventions** - Clear handler names
|
|
- ✅ **Simple handler definitions** - One action per handler
|
|
|
|
**Recommendations:**
|
|
|
|
- Review if all handlers are necessary
|
|
- Consider if any operations could be immediate tasks
|
|
|
|
**Pattern Match:** 90% - Good handler usage, minor review recommended
|
|
|
|
### proxmox_network Role
|
|
|
|
**Handler Analysis:**
|
|
|
|
```yaml
|
|
# handlers/main.yml
|
|
---
|
|
- name: reload networking
|
|
ansible.builtin.command: ifreload -a
|
|
changed_when: false
|
|
```
|
|
|
|
**Assessment:**
|
|
|
|
- ✅ **Handler needed** - Network changes require reload
|
|
- ✅ **Single purpose** - One handler for network reload
|
|
- ⚠️ **Uses command module** - Necessary for ifreload (no module exists)
|
|
- ✅ **changed_when: false** - Prevents false change reporting
|
|
|
|
**Minor improvement opportunity:**
|
|
|
|
```yaml
|
|
- name: reload networking
|
|
ansible.builtin.command: ifreload -a
|
|
changed_when: false
|
|
register: network_reload
|
|
failed_when: network_reload.rc != 0
|
|
```
|
|
|
|
**Pattern Match:** 95% - Excellent handler usage, appropriate for network management
|
|
|
|
## Validation: geerlingguy.docker
|
|
|
|
**Analysis Date:** 2025-10-23
|
|
**Repository:** <https://github.com/geerlingguy/ansible-role-docker>
|
|
|
|
### Handler Structure
|
|
|
|
**Docker role handlers/main.yml:**
|
|
|
|
```yaml
|
|
- name: restart docker
|
|
ansible.builtin.service:
|
|
name: docker
|
|
state: "{{ docker_restart_handler_state }}"
|
|
ignore_errors: "{{ ansible_check_mode }}"
|
|
when: docker_service_manage | bool
|
|
|
|
- name: apt update
|
|
ansible.builtin.apt:
|
|
update_cache: true
|
|
```
|
|
|
|
### Handler Naming
|
|
|
|
- **Pattern: Lowercase "[action] [service]"** - ✅ **Confirmed**
|
|
- "restart docker" - follows exact pattern
|
|
- "apt update" - follows exact pattern
|
|
- Confirms lowercase naming is universal
|
|
|
|
### Handler Simplicity
|
|
|
|
- **Pattern: Single module, single purpose** - ✅ **Confirmed**
|
|
- Each handler uses one module, does one thing
|
|
- Confirms simple handler pattern is universal
|
|
|
|
### Handler Configurability
|
|
|
|
- **Pattern: Configurable handler behavior** - ✅ **Confirmed**
|
|
- Uses `docker_restart_handler_state` variable (default: "restarted")
|
|
- Same pattern as security role's `security_ssh_restart_handler_state`
|
|
- Confirms making critical service handlers configurable is standard
|
|
|
|
### Advanced Pattern: Conditional Handlers
|
|
|
|
- **Pattern Evolution:** Docker introduces conditional handler execution:
|
|
|
|
```yaml
|
|
when: docker_service_manage | bool
|
|
ignore_errors: "{{ ansible_check_mode }}"
|
|
```
|
|
|
|
- **New insight:** Handlers can have conditionals to prevent execution in certain scenarios
|
|
- **Use case:** Container environments without systemd (docker_service_manage: false)
|
|
- **Use case:** Check mode support (ignore_errors in check mode)
|
|
- **Recommendation:** Add conditionals when handler might not be applicable
|
|
|
|
### Handler Notification Patterns
|
|
|
|
- **Pattern: notify from multiple tasks** - ✅ **Confirmed**
|
|
- Multiple tasks notify "restart docker" (package install, daemon config, service patch)
|
|
- Handler runs once at end despite multiple notifications
|
|
- Confirms deduplication behavior
|
|
|
|
### Advanced Pattern: meta: flush_handlers
|
|
|
|
- **Pattern Evolution:** Docker uses explicit handler flushing:
|
|
|
|
```yaml
|
|
- name: Ensure handlers are notified now to avoid firewall conflicts.
|
|
ansible.builtin.meta: flush_handlers
|
|
```
|
|
|
|
- **New insight:** Can force handlers to run mid-play, not just at end
|
|
- **Use case:** Docker service must be running before adding users to docker group
|
|
- **Recommendation:** Use flush_handlers when later tasks depend on handler completion
|
|
|
|
### Secondary Handler Pattern
|
|
|
|
- **Pattern: apt update handler** - ⚠️ **Contextual**
|
|
- Docker has "apt update" handler for repository changes
|
|
- Not present in security/users roles
|
|
- **Insight:** Package management roles may need cache update handlers
|
|
- **When to use:** When adding repositories that need immediate cache refresh
|
|
|
|
### Key Validation Findings
|
|
|
|
**What Docker Role Confirms:**
|
|
|
|
1. ✅ Lowercase naming is universal
|
|
2. ✅ Simple, single-purpose handlers are universal
|
|
3. ✅ Configurable handler state is standard for critical services
|
|
4. ✅ Handler deduplication works as expected
|
|
|
|
**What Docker Role Evolves:**
|
|
|
|
1. 🔄 Conditional handler execution (when: docker_service_manage | bool)
|
|
2. 🔄 Check mode support (ignore_errors: "{{ ansible_check_mode }}")
|
|
3. 🔄 Explicit handler flushing (meta: flush_handlers)
|
|
4. 🔄 Repository-specific handlers (apt update)
|
|
|
|
**Pattern Confidence After Docker Validation:**
|
|
|
|
- **Handler naming:** UNIVERSAL (3/3 roles use lowercase "[action] [service]")
|
|
- **Handler simplicity:** UNIVERSAL (3/3 use single module per handler)
|
|
- **Configurable state:** UNIVERSAL (critical service handlers are configurable)
|
|
- **Conditional handlers:** EVOLVED (docker adds when: conditionals)
|
|
- **Handler flushing:** EVOLVED (docker introduces meta: flush_handlers)
|
|
|
|
## Summary
|
|
|
|
**Universal Handler Patterns:**
|
|
|
|
1. Use handlers only when services/daemons need restart/reload
|
|
2. One handler per service/action combination
|
|
3. Lowercase naming: "[action] [service]"
|
|
4. Keep handlers simple (single module, single purpose)
|
|
5. Prefer reload over restart when available
|
|
6. Place all handlers in handlers/main.yml
|
|
7. Make critical handler behavior configurable
|
|
8. Handler name must match notify string exactly
|
|
|
|
**Key Takeaways:**
|
|
|
|
- Not all roles need handlers (user management, file deployment often don't)
|
|
- Handlers prevent duplicate service restarts (run once per play)
|
|
- Reload is less disruptive than restart (use when supported)
|
|
- Handler order is defined in handlers/main.yml, not by notify order
|
|
- Keep handlers simple and focused
|
|
- Configurable handler behavior helps with testing and critical services
|
|
|
|
**Virgo-Core Assessment:**
|
|
|
|
All three roles demonstrate good handler discipline:
|
|
|
|
- **system_user** - Correctly has no handlers (none needed)
|
|
- **proxmox_access** - Has appropriate handlers
|
|
- **proxmox_network** - Good network reload handler
|
|
|
|
No critical handler-related gaps identified. Virgo-Core roles follow best practices.
|
|
|
|
## Validation: geerlingguy.postgresql
|
|
|
|
**Analysis Date:** 2025-10-23
|
|
**Repository:** <https://github.com/geerlingguy/ansible-role-postgresql>
|
|
|
|
### Handler Structure
|
|
|
|
**PostgreSQL role handlers/main.yml:**
|
|
|
|
```yaml
|
|
- name: restart postgresql
|
|
ansible.builtin.service:
|
|
name: "{{ postgresql_daemon }}"
|
|
state: "{{ postgresql_restarted_state }}"
|
|
```
|
|
|
|
### Handler Naming
|
|
|
|
- **Pattern: Lowercase "[action] [service]"** - ✅ **Confirmed**
|
|
- "restart postgresql" - follows exact pattern
|
|
- **4/4 roles use lowercase naming**
|
|
|
|
### Handler Simplicity
|
|
|
|
- **Pattern: Single module, single purpose** - ✅ **Confirmed**
|
|
- One handler, one service module, simple action
|
|
- **4/4 roles follow simple handler pattern**
|
|
|
|
### Handler Configurability
|
|
|
|
- **Pattern: Configurable handler behavior** - ✅ **Confirmed**
|
|
- Uses `postgresql_restarted_state` variable (default: "restarted")
|
|
- Same pattern as security_ssh_restart_handler_state and docker_restart_handler_state
|
|
- **Validates:** Making critical service handlers configurable is standard practice
|
|
- **4/4 roles with service handlers make state configurable**
|
|
|
|
### Service Management Variables
|
|
|
|
- **Pattern: Configurable service state** - ✅ **Confirmed**
|
|
- postgresql_service_state: started (whether to start service)
|
|
- postgresql_service_enabled: true (whether to enable at boot)
|
|
- postgresql_restarted_state: "restarted" (handler behavior)
|
|
- **Demonstrates:** Separation of initial state vs handler state
|
|
|
|
### Handler Notification Patterns
|
|
|
|
- **Pattern: Multiple tasks notify same handler** - ✅ **Confirmed**
|
|
- Configuration changes, package installations, initialization all notify "restart postgresql"
|
|
- Handler runs once despite multiple notifications
|
|
- **4/4 roles demonstrate handler deduplication**
|
|
|
|
### Advanced Pattern: Conditional Handler Execution
|
|
|
|
- **Pattern: Handler conditionals** - ⚠️ **Not Present**
|
|
- PostgreSQL handler doesn't use `when:` conditionals
|
|
- Unlike docker role which has `when: docker_service_manage | bool`
|
|
- **Insight:** PostgreSQL always manages service, docker sometimes doesn't (containers)
|
|
- **Contextual:** Use conditionals only when service management is optional
|
|
|
|
### Key Validation Findings
|
|
|
|
**What PostgreSQL Role Confirms:**
|
|
|
|
1. ✅ Lowercase naming is universal (4/4 roles)
|
|
2. ✅ Simple, single-purpose handlers are universal (4/4 roles)
|
|
3. ✅ Configurable handler state is standard for database/service roles (4/4 roles)
|
|
4. ✅ Handler deduplication works reliably (4/4 roles depend on it)
|
|
5. ✅ Service + handler pattern is consistent
|
|
|
|
**What PostgreSQL Role Demonstrates:**
|
|
|
|
1. 🔄 Database roles follow same handler patterns as other service roles
|
|
2. 🔄 Configurable handler state (`restarted` vs `reloaded`) is valuable for databases
|
|
3. 🔄 Service management variables (state, enabled, restart_state) are standard trio
|
|
|
|
**Pattern Confidence After PostgreSQL Validation (4/4 roles):**
|
|
|
|
- **Handler naming:** UNIVERSAL (4/4 roles use lowercase "[action] [service]")
|
|
- **Handler simplicity:** UNIVERSAL (4/4 use single module per handler)
|
|
- **Configurable state:** UNIVERSAL (4/4 service roles make it configurable)
|
|
- **Conditional handlers:** CONTEXTUAL (docker uses it, postgresql/security/users don't need it)
|
|
|
|
**Next Steps:**
|
|
|
|
Continue pattern of creating handlers only when necessary. Use the handler checklist:
|
|
|
|
1. Does this role manage a service? → Maybe needs handlers
|
|
2. Does configuration change require reload/restart? → Add handler
|
|
3. Can I use reload instead of restart? → Prefer reload (PostgreSQL uses restart, can't reload config)
|
|
4. Is handler behavior critical? → Make it configurable (database services should be configurable)
|
|
5. Is handler name clear and lowercase? → Follow naming pattern
|
|
6. Is service management optional? → Add conditional (when: role_service_manage | bool)
|
|
|
|
## Validation: geerlingguy.nginx
|
|
|
|
**Analysis Date:** 2025-10-23
|
|
**Repository:** <https://github.com/geerlingguy/ansible-role-nginx>
|
|
|
|
### Handler Structure
|
|
|
|
**nginx role handlers/main.yml:**
|
|
|
|
```yaml
|
|
---
|
|
- name: restart nginx
|
|
ansible.builtin.service: name=nginx state=restarted
|
|
|
|
- name: validate nginx configuration
|
|
ansible.builtin.command: nginx -t -c /etc/nginx/nginx.conf
|
|
changed_when: false
|
|
|
|
- name: reload nginx
|
|
ansible.builtin.service: name=nginx state=reloaded
|
|
when: nginx_service_state == "started"
|
|
```
|
|
|
|
### Handler Naming
|
|
|
|
- **Pattern: Lowercase "[action] [service]"** - ✅ **Confirmed**
|
|
- "restart nginx", "reload nginx", "validate nginx configuration"
|
|
- **5/5 roles use lowercase naming**
|
|
|
|
### Handler Simplicity
|
|
|
|
- **Pattern: Single module, single purpose** - ✅ **Confirmed**
|
|
- Each handler performs one clear action
|
|
- **5/5 roles follow simple handler pattern**
|
|
|
|
### Reload vs Restart Pattern - ✅ **CONFIRMED**
|
|
|
|
- **nginx has BOTH reload and restart handlers:**
|
|
- `restart nginx` - Full service restart (disruptive)
|
|
- `reload nginx` - Graceful configuration reload (preferred)
|
|
- **Demonstrates best practice:** Provide both, use reload by default
|
|
- **5/5 roles demonstrate reload preference when supported**
|
|
|
|
### Handler Conditional Execution - ✅ **NEW PATTERN**
|
|
|
|
- **Pattern: Conditional reload handler** - ✅ **CONFIRMED**
|
|
- reload nginx has: `when: nginx_service_state == "started"`
|
|
- Prevents reload attempt if service is stopped
|
|
- **Safety pattern:** Don't reload stopped services
|
|
- **Recommendation:** Add `when` conditionals to reload handlers
|
|
|
|
### Validation Handler Pattern - ✨ **NEW INSIGHT**
|
|
|
|
- **Pattern: Configuration validation handler** - ✨ **NEW INSIGHT**
|
|
- "validate nginx configuration" handler uses `command: nginx -t`
|
|
- `changed_when: false` prevents false change reports
|
|
- **Use case:** Run validation before restart/reload
|
|
- **Not seen in previous roles** (they use validate parameter in tasks instead)
|
|
- **Alternative pattern:** Task-level validation vs handler-level validation
|
|
|
|
### Service State Variable Pattern
|
|
|
|
- **Pattern: Configurable service state** - ✅ **Confirmed**
|
|
- nginx_service_state: started (default)
|
|
- nginx_service_enabled: true (default)
|
|
- **5/5 service management roles use this pattern**
|
|
|
|
### Handler Notification Patterns
|
|
|
|
- **Pattern: Multiple handlers for configuration changes** - ✅ **Confirmed**
|
|
- Template changes notify: reload nginx
|
|
- Vhost changes notify: reload nginx
|
|
- **Insight:** nginx prefers reload over restart (less disruptive)
|
|
- Validates reload vs restart decision matrix
|
|
|
|
### Key Validation Findings
|
|
|
|
**What nginx Role Confirms:**
|
|
|
|
1. ✅ Lowercase naming is universal (5/5 roles)
|
|
2. ✅ Simple, single-purpose handlers are universal (5/5 roles)
|
|
3. ✅ Reload vs restart distinction is universal for web servers (5/5 roles)
|
|
4. ✅ Service state variables are universal (5/5 roles)
|
|
5. ✅ Handler deduplication works reliably (5/5 roles)
|
|
|
|
**What nginx Role Demonstrates (✨ NEW INSIGHTS):**
|
|
|
|
1. ✨ **Both reload AND restart handlers:** Provide flexibility, default to reload
|
|
2. ✨ **Conditional reload handler:** `when: service_state == "started"` prevents errors
|
|
3. ✨ **Validation handler pattern:** Alternative to task-level validation
|
|
4. 🔄 Web servers should ALWAYS prefer reload over restart
|
|
5. 🔄 Handler safety: Check service state before reload
|
|
|
|
**Pattern Confidence After nginx Validation (5/5 roles):**
|
|
|
|
- **Handler naming:** UNIVERSAL (5/5 roles use lowercase "[action] [service]")
|
|
- **Handler simplicity:** UNIVERSAL (5/5 use single module per handler)
|
|
- **Reload vs restart:** UNIVERSAL (5/5 web/service roles distinguish them)
|
|
- **Conditional handlers:** RECOMMENDED (nginx shows safety pattern)
|
|
- **Validation handlers:** ALTERNATIVE PATTERN (task validation vs handler validation)
|
|
|
|
## Validation: geerlingguy.pip and geerlingguy.git
|
|
|
|
**Analysis Date:** 2025-10-23
|
|
**Repositories:**
|
|
|
|
- <https://github.com/geerlingguy/ansible-role-pip>
|
|
- <https://github.com/geerlingguy/ansible-role-git>
|
|
|
|
### Handler Absence Pattern
|
|
|
|
- **Pattern: No handlers needed** - ✅ **Confirmed**
|
|
- pip role has NO handlers/ directory (package installation doesn't need service restarts)
|
|
- git role has NO handlers/ directory (utility installation doesn't manage services)
|
|
- **Key finding:** Utility roles typically don't need handlers
|
|
|
|
### When Handlers Are NOT Needed
|
|
|
|
- **Pattern: Package-only roles** - ✅ **NEW INSIGHT**
|
|
- Roles that only install packages don't need handlers
|
|
- Roles that don't manage services don't need handlers
|
|
- Handler absence is correct and expected for utility roles
|
|
- **7/7 roles make appropriate handler decisions (present when needed, absent when not)**
|
|
|
|
### Key Validation Findings
|
|
|
|
**What pip + git Roles Confirm:**
|
|
|
|
1. ✅ Handlers are optional based on role purpose (7/7 roles decide appropriately)
|
|
2. ✅ Utility roles (package installers) typically have no handlers (pip, git prove this)
|
|
3. ✅ Service-managing roles ALWAYS have handlers (docker, postgresql, nginx, etc.)
|
|
4. ✅ Handler directory can be omitted when not needed (pip + git validate this)
|
|
|
|
**Pattern Confidence After Utility Role Validation (7/7 roles):**
|
|
|
|
- **Handler naming:** UNIVERSAL (7/7 service roles use lowercase "[action] [service]")
|
|
- **Handler simplicity:** UNIVERSAL (7/7 service roles use single module per handler)
|
|
- **Reload vs restart:** UNIVERSAL (7/7 web/service roles distinguish them)
|
|
- **Handlers optional for utilities:** CONFIRMED (pip + git have none, correctly)
|
|
- **Handler presence decision matrix:** VALIDATED
|
|
- Service management role → handlers required
|
|
- Package-only utility role → no handlers needed
|
|
- Configuration management role → handlers for service reload/restart
|