Ansible basics
Here are a few snippets for common things to automate with Ansible (some of them are only for Fedora), all code examples are taken from my Ansible setup! They could serve as examples to automate simple commands or common operations. In case it is not enough, do a Ctrl+F in the Ansible Index of all modules.
Basics about playbook and tasks
Playbook redaction
Here is a minimal example of a playbook stored in a playbook.yml file here. It has a name, it target only localhost and the single task we gave is the installation of cowsay via DNF (sudo dnf install cowsay). The tasks key is a list of tasks objects. Each task is defined by a name, and usage of a module. Here become: true means that we want to run the task as root.
- name: Personal setup of Fedora
hosts: localhost
tasks:
- name: Install cowsay
become: true
dnf5: name=cowsay
To run it use ansible-playbook with -K (synonym of --ask-become-pass) flag to get asked about sudo password so tasks that run as root do not depend on sudo timeout.
Playbook execution
ansible-playbook playbook.yml -K -v
Here is the approximate (cleaned) output, you can see that cowsay has been installed (btw you can test it with cowsay hey there).
Using /home/sam/.ansible.cfg as config file
BECOME password:
PLAY [Personal setup of Fedora]
TASK [Gathering Facts]
ok: [localhost]
TASK [Install cowsay]
changed: [localhost] => {"changed": true, "msg": "", "rc": 0,
"results": ["Installed: cowsay-3.7.0-12.fc40.noarch"]}
PLAY RECAP
localhost : ok=2 changed=1 unreachable=0 failed=0
skipped=0 rescued=0 ignored=0
You will probably see a warning [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all', you can ignore this or try to disable it.
If you run it again, you will get this output, as you can see the dnf5 module has seen cowsay was already installed and didn't attempted to install it again.
TASK [Install cowsay] ********************************************
ok: [localhost] => {"changed": false, "msg": "Nothing to do", "rc": 0, "results": []}
Task redaction
Here is an example of an Ansible task:
- name: Install flatpak packages
flatpak:
state: present
method: user
name:
- com.bitwarden.desktop
- org.freefilesync.FreeFileSync
Let's dissect it into pieces:
name: The name of the task, shown during execution and in automated docsflatpak: One of the many available modules in the core of Ansible. This modules is able to install/remove/update flatpak packages. Anything inside this object works because the module is expecting those keys... You can refer to the documentation of this module viaansible-doc flatpak.
Some module need to be referred by their full name (including collection name before) that's the case for cargo withcommunity.general.cargo.flatpak.state: the package must be present (contrary tolatestthat indicate it should be present and up-to-date)flatpak.name: either a list of package names or
Packages managers
flatpak
Equivalent of
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Configure flathub as a remote of Flatpak
flatpak_remote:
name: flathub
flatpakrepo_url: https://dl.flathub.org/repo/flathub.flatpakrepo
method: user
Equivalent of...
flatpak install flathub com.bitwarden.desktop org.freefilesync.FreeFileSync -y
# or 2 commands
flatpak install flathub com.bitwarden.desktop -y
flatpak install flathub org.freefilesync.FreeFileSync -y`
- name: Install flatpak packages
flatpak:
state: present
method: user
name:
- com.bitwarden.desktop
- org.freefilesync.FreeFileSync
Equivalent of cargo install cargo-binstall
- name: Install Cargo binstall for faster package install
community.general.cargo:
name: cargo-binstall
dnf
Equivalent of sudo dnf install dnf5 fish -y
- name: Install DNF packages
become: true
dnf5:
state: present # already the default, just for explicitness
name:
- dnf5
- flatpak
For a single DNF package you can use a shorter form because YAML support multiple ways to write the same thing
- name: Install DNF packages
become: true
dnf5: name=python3-pip
pip
Equivalent of pip install ansi2html httpie
- name: Install pip packages
pip:
name:
- ansi2html
- httpie
Configuration files and OS related things
Equivalent of opening nano or vim in sudo to edit /etc/dnf/dnf.conf and a change or add a few lines.
Considering the following state of /etc/dnf/dnf.conf
[main]
gpgcheck=True
installonly_limit=3
clean_requirements_on_remove=True
best=False
We would like to replace installonly_limit=3 by installonly_limit=5 and add 2 new lines at end max_parallel_downloads=10 and fastestmirror=True. We could do it with sed to replace lines using the shell module...
But it's much easier to use the module lineinfile !
- name: Change DNF config for performance optimisations
become: true
lineinfile:
path: /etc/dnf/dnf.conf
state: present
regexp: "{{ item.regex }}"
line: "{{ item.line }}"
loop:
- { line: 'fastestmirror=True', regex: 'fastestmirror=.*' } # Choose fastest available mirror
- { line: 'max_parallel_downloads=10', regex: 'max_parallel_downloads=.*' } # download more packages in parallel (adjust depending on your internet speed, default to 3)
- { line: 'installonly_limit=5', regex: 'installonly_limit=.*' } # The number of kernels to keep before removing, living in 1GB boot partition
Note: If you need to have contiguous lines look at blockinfile, but we don't use here as all lines are independant in this case and some might be already there...
Equivalent of changing /etc/sudoers file with sudo visudo to change sudo timeout to 30 minutes instead of 5. One additionnal challenge is to make sure the file is valid after edition, but not using visudo to edit it. There is actually a trick with validate to run a command that validate the syntax of the file before applying changes. The regexp is a regular expression used to find an existing line to replace instead of just adding it at the end.
- name: Change sudo timeout to 30 minutes
become: true
lineinfile:
path: /etc/sudoers
state: present
regexp: 'Defaults\s+timestamp_timeout=.*'
line: 'Defaults timestamp_timeout=30'
validate: 'visudo -cf %s'
Equivalent of sudo usermod -aG wireshark $USER
- name: Configure wireshark to be able to access network interfaces
become: true
user:
name: "{{ ansible_env.USER }}"
append: true
groups: wireshark
Processes and shell commands
Equivalent of running any command that doesn't have a module, let's take rustup-init -y as an example (it installs all necessary Rust tools).
We can use the command module for this, and make it run once by indicating one file that will be created creates key to avoid running it again when everything is already installed. I just picked rustc binary here as it should be installed after this command (along with cargo, rustfmt, ...).
Command module
- name: Run rustup init to install all useful Rust tools
command:
cmd: rustup-init -y
creates: "{{ ansible_env.HOME }}/.cargo/bin/rustc"
The shorter form is
- name: Run rustup init to install all useful Rust tools
command: rustup-init -y
Warning: commands like cargo build --release && cp target/release/espanso ~/.cargo/bin/espanso will not work because it will directly launch an external process of cargo instead of a shell... In this case the && symbol is thought to be run by a shell. If we need to run multiple commands possibly with some shell specific syntax, we need to use the shell module instead !
Shell module
We can take advantage of the multiline string syntax defined in YAML (with a pipe and any number of lines all indented on the next level)
- name: "Build Espanso from source and install it"
shell: |
cargo build --release
cp target/release/espanso ~/.cargo/bin/espanso
args:
chdir: ~/code/build/espanso
creates: ~/.cargo/bin/espanso
Breaking down a playbook into multiple pieces
When you start dealing with a lot of tasks and dozens of long list of packages, the playbook file can start to become very long... it's time to break things down into multiple files.
NOTE: I have no experience with that actually, it's just my setup that worked for me...
TODO: document what I did... in the meantime have a look at my tasks.md