Ansible playbook for personal computer 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! :smiley:).

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. :thinking:

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. :smiling_face_with_sunglasses:

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. :thinking:

This handles the RPM Fusion repos. :+1:

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:

  • playbooks
  • tasks
  • dotfiles

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:

  • enable repos/download binaries
  • install/copy to place
  • copy over relevant configuration

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. :thinking:

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. :thinking:

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. :face_exhaling:


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. :slight_smile:

interi org, user apps

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.

patterns for user software

Here are some patterns I noticed, and then the software package that exhibits them. This initial pass will be focused on using Fedora Linux.

package managers

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).

download 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:

  1. download it
  2. save the archive for extraction and idempotency (we do not want to download it if it already exists)
  3. copy the binary onto the user path, with appropriate permissions

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.

default configuration files

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.

default configuration databases

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.