I am building a playbook to configure my laptop, beginning with the components listed at Setting up Fedora 42 on Librem 14 for personal use.
I use this topic to discuss my system configuration.
I am building a playbook to configure my laptop, beginning with the components listed at Setting up Fedora 42 on Librem 14 for personal use.
I use this topic to discuss my system configuration.
YAML files being with ---, start with that as a best practice.
This playbook will handle all the system software stuff, hence the become: yes.
I’m not sure how to handle some of the tasks for config yet; I setup a lot of personal computers for folks, so I know which packages most of them will use (since they are recommended by me!
).
I’ll start with a general base and see where it gets tricky.
These are the packages I install in Fedora. There are some others, but they require other steps to work, which I’ll address one at a time.
This is where Ansible starts doing stuff to the system. First, update the packages, then install the packages from that list earlier.
So, how do we install 1password?
I’ll think about that while I test the current version. ![]()
Installed from Fedora repos:
maiki@fedora:~$ sudo dnf install ansible
[sudo] password for maiki:
Updating and loading repositories:
Fedora 43 - x86_64 - Updates 100% | 82.7 KiB/s | 29.6 KiB | 00m00s
Repositories loaded.
Package Arch Version Repository Size
Installing:
ansible noarch 11.12.0-1.fc43 updates 376.9 MiB
Installing dependencies:
ansible-core noarch 2.18.11-1.fc43 updates 14.3 MiB
python3-cryptography x86_64 45.0.4-4.fc43 fedora 5.4 MiB
python3-jinja2 noarch 3.1.6-6.fc43 fedora 3.1 MiB
python3-markupsafe x86_64 3.0.2-6.fc43 fedora 61.5 KiB
python3-resolvelib noarch 1.0.1-11.fc43 fedora 92.3 KiB
Transaction Summary:
Installing: 6 packages
Total size of inbound packages is 66 MiB. Need to download 66 MiB.
After this operation, 400 MiB extra will be used (install 400 MiB, remove 0 B).
Is this ok [y/N]: y
[1/6] python3-cryptography-0:45.0.4-4.fc43.x86_64 100% | 2.2 MiB/s | 1.5 MiB | 00m01s
[2/6] python3-jinja2-0:3.1.6-6.fc43.noarch 100% | 5.7 MiB/s | 515.8 KiB | 00m00s
[3/6] python3-resolvelib-0:1.0.1-11.fc43.noarch 100% | 769.8 KiB/s | 46.2 KiB | 00m00s
[4/6] python3-markupsafe-0:3.0.2-6.fc43.x86_64 100% | 479.7 KiB/s | 31.7 KiB | 00m00s
[5/6] ansible-core-0:2.18.11-1.fc43.noarch 100% | 3.1 MiB/s | 3.7 MiB | 00m01s
[6/6] ansible-0:11.12.0-1.fc43.noarch 100% | 25.6 MiB/s | 60.2 MiB | 00m02s
--------------------------------------------------------------------------------------------------------------------------------------------------------------
[6/6] Total 100% | 19.2 MiB/s | 66.0 MiB | 00m03s
Running transaction
[1/8] Verify package files 100% | 29.0 B/s | 6.0 B | 00m00s
[2/8] Prepare transaction 100% | 12.0 B/s | 6.0 B | 00m00s
[3/8] Installing python3-markupsafe-0:3.0.2-6.fc43.x86_64 100% | 3.6 MiB/s | 65.8 KiB | 00m00s
[4/8] Installing python3-jinja2-0:3.1.6-6.fc43.noarch 100% | 114.5 MiB/s | 3.1 MiB | 00m00s
[5/8] Installing python3-resolvelib-0:1.0.1-11.fc43.noarch 100% | 12.2 MiB/s | 100.1 KiB | 00m00s
[6/8] Installing python3-cryptography-0:45.0.4-4.fc43.x86_64 100% | 70.9 MiB/s | 5.5 MiB | 00m00s
[7/8] Installing ansible-core-0:2.18.11-1.fc43.noarch 100% | 28.4 MiB/s | 14.8 MiB | 00m01s
[8/8] Installing ansible-0:11.12.0-1.fc43.noarch 100% | 34.2 MiB/s | 391.9 MiB | 00m11s
Complete!
Ran into indention issues, but worked it out:
maiki@fedora:~/projects/ansible-playbooks$ ansible-playbook personal-computer-config.yml --syntax-check
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
ERROR! 'package_list' is not a valid attribute for a Play
The error appears to be in '/home/maiki/projects/ansible-playbooks/personal-computer-config.yml': line 2, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
---
- name: Personal Computer Configuration
^ here
maiki@fedora:~/projects/ansible-playbooks$ ansible-playbook personal-computer-config.yml --syntax-check
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
playbook: personal-computer-config.yml
maiki@fedora:~/projects/ansible-playbooks$ ansible-playbook personal-computer-config.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Personal Computer Configuration] ***********************************************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************************************************
ok: [localhost]
TASK [Update DNF and upgrade all packages] *******************************************************************************************************************
ok: [localhost]
TASK [Install packages] **************************************************************************************************************************************
changed: [localhost]
PLAY RECAP ***************************************************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ansible has modules to handle those instructions:
And it runs!
maiki@fedora:~/projects/ansible-playbooks$ ansible-playbook personal-computer-config.yml --ask-become-pass
BECOME password:
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [Personal Computer Configuration] ***********************************************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************************************************
ok: [localhost]
TASK [Import 1Password GPG key] ******************************************************************************************************************************
ok: [localhost]
TASK [Add 1Password repository] ******************************************************************************************************************************
changed: [localhost]
TASK [Update DNF and upgrade all packages] *******************************************************************************************************************
ok: [localhost]
TASK [Install packages] **************************************************************************************************************************************
ok: [localhost]
PLAY RECAP ***************************************************************************************************************************************************
localhost : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Let’s see, what other configuration is there?
I can potentially load a custom Firefox profile and load the extension:
- name: Setup Firefox policies
hosts: localhost
become: yes
tasks:
- name: Ensure Firefox policies directory exists
file:
path: /usr/lib/firefox/distribution
state: directory
mode: '0755'
- name: Deploy Firefox policies JSON
copy:
dest: /usr/lib/firefox/distribution/policies.json
content: |
{
"policies": {
"ExtensionSettings": {
"*": {
"blocked_install_message": "Extension installation disabled by policy.",
"install_sources": ["https://addons.mozilla.org/*"],
"installation_mode": "allowed"
},
"2b4e552d544508781d3256909f7e7dd2342dacce@jetpack": {
"installation_mode": "force_installed",
"install_url": "https://addons.mozilla.org/firefox/downloads/file/3751767/1password_x_password_manager-2.0.0.xpi"
}
}
}
}
That’s cool, and I will probably use Firefox policies.json for homelearning as a base for my own setup. ![]()
I wonder if it should just be a file on it’s own, and I reference it from the same repo or whatever, and copy it over. ![]()
This handles the RPM Fusion repos. ![]()
I also had…
- name: Enable openh264 repository
ansible.builtin.shell: >
dnf config-manager setopt fedora-cisco-openh264.enabled=1
Yet that is not idempotent, since it uses the direct shell module. I’m leaving it out for now, but will incorporate it back in somehow.
Because that command may only be changing a single value in a text file, I may be able to use something like:
- name: Ensure openh264 repository is enabled
ansible.builtin.lineinfile:
path: /etc/yum.repos.d/fedora-cisco-openh264.repo
regexp: '^enabled='
line: 'enabled=1'
state: present
The playbook is doing well, but now I need to include some files to be copied over, which means I should start putting this into version control. I will do so, and then continue updating here.
I’ve created the repo on Codeberg:
After reviewing possible ways to divide the tasks and dotfiles into easy to reuse parts, the repo directory will have three top-level directories:
The idea is that each task will handle one concern, such as installing an app or configuring my user directory. That way I can track and edit each concern on it’s own.
Each task will follow a pattern similar to:
Same pattern, but learning more about Ansible, it seems like roles are what I’m thinking of. I’m moving each of my concerns to a series of roles, and then combining them into playbooks.
Here is how I install 1password.
---
- name: Import 1Password GPG key
ansible.builtin.rpm_key:
state: present
key: "https://downloads.1password.com/linux/keys/1password.asc"
- name: Add 1Password repository
ansible.builtin.yum_repository:
name: 1password
description: "1Password Stable Channel"
baseurl: https://downloads.1password.com/linux/rpm/stable/$basearch
gpgcheck: yes
repo_gpgcheck: yes
enabled: yes
gpgkey: https://downloads.1password.com/linux/keys/1password.asc
- name: Install 1Password
ansible.builtin.dnf:
name: 1password
state: present
That is how Get the 1Password for Linux app | 1Password Support describes adding the app, using Ansible’s builtin modules.
I also use the Firefox extension; I will be installing that as part of configuring Firefox.
I have a role for RPM Fusion:
---
# Ensure RPM Fusion repositories are available for additional packages
- name: Add RPM Fusion Free and Nonfree Repositories
ansible.builtin.dnf:
name:
- "https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-{{ ansible_facts['distribution_version'] }}.noarch.rpm"
- "https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-{{ ansible_facts['distribution_version'] }}.noarch.rpm"
state: present
# Enable fedora-cisco-openh264 repository precisely within the correct section
- name: Enable fedora-cisco-openh264 repository
ansible.builtin.lineinfile:
path: /etc/yum.repos.d/fedora-cisco-openh264.repo
regexp: '^enabled=.*'
line: 'enabled=1'
insertafter: '^\[fedora-cisco-openh264\]'
firstmatch: true
backup: yes
# Refresh DNF Metadata
- name: Refresh DNF Metadata
ansible.builtin.dnf:
update_cache: yes
It took me a bit to figure out how to target the specific line in the openh264 repo file. The configuration guide uses a dnf sub-command which is not supported by the module in Ansible. Two modules I try to avoid are command and shell. While quite powerful, they are unable to utilize Ansible to be idempotent (for obvious reasons).
Fortunately the command sudo dnf config-manager setopt fedora-cisco-openh264.enabled=1 effectively sets a value in a text file. The file is complicated by having several sections:
[fedora-cisco-openh264]
name=Fedora $releasever openh264 (From Cisco) - $basearch
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-cisco-openh264-$releasever&arch=$basearch
type=rpm
enabled=1
metadata_expire=14d
repo_gpgcheck=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=True
[fedora-cisco-openh264-debuginfo]
name=Fedora $releasever openh264 (From Cisco) - $basearch - Debug
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-cisco-openh264-debug-$releasever&arch=$basearch
type=rpm
enabled=0
metadata_expire=14d
repo_gpgcheck=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=True
[fedora-cisco-openh264-source]
name=Fedora $releasever openh264 (From Cisco) - $basearch - Source
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-cisco-openh264-source-$releasever&arch=$basearch
type=rpm
enabled=0
metadata_expire=14d
repo_gpgcheck=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=True
The lineinfile module has a lot of options to help drill down which enabled= I want to edit. And always make backups when editing a file, yo!
The role for taskwarrior is very simple at the moment:
---
# Install Taskwarrior
- name: Install Taskwarrior
ansible.builtin.dnf:
name:
- task
state: present
When it installed it generated configuration at ~/.taskrc:
# [Created by task 3.4.1 12/15/2025 13:31:56]
data.location=/home/maiki/.task
news.version=3.4.1
# To use the default location of the XDG directories,
# move this configuration file from ~/.taskrc to ~/.config/task/taskrc and update location config as follows:
#data.location=~/.local/share/task
#hooks.location=~/.config/task/hooks
# Color theme (uncomment one to use)
#include light-16.theme
#include light-256.theme
#include bubblegum-256.theme
#include dark-16.theme
#include dark-256.theme
#include dark-red-256.theme
#include dark-green-256.theme
#include dark-blue-256.theme
#include dark-violets-256.theme
#include dark-yellow-green.theme
#include dark-gray-256.theme
#include dark-gray-blue-256.theme
#include solarized-dark-256.theme
#include solarized-light-256.theme
#include no-color.theme
I needed to check where the themes were installed:
maiki@frame:~$ rpm -ql task
/usr/bin/task
/usr/lib/.build-id
/usr/lib/.build-id/a8
/usr/lib/.build-id/a8/449b6841f5234e598d57cf8040b05012a5f542
/usr/share/bash-completion/completions
/usr/share/bash-completion/completions/task
/usr/share/doc/task
/usr/share/doc/task/AUTHORS
/usr/share/doc/task/COPYING
/usr/share/doc/task/ChangeLog
/usr/share/doc/task/README.md
/usr/share/doc/task/scripts
/usr/share/doc/task/scripts/add-ons
/usr/share/doc/task/scripts/add-ons/README
/usr/share/doc/task/scripts/add-ons/update-holidays.pl
/usr/share/doc/task/scripts/hooks
/usr/share/doc/task/scripts/hooks/README
/usr/share/doc/task/scripts/hooks/on-add
/usr/share/doc/task/scripts/hooks/on-add.the
/usr/share/doc/task/scripts/hooks/on-exit
/usr/share/doc/task/scripts/hooks/on-exit.shadow-file
/usr/share/doc/task/scripts/hooks/on-launch
/usr/share/doc/task/scripts/hooks/on-modify
/usr/share/doc/task/scripts/vim
/usr/share/doc/task/scripts/vim/README
/usr/share/doc/task/scripts/vim/ftdetect
/usr/share/doc/task/scripts/vim/ftdetect/task.vim
/usr/share/doc/task/scripts/vim/syntax
/usr/share/doc/task/scripts/vim/syntax/taskdata.vim
/usr/share/doc/task/scripts/vim/syntax/taskedit.vim
/usr/share/doc/task/scripts/vim/syntax/taskrc.vim
/usr/share/doc/task/task-ref.pdf
/usr/share/fish/vendor_completions.d
/usr/share/fish/vendor_completions.d/task.fish
/usr/share/licenses/task
/usr/share/licenses/task/LICENSE
/usr/share/licenses/task/LICENSE.dependencies
/usr/share/licenses/task/cargo-vendor.txt
/usr/share/man/man1/task.1.gz
/usr/share/man/man5/task-color.5.gz
/usr/share/man/man5/task-sync.5.gz
/usr/share/man/man5/taskrc.5.gz
/usr/share/task
/usr/share/task/bubblegum-256.theme
/usr/share/task/dark-16.theme
/usr/share/task/dark-256.theme
/usr/share/task/dark-blue-256.theme
/usr/share/task/dark-gray-256.theme
/usr/share/task/dark-gray-blue-256.theme
/usr/share/task/dark-green-256.theme
/usr/share/task/dark-red-256.theme
/usr/share/task/dark-violets-256.theme
/usr/share/task/dark-yellow-green.theme
/usr/share/task/holidays.cs-CZ.rc
/usr/share/task/holidays.da-DK.rc
/usr/share/task/holidays.de-AT.rc
/usr/share/task/holidays.de-BE.rc
/usr/share/task/holidays.de-CH.rc
/usr/share/task/holidays.de-DE.rc
/usr/share/task/holidays.el-GR.rc
/usr/share/task/holidays.en-CA.rc
/usr/share/task/holidays.en-GB.rc
/usr/share/task/holidays.en-NZ.rc
/usr/share/task/holidays.en-US.rc
/usr/share/task/holidays.es-CO.rc
/usr/share/task/holidays.es-ES.rc
/usr/share/task/holidays.es-US.rc
/usr/share/task/holidays.fi-FI.rc
/usr/share/task/holidays.fr-BE.rc
/usr/share/task/holidays.fr-CA.rc
/usr/share/task/holidays.fr-FR.rc
/usr/share/task/holidays.hr-HR.rc
/usr/share/task/holidays.hu-HU.rc
/usr/share/task/holidays.is-IS.rc
/usr/share/task/holidays.it-IT.rc
/usr/share/task/holidays.nb-NO.rc
/usr/share/task/holidays.nl-BE.rc
/usr/share/task/holidays.nl-NL.rc
/usr/share/task/holidays.pl-PL.rc
/usr/share/task/holidays.por-PRT.rc
/usr/share/task/holidays.pt-BR.rc
/usr/share/task/holidays.pt-PT.rc
/usr/share/task/holidays.ru-RU.rc
/usr/share/task/holidays.sk-SK.rc
/usr/share/task/holidays.sv-FI.rc
/usr/share/task/holidays.sv-SE.rc
/usr/share/task/holidays.tr-TR.rc
/usr/share/task/light-16.theme
/usr/share/task/light-256.theme
/usr/share/task/no-color.theme
/usr/share/task/solarized-dark-256.theme
/usr/share/task/solarized-light-256.theme
/usr/share/zsh
/usr/share/zsh/site-functions
/usr/share/zsh/site-functions/_task
So I update the line in my config:
# Color theme (uncomment one to use)
#include light-16.theme
#include light-256.theme
#include bubblegum-256.theme
#include dark-16.theme
#include dark-256.theme
#include dark-red-256.theme
#include dark-green-256.theme
#include dark-blue-256.theme
#include dark-violets-256.theme
#include dark-yellow-green.theme
include /usr/share/task/dark-gray-256.theme
#include dark-gray-blue-256.theme
#include solarized-dark-256.theme
#include solarized-light-256.theme
#include no-color.theme
My former .taskrc is archived somewhere, but I’m going to start over now to refresh myself on what each setting means.
I’ve been learning more about Ansible, and this approach to system configuration greatly interests me.
I think for this initial pass I will focus on Fedora Linux specific configurations, along with my personal dotfiles used for copying.
However, I can imagine wanting to really delve into each of these roles for high-level agnostic system configuration, with defaults and overrides that allow building a playbook from a conventional collection of roles. ![]()
Ansible solves a problem I’ve been personally dealing with since using computers, but I did not have the concepts in place to understand: how do I configure a system in an idempotent, declarative way?
I’m going to get into why I want that, but first I’m wondering why I never sought this out. ![]()
My personal computing is powerful, both in resources and knowledge. Yet, it does not seem to be simple nor easy enough to compel new users, today. The shift to ubiquitous internet clients (a critical mass of users with smartphones) was severe enough, where many folks today do not know where their personal files exist; now the marketing machine is trying to profit before the AI bubble pops and the hucksters want you to forget about any other digital technology.
It means I’ve felt less connected to personal computer users, and my work shaped to assist folks as technology became more complicated.
Recently I’ve been hanging out with the caregivers of teenagers, which for this discussion is strange because COPPA projects this story that 13 year olds are accountable adults and should enter into customer relationships with technology companies. Everyone is worried about what kids have to deal with today, and no one trusts technology companies.
My reaction is the same: yeah, do not trust a company with protecting a child, their values do not align. Instead… and then I enumerate this long list of best practices using technology and very soon eyes are glazing over and I sound really smart techno-babbling at concerned parents. ![]()
Okay, back to declarative system configuration: I know it sounds funny, but this is actually an important part of sharing healthy computing knowledge!
I know how to train people on technology use, yet it does not scale. It requires I participate in a conversation and we learn together. I operate as a knowledgeable facilitator of technology.
I’ve sought easier ways to explain how to configure a system. It is normally a cycle of researching the arcane configuration and commands, explaining it in plain terms, and then doing the thing myself because it was still too complicated for the users.
So I took notes. Lots of notes. I wrote scripts. I explained how it works. I can sense: still complicated.
Okay, so I began looking at the structures of knowledge sharing, how knowledge may be packaged and understood in different, and potentially progressive, ways. Scaffolding.
Ansible is really interesting scaffolding to me. The system is designed to declare many kinds of system configuration, which means it’s has deep complexity. Yet conventions exist to make it easy and simple to use.
Producing reusable community computing code in roles and sharing informative and useful collections means we can produce tooling for more facilitators. We can learn from each other, amplify our collective ability to use technology in a way that is healthy and supportive.
Okay, want to get those thoughts out so I can focus on the geeky parts. ![]()
Okay, while Ansible is capable of managing fleets (and we’ll certainly get there!), my current focus is on user system configuration.
That’s my way of referring to your home directory and files, the apps you use, and how you configure those apps. User system configuration.
I use free and open source operating systems, and mostly use Fedora Linux for personal computing. That means we talk about “dotfiles”, config files that are normally hidden within files and folders whose filenames begin with “.” as a convention.
Fedora Linux will be my primary target, and other Linux/BSD distros will follow. However, macOS and Windows both use basically the same paradigm for user system configuration, and Ansible has builtin modules to handle those OS, so when free software runs on those platforms our roles may adapt to target them; testing may be an issue, but if someone is requesting it we have at least one user to check for us.
When possible, we should use the method for installing and configuring an app that provides the easiest support path for the users. That means we support distros, and follow community guidelines to assist users seeking support. Of course it means we document this for users as well.
Here are some patterns I noticed, and then the software package that exhibits them. This initial pass will be focused on using Fedora Linux.
A lot of software is available in the official repos for Linux distros, and when it is isn’t the software project may provide their own software repo from which to install the software.
This is nice, since the normal system operation will ensure those packages are updated (opposed to downloading a compressed archive of a binary).
Some software is built against the target computer platform and then released as a compressed archive containing the binary file and optional other files such as config or documentation.
That means to install this software we need:
I’m considering using /var/tmp/ as a default archive save location, and ~/.local/bin/ for local user software. I may adapt the roles to install binaries for a system, which might use /opt/ or another sensible path. All these will be default variables, of course.
A configuration file is often included with software. Package managers will copy or generate it, and archives might include example config files to copy and edit.
Some software may be partially or completely configured from a database. These will be provided with software defaults, which may then be configured using a tool that interacts with the database, and sometimes with text files.
Firefox and GNOME (dconf) are examples of having databases for settings, and may also be partially configured via text files.