Linuxfabrik LFOps
Ansible Collection of roles, playbooks & plugins for Linux-based cloud infrastructure. Covers OS hardening, MariaDB, Icinga2, Nextcloud, FreeIPA, KVM & more. Bitwarden & Cloud integration.
•
made by Linuxfabrik






[](https://galaxy.ansible.com/ui/repo/published/linuxfabrik/lfops/)
[](https://github.com/sponsors/Linuxfabrik)
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7AW3VVX62TR4A&source=url)
# LFOps
LFOps is a comprehensive Ansible Collection providing 145+ playbooks and 160+ roles to bootstrap and manage Linux-based IT infrastructures. It covers the full server lifecycle - from initial provisioning and hardening to application deployment, monitoring, and automated backups. LFOps supports RHEL 8/9/10, Debian, and Ubuntu.
> If these Ansible roles and playbooks help you running your Linux infrastructure, please give it a star.
## Documentation
Full documentation is available at [linuxfabrik.github.io/lfops](https://linuxfabrik.github.io/lfops/). It is automatically built and deployed on every push to `main`.
## Table of Contents
* [Documentation](#documentation)
* [Requirements](#requirements)
* [Installation](#installation)
* [Using ansible-galaxy](#using-ansible-galaxy)
* [Development Setup](#development-setup)
* [Mitogen](#mitogen)
* [Mitogen with ansible-playbook](#mitogen-with-ansible-playbook)
* [Mitogen with ansible-navigator](#mitogen-with-ansible-navigator)
* [Mitogen Compatibility](#mitogen-compatibility)
* [Using ansible-navigator](#using-ansible-navigator)
* [Usage](#usage)
* [Running a Playbook](#running-a-playbook)
* [Typical Workflow Example](#typical-workflow-example)
* [The "all" Playbook](#the-all-playbook)
* [Skipping Roles in a Playbook](#skipping-roles-in-a-playbook)
* [Manual Steps at the End of a Run](#manual-steps-at-the-end-of-a-run)
* [Configuration](#configuration)
* [Recommended ansible.cfg](#recommended-ansiblecfg)
* [Compact Output](#compact-output)
* [Dashboard](#dashboard)
* [LFOps-wide Variables](#lfops-wide-variables)
* [Bitwarden Integration](#bitwarden-integration)
* [Compatibility](#compatibility)
* [Tips, Tricks & Troubleshooting](#tips-tricks--troubleshooting)
* [Contributing](#contributing)
* [License](#license)
## Requirements
* **Ansible**: ansible-core >= 2.16
* **Python on the controller**: 3.10 - 3.12 for ansible-core 2.16; for newer releases see the [ansible-core support matrix](https://docs.ansible.com/projects/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix)
* **Python on managed nodes**: >= 3.6
* **Dependencies**: The collection depends on several community collections (see [`requirements.yml`](requirements.yml)). These are installed automatically when using `ansible-galaxy`.
**Which ansible-core version should I use?**
* If you manage RHEL 8 hosts, use **ansible-core 2.16**. It is the last release that runs modules with the system Python 3.6 of RHEL 8. It is still maintained, since Red Hat supports it for the whole life cycle of RHEL 8.
* Otherwise, use the **latest ansible-core** release.
**Set the Python interpreter of the managed nodes.** Since ansible-core 2.17, Ansible no longer prefers the system Python of the distribution, but the newest `python3.X` from its list of known versions that it finds on the host. Some roles install an additional Python, for example duplicity (Python 3.11 on RHEL 9, Python 3.13 on RHEL 10). From the next run on, Ansible then runs its modules under that Python, which lacks the Python libraries of the distribution, and roles such as kernel_settings, monitoring_plugins and python_venv fail. Pin the system Python in the inventory for all hosts except RHEL 8, where ansible-core 2.16 already picks `/usr/libexec/platform-python`:
```yaml
ansible_python_interpreter: '/usr/bin/python3'
```
Tasks that run on the Ansible controller are not affected by this setting, they use the Python that runs Ansible.
## Installation
### Using ansible-galaxy
```bash
# Install the collection and its dependencies
ansible-galaxy collection install linuxfabrik.lfops
```
To install the dependencies separately:
```bash
ansible-galaxy collection install -r requirements.yml
```
### Development Setup
If you want to contribute or test local changes, clone the repository and symlink it into Ansible's collection path:
```bash
git clone git@github.com:Linuxfabrik/lfops.git /path/to/lfops
mkdir -p ~/.ansible/collections/ansible_collections/linuxfabrik
ln -s /path/to/lfops ~/.ansible/collections/ansible_collections/linuxfabrik/lfops
```
**Important**: The symlink target must be named `lfops` and point to the cloned repository root (which contains the `galaxy.yml`).
To verify the collection is detected:
```bash
ansible-galaxy collection list | grep linuxfabrik
```
## Mitogen
[Mitogen](https://mitogen.networkgenomics.com/ansible_detailed.html) is an optional but recommended performance optimization for Ansible. It replaces the default SSH-based task execution with a persistent Python-to-Python channel, significantly reducing connection overhead and speeding up playbook runs.
### Mitogen with ansible-playbook
Install Mitogen into your Ansible virtual environment:
```bash
pip install mitogen
```
Determine the strategy plugins path (this depends on the Python version in your venv):
```bash
python3 -c "import os, ansible_mitogen; print(os.path.join(ansible_mitogen.__path__[0], 'plugins', 'strategy'))"
```
Add the output to your `ansible.cfg`:
```ini
[defaults]
strategy_plugins = /path/to/venv/lib/pythonX.Y/site-packages/ansible_mitogen/plugins/strategy
strategy = mitogen_linear
```
Since the `strategy_plugins` path contains the Python version, it differs per venv. If you use multiple venvs (e.g. ansible-core 2.16 and the latest release), either use separate `ansible.cfg` files or install Mitogen to a shared location:
```bash
pip install --target=/opt/mitogen mitogen
```
```ini
[defaults]
strategy_plugins = /opt/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linear
```
**Important**: Do not set `ANSIBLE_STRATEGY=mitogen_linear` via shell aliases or environment variables. This is hard to debug and overrides the config file silently. Always configure Mitogen in `ansible.cfg`.
### Mitogen with ansible-navigator
The LFOps [Execution Environment](#using-ansible-navigator) container image already includes Mitogen with the strategy plugins available at `/opt/mitogen/ansible_mitogen/plugins/strategy`. To enable it, add the following to the `ansible.cfg` used by ansible-navigator:
```ini
[defaults]
strategy_plugins = /opt/mitogen/ansible_mitogen/plugins/strategy
strategy = mitogen_linear
```
### Mitogen Compatibility
| ansible-core | Mitogen | Controller Python |
|--------------|----------|-------------------|
| 2.16 | >= 0.3.7 | 3.10 - 3.14 |
| 2.17 | >= 0.3.7 | 3.10 - 3.14 |
| 2.18 | >= 0.3.7 | 3.11 - 3.14 |
Always keep Mitogen up to date (`pip install --upgrade mitogen`). If you encounter `SyntaxError: future feature annotations is not defined`, either the Mitogen version is outdated or the Python version on the remote host is too old (< 3.7).
## Using ansible-navigator
LFOps provides a pre-built Execution Environment (EE) container image that includes all dependencies:
```bash
pip install ansible-navigator
```
Configure `ansible-navigator.yml` in your project directory:
```yaml
ansible-navigator:
execution-environment:
container-engine: podman # or docker
enabled: true
environment-variables:
set:
ANSIBLE_STDOUT_CALLBACK: 'linuxfabrik.lfops.compact'
image: ghcr.io/linuxfabrik/lfops_ee:latest
pull:
policy: missing
mode: stdout
```
`ANSIBLE_STDOUT_CALLBACK` selects the [compact output](#compact-output). ansible-navigator runs the playbook through ansible-runner, which replaces the stdout callback with its own wrapper. That wrapper only builds on the `ANSIBLE_STDOUT_CALLBACK` environment variable and ignores `stdout_callback` in the `ansible.cfg`.
Then run playbooks using `ansible-navigator` instead of `ansible-playbook`:
```bash
ansible-navigator run linuxfabrik.lfops.setup_basic \
--inventory path/to/inventory \
--limit myhost
```
The EE is based on Rocky Linux 9 with ansible-core 2.16, Python 3.11, and includes Mitogen for accelerated execution. See [`execution-environment.yml`](execution-environment.yml) for the full build definition. Public container images are available on [GitHub Container Registry](https://github.com/Linuxfabrik/lfops/pkgs/container/lfops_ee).
## Usage
### Running a Playbook
Each playbook requires the target host to be in a specific inventory group named `lfops_`. For example, to run the `php` playbook against `myhost`:
1. Add the host to the `lfops_php` group in your inventory.
2. Run the playbook:
```bash
ansible-playbook linuxfabrik.lfops.php \
--inventory path/to/inventory \
--limit myhost
```
Use `--diff` to see changes, `--check` for dry-run mode, and `--tags` to run specific parts of a playbook.
### Typical Workflow Example
Add your host to the required groups in the inventory:
```yaml
[lfops_hetzner_vm]
myhost
[lfops_setup_basic]
myhost
[lfops_monitoring_plugins]
myhost
[lfops_setup_nextcloud]
myhost
```
Then run the playbooks in order:
```bash
# Provision the VM
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.hetzner_vm --limit myhost
# Basic setup (hardening, SSH, firewall, chrony, etc.)
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_basic --limit myhost
# Deploy monitoring
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.monitoring_plugins --limit myhost
# Deploy a full application stack (playbooks prefixed by "setup_" include all dependencies)
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_nextcloud --limit myhost
# Change specific settings afterwards, e.g. PHP
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.setup_nextcloud --limit myhost --tags php
```
### The "all" Playbook
The `linuxfabrik.lfops.all` playbook imports all other playbooks. This is useful when a change affects multiple playbooks. For example, to deploy an updated MariaDB dump script to all hosts that have MariaDB (whether installed directly or as part of WordPress, Nextcloud, etc.):
```bash
ansible-playbook --inventory path/to/inventory linuxfabrik.lfops.all --tags mariadb_server --limit myhost
```
**Always** use `--tags` and `--limit` with the `all` playbook.
### Skipping Roles in a Playbook
Each playbook wires in the roles it needs, gated by per-role skip variables. These let you tailor what a playbook runs without editing it:
* Set a skip variable to `true` to **skip** a role the playbook runs by default.
* Set it to `false` to **enable** a role the playbook ships but leaves off by default.
For example, to skip the firewall role in `setup_basic`:
```yaml
setup_basic__skip_firewall: true
```
Every playbook, the roles it runs, and their skip variables (including the defaults) are documented in [playbooks/README.md](playbooks/README.md).
In playbooks that support role injections (like `setup_icinga2_master`), there are two variables:
* `playbook_name__role_name__skip_role`: Skips the role and disables the role's injections. Have a look at the playbook for the default value.
* `playbook_name__role_name__skip_role_injections`: Disables or re-enables the role's injections. Takes priority over `playbook_name__role_name__skip_role`. Defaults to `playbook_name__role_name__skip_role` for ease of use. Have a look at the playbook for the affected injections.
### Manual Steps at the End of a Run
Some changes cannot be completed by Ansible alone, for example a changed kernel command line that only takes effect after a reboot. Where that happens, the role says so where the task runs, and the playbook repeats every such message as one block directly above the `PLAY RECAP`:
```
TASK [linuxfabrik.lfops.shared : Manual steps required] ************************
changed: [myhost] =>
msg:
- 'bootloader: The kernel command line has changed. Please reboot the server manually to apply it.'
```
The block is the summary of what the run left for you to do. It is skipped when a run leaves nothing behind, so an empty run stays quiet. A play that aborts does not reach the block; the message of the role that queued it is still in the output above.
## Configuration
### Recommended ansible.cfg
Place an `ansible.cfg` in your inventory/project directory:
```ini
[defaults]
forks = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = ~/.ansible_cache
fact_caching_timeout = 86400
host_key_checking = False
inventory_ignore_extensions = ~, .orig, .bak, .ini, .cfg, .retry, .pyc, .pyo, .csv, .md
inventory_ignore_patterns = '(host|group)_files'
nocows = 1
retry_files_enabled = False
stdout_callback = linuxfabrik.lfops.compact ; see "Compact Output" section
strategy_plugins = /path/to/ansible_mitogen/plugins/strategy ; see "Mitogen" section
strategy = mitogen_linear
timeout = 15
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
```
### Compact Output
The goal of the `linuxfabrik.lfops.compact` stdout callback is the most compact output without repetitions that still carries every detail a Linux system engineer needs from a run against hundreds or thousands of hosts, so that nobody has to scroll through endless walls of text. The details are the same as in the output of `ansible.posix.debug`: every changed, failed and unreachable host, every diff and every warning.
The plugin keeps the output of large runs readable. Every task is one block: the role and the task name on lines of their own, a progress bar with one asterisk per host result in the color of its status (green ok, yellow changed, cyan skipped, red failed), and one line each for the ok and the skipped hosts. Hosts with the same name prefix are merged in bash brace expansion syntax. Changed hosts are listed with their diff, or, without a diff, with the `msg`, `stdout` and `stderr` of the module, and the warnings with the hosts they occurred on, both grouped by identical text, so a change or warning that affects many hosts appears once. Failures that a role accepts with `ignore_errors` are shown in one `ignored` line per message. At the end of the run, a report in Markdown lists each distinct message once with the hosts it occurred on, in sections by severity (FATAL, UNREACHABLE, WARNING, IGNORED, CHANGED) and grouped by role or playbook:
```text
Playbook: linuxfabrik.lfops.setup_basic
Role: linuxfabrik.lfops.dnf
Task: Set the options in /etc/dnf/dnf.conf
*****
changed: web-03
--- before: /etc/dnf/dnf.conf (content)
+++ after: /etc/dnf/dnf.conf (content)
@@ -4,3 +4,4 @@
clean_requirements_on_remove=True
best=True
skip_if_unavailable=False
+minrate=200k
ok: web-{01,02}, db-01, proxy01
```
Enable it with `stdout_callback = linuxfabrik.lfops.compact` in the `ansible.cfg`, or, with ansible-navigator, with the `ANSIBLE_STDOUT_CALLBACK` environment variable (see [Using ansible-navigator](#using-ansible-navigator)). See `ansible-doc --type callback linuxfabrik.lfops.compact` for details.
### Dashboard
The goal of the `linuxfabrik.lfops.dashboard` stdout callback is a live view of a run, similar to how glances shows the load of a system: always visible, without scrolling, and adapting to the size of the terminal.
It shows the run on one fixed screen that is updated in place instead of scrolling: the current role and task, counters of the results of the current task and of the whole run, and the hosts, sorted by their most severe result (fatal, warning, changed, ignored, ok) and then by name, each with its counts of fatal (F), warning (W) and changed (C) results and its last notable result, marked with the letter of its kind. A host with only ok results shows its last ok result, hosts with only skipped results are counted, not listed:
```text
linuxfabrik.lfops.setup_basic 35 hosts 00:12:34
LFOps main@3e8a7b75, 2 uncommitted ansible-core 2.16.19 mounted from /h/u/lfops
Role: linuxfabrik.lfops.firewall
Task: Deploy the firewall rules
──────────────────────────────────────────────────────────────────────────────────
Task ok 20 changed 3 skipped 5 fatal 1 unreachable 0 pending 6
Run ok 1840 changed 61 warning 12 fatal 1 unreachable 0
──────────────────────────────────────────────────────────────────────────────────
HOST F W C LAST
web-02 1 0 2 F firewall - Deploy the firewall rules: ...
web-01 0 2 5 W lvm - pvcreate / pvresize: Rescan path does not exist ...
db-01 0 0 7 C firewall - Deploy the firewall rules
app-01 0 0 0 O firewall - Deploy the firewall rules
```
At the end of the run, the details follow in the format of the [compact output](#compact-output), but only for the tasks with changes, warnings or failures, then the recap and the report. Without a terminal (log file, pipe, CI), with `--verbose`, or on a terminal smaller than 65 columns and 12 lines, it behaves exactly like `linuxfabrik.lfops.compact`. Enable it like the compact output, with `linuxfabrik.lfops.dashboard` as the stdout callback. See `ansible-doc --type callback linuxfabrik.lfops.dashboard` for details.
### LFOps-wide Variables
It would sometimes be convenient to allow the user to set a default for multiple roles. However, since we strictly prefix all our variables with the role name, this is not that straightforward. Instead, we provide a handful of variables prefixed with `lfops__` that act as the default for multiple roles. It is, of course, still possible to overwrite the LFOps-wide variable with the role-specific one (for example, the value of `icingaweb2_module_director__monitoring_plugins_version` takes precedence over that of `lfops__monitoring_plugins_version`).
#### `lfops__monitoring_plugins_version`
This variable is used as the default whenever the version of the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins) is required. For example, it is used to deploy the correct version of the Director Basket and Grafana Dashboards in the `icingaweb2_module_director` and `icingaweb2_module_grafana` roles, respectively. For documentation of the value, have a look at the `monitoring_plugins__version` variable in the [monitoring_plugins role README](https://github.com/Linuxfabrik/lfops/blob/main/roles/monitoring_plugins/README.md).
```yaml
lfops__monitoring_plugins_version: 'dev'
```
#### `lfops__reboot_now`
Roles do not reboot a host on their own. A change that only takes effect after a reboot (for example a kernel command line deployed by the `bootloader` role) is registered with the [schedule_reboot](https://github.com/Linuxfabrik/lfops/tree/main/roles/schedule_reboot) mechanism, and the host reboots once at its maintenance window, together with every other reason pending by then.
Set this variable to `true` to have the reboot performed during the run instead. It goes through the same mechanism, so the notification mail, the Icinga downtime and the grace period still apply, and Ansible waits for the host to come back before the play continues.
```bash
ansible-playbook linuxfabrik.lfops.bootloader --limit myhost \
--extra-vars='lfops__reboot_now=true'
```
Use `--extra-vars` for a single change that has to be effective right away. Set it in `group_vars` for a group of hosts whose reboots need no window, for example a development environment.
The host needs the `schedule_reboot` role for either path. Every playbook that installs a role able to request a reboot runs it by default. On a host where it is missing, a role that would request a reboot only reports that the operator has to reboot, and with `lfops__reboot_now` set the run aborts rather than reporting a reboot it cannot perform.
#### `lfops__remove_rpmnew_rpmsave`
This variable aims to simplify the management of rpmnew and rpmsave files (and their Debian equivalents) by allowing the admin to remove them with LFOps. The workflow would be to adjust the template in LFOps according to the new config file, then deploy with `--extra-vars='lfops__remove_rpmnew_rpmsave=true'` to update the config and remove the rpmnew / rpmsave in one run.
#### `lfops__repo_basic_auth_login`
This variable is used as the default across all `repo_*` roles if it is set. Can be used to authenticate against the repository server using HTTP basic auth. Have a look at the respective role's README for details.
Note: Currently this only works for RPM repositories.
```yaml
lfops__repo_basic_auth_login:
username: 'mirror-user'
password: 'linuxfabrik'
```
#### `lfops__repo_mirror_url`
This variable is used as the default across all `repo_*` roles if it is set. Can be used to set the URL to a custom mirror server providing the repository. Have a look at the respective role's README for details.
```yaml
lfops__repo_mirror_url: 'https://mirror.example.com'
```
#### `lfops__skip_disk_space_check`
Playbooks and roles with known disk space needs check them before they install anything, and abort naming the filesystem and how much space is missing. Set this variable to `true` if you know that the space suffices.
```bash
ansible-playbook linuxfabrik.lfops.setup_icinga2_master --limit myhost \
--extra-vars='lfops__skip_disk_space_check=true'
```
#### `lfops__skip_restart_handlers`
Set this to `true` to deploy configuration changes without restarting the affected services. Every role's restart handler is skipped, so a config change is written to disk but the running process keeps its old configuration. Reload handlers are not affected, because a reload applies the new configuration without an outage.
```bash
ansible-playbook linuxfabrik.lfops.nextcloud --limit myhost \
--extra-vars='lfops__skip_restart_handlers=true'
```
**The skipped restart is not remembered.** A later ordinary run finds the configuration files already correct, notifies nothing, and therefore does not restart either. The service keeps running its old configuration until you restart it explicitly. Do that by setting the role's service state variable for one run:
```bash
ansible-playbook linuxfabrik.lfops.nextcloud --limit myhost \
--tags nextcloud:state --extra-vars='nextcloud__service_state=restarted'
```
Some roles restart a service and then talk to it in the same run, for example `keycloak` (provisions the bootstrap admin), `influxdb` (creates users and databases) and `opensearch` (runs `securityadmin.sh`). Do not use this variable during an initial installation of those roles.
The `icingadb` role restarts `icinga2`, which it does not own. A restart deferred there cannot be flushed through `icingadb`; use the `icinga2_master` role instead (`--tags icinga2_master:state --extra-vars='icinga2_master__service_state=restarted'`).
### Bitwarden Integration
LFOps includes a Bitwarden lookup plugin that fetches secrets from your vault (and creates items if they do not exist). Requires the [bw CLI](https://bitwarden.com/help/article/cli/) version v2022.9.0+.
Usage example in your inventory:
```yaml
grafana_grizzly__grafana_service_account_login:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': inventory_hostname,
'purpose': 'Grafana Service Account Token',
'username': 'grizzly',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
) }}"
```
Before running Ansible, unlock your vault and start the Bitwarden API server:
```bash
export BW_SESSION="$(bw unlock --raw)"
bw status | jq
bw serve --hostname 127.0.0.1 --port 8087 &
```
The order matters. `bw serve` takes its session from the environment it is started in and keeps it for its lifetime, so exporting `BW_SESSION` afterwards does not reach a running instance. In that case `bw status` reports `unlocked` while the API Ansible talks to is still locked. Check the API itself with `curl --silent http://127.0.0.1:8087/status`, and restart `bw serve` if it disagrees with your shell.
The lookup returns multiple keys, including `username` and `password`. To get only the password:
```yaml
freeipa_server__directory_manager_password:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': inventory_hostname,
'purpose': 'FreeIPA',
'username': 'cn=Directory Manager',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
)['password'] }}"
```
**Note**: When using the lookup in `group_vars`, avoid `inventory_hostname` if you want a shared credential (e.g. one password for all FreeIPA clients). Instead, use the actual server hostname:
```yaml
freeipa_server__ipa_admin_password:
"{{ lookup('linuxfabrik.lfops.bitwarden_item',
{
'hostname': 'freeipa.example.com',
'purpose': 'FreeIPA',
'username': 'admin',
'collection_id': lfops__bitwarden_collection_id,
'organization_id': lfops__bitwarden_organization_id,
},
)['password'] }}"
```
A lookup is evaluated by the templating engine and has no check mode, so a run with `--check` creates a missing item in your vault for real. To forbid that, and to guard against typos in a `hostname` or `purpose` silently minting a second credential, disable item creation for the run:
```bash
LFOPS_BITWARDEN_LOOKUP_ITEM_CREATE=false ansible-playbook linuxfabrik.lfops.setup_nextcloud --limit myhost
```
The lookup then aborts as soon as an item does not exist. The same switch is available as `create` in the `[bitwarden_item_lookup]` section of your `ansible.cfg`.
See `ansible-doc -t lookup linuxfabrik.lfops.bitwarden_item` for all options.
LFOps also ships a small set of custom modules for resource types that are not covered by Ansible core:
* **`bitwarden_item`**: create / update / fetch Bitwarden items.
* **`gpg_key`**: manage GPG keys.
* **`ipa*`** (`ipagroup`, `ipahbacrule`, `ipasudorule`, ...) - FreeIPA resource management.
* **`lvm_pv`**: manage LVM physical volumes.
* **`nextcloud_occ_app`**, **`nextcloud_occ_app_config`**, **`nextcloud_occ_system_config`**: drive Nextcloud `occ` from a playbook.
* **`sqlite_query`**: run SQLite queries.
* **`uptimerobot_*`**: manage UptimeRobot monitors, maintenance windows, public status pages and alert contacts via the [UptimeRobot API v2](https://uptimerobot.com/api/legacy/), with full idempotency, `--check`, `--diff` and a parallel set of `*_info` modules for read-only inspection. See the [`uptimerobot` role README](roles/uptimerobot/) for the full reference.
Each custom module ships its own `DOCUMENTATION`, `EXAMPLES` and `RETURN` blocks; browse them with `ansible-doc linuxfabrik.lfops.`.
## Compatibility
Which Ansible role is proven to run on which OS? See [COMPATIBILITY.md](https://github.com/Linuxfabrik/lfops/blob/main/COMPATIBILITY.md).
## Tips, Tricks & Troubleshooting
**Where should I set `ansible_become: true`?**
* Don't use `become: true` in role playbooks. Instead, set `ansible_become: true` in your group_vars or host_vars only (not in `all.yml` - `localhost` must not be part of the group, otherwise you'll get errors like `sudo: a password is required`).
**How do I find all groups a host belongs to?**
* Run:
```bash
ansible --inventory path/to/inventory myhost -m debug -a "var=group_names"
```
**How do I connect as an unprivileged user?**
* Make sure the user is allowed to switch to all other accounts, not just root. Otherwise tasks using `become_user: 'apache'` etc. will fail. The sudoers entry must use `(ALL)`:
```
ansible-user ALL=(ALL) NOPASSWD: ALL
```
**How do I route a role's outbound traffic through an HTTP proxy?**
* Some roles download installers or contact external APIs directly from the managed node. LFOps does not expose per-role proxy variables. Instead, set the proxy globally on the target in `/etc/environment`; the variables are picked up by tasks running over SSH, including key-based logins:
```
HTTP_PROXY=http://192.0.2.30:8080/
HTTPS_PROXY=http://192.0.2.30:8080/
http_proxy=http://192.0.2.30:8080/
https_proxy=http://192.0.2.30:8080/
```
* Set `NO_PROXY` / `no_proxy` (comma-separated hosts, domains or CIDRs) for destinations that must be reached directly.
**How do I find out which playbooks ran against a host?**
* All playbooks log every run to `/var/log/linuxfabrik-lfops.log` on the target host:
```
2024-05-23 11:15:26.604794 - Playbook linuxfabrik.lfops.apps: START
2024-05-23 11:15:32.877064 - Playbook linuxfabrik.lfops.apps: END
```
**Debian: `No package matching '...' is available`**
* Run `apt update` before running the specific role.
**`[WARNING]: Collection x.y does not support Ansible version 2.16.xx`**
* Install a newer Ansible version and update all collections:
```bash
python3 -m venv ~/venvs/ansible-latest
source ~/venvs/ansible-latest/bin/activate
pip install --upgrade pip
pip install --upgrade ansible-core
ansible-galaxy collection install -r requirements.yml --upgrade
```
**`error creating bridge interface ...: Numerical result out of range`**
* On Linux an interface name must not exceed 15 characters. Choose a shorter bridge name.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
## License
[The Unlicense](https://unlicense.org/)