How I Automated Firefox Install Via Ansible


The Problem

Internet browsers are a hotly debated topic with nerds on the internet. I’m not here today to sway your opinion, I just wanted to showcase how I used Ansible to help solve a problem that using Firefox on Debian causes.

output of searchng As you may, or may not know, the default repositories on Debian do not include an up to date firefox. At the time of writing, this version mismatch is not that big. The repos have version 115.3.0, while the current version is 118.0.2. Over time this mismatch becomes huge. I don’t mind “too much” that most of Debian’s software is outdated. But when it comes to my web browser? I don’t want to be behind date at all.

Possible Solutions…

Now there’s a few ways to handle this.

  1. Use the snap
  2. Use the flatpack
  3. Use an alternative package manager, such as guix or nix.
  4. Download & unpack the tar binary

I’m not going to lie, options 1 & 2 are not ideal for me. I do have “some” flatpacks installed, but even those are a compromise. Also I’m currently moving away from Nix for personal reasons I may get into in a future post. I still think Nix is the coolest operating system & way to handle a system - but it’s not mainstream.

For this, I wanted to find a mainstream solution that I could use in the real world. So that meant the solution I devised had to have a few ground rules.

  1. Partially distro agnostic
    1. I should be able to do this method of install on any distro
  2. It should use a tool that’s in use in the real world
  3. It should configure as much of the install as possible without user intervention

This meant one thing for me: ansible. Ansible lets you automate any process easily, and I’ve been dying to use it more to make my understanding of it in a professional setting… well… better!

The Solution

In my attempt to move away from Nix, but hold the principles of system configuration via files true, i opted to do a lot of work setting up my most recent debian install with ansible. The following is just a small sliver of the setup I made, focusing on firefox.

The Project Directory Layout

celer@stinkpad ~/git/debian-ansible-laptop $ tree .
.
├── firefox.yml
├── inventories
│   └── hosts.yml
├── README.md
├── roles
│   ├── firefox
│   │   ├── files
│   │   │   └── firefox.desktop
│   │   └── tasks
│   │       └── main.yml

Before I go into the specifics of commands we should review how the files are laid out here. I think this is important for anyone unfamiliar with Ansible to see to understand what the commands do, and what files they reference.

Inventories

[local-machine]
127.0.0.1 ansible_user=celer ansible_port=22

Firefox.yml

- hosts: local-machine
  become: true
  roles:
    - firefox

Firefox/tasks/main.yml

- name: Ensure pre reqs exist
  apt:
    name:
      - wget
      - tar
      - curl
      - libdbus-glib-1-2 #Another requirement for firefox to even start
      - jq # Needed for getting the latest FF version online

- name: Get latest firefox version number from the net
  shell:
    cmd: curl -s https://product-details.mozilla.org/1.0/firefox_versions.json | jq .LATEST_FIREFOX_VERSION | sed 's/"//g'
  register: online_ff_version

- name: See if firefox is installed
  stat:
    path: "/opt/firefox/firefox"
  register: does_ff_exist

- name: See installed version of firefox version
  shell:
    cmd: "/opt/firefox/firefox --version | sed 's/Mozilla Firefox //g'"
  register: installed_ff_version
  when: does_ff_exist.stat.exists

- name: Define target firefox version
  set_fact:
    firefox_version: "{{ online_ff_version.stdout }}"

- name: Ensure folder exists
  file:
    path: "/opt/firefox"
    state: "directory"
  when: not does_ff_exist.stat.exists

- name: Download firefox
  shell:
    cmd: "wget -O /opt/firefox.tar.bz2 https://download-installer.cdn.mozilla.net/pub/firefox/releases/{{ firefox_version }}/linux-x86_64/en-US/firefox-{{ firefox_version }}.tar.bz2"
  become: yes
  when: "firefox_version  !=  installed_ff_version.stdout"

- name: Unpack firefox
  unarchive:
    src: /opt/firefox.tar.bz2
    dest: /opt
    remote_src: yes
  when: "firefox_version != installed_ff_version.stdout"

- name: Copy firefox desktop entry to proper location
  copy:
    src: ../files/firefox.desktop
    dest: /usr/share/applications/firefox.desktop
  when: "firefox_version != installed_ff_version.stdout"

- name: Remove unwanted files
  file:
    path: /opt/firefox.tar.bz2
    state: absent
  when: "firefox_version != installed_ff_version.stdout"

firefox.desktop

[Desktop Entry]
Type=Application
Name=Firefox
Comment=The Free And Open Internet Browser
Path=/opt/firefox
Exec=env MOZ_ENABLE_WAYLAND=1 /opt/firefox/firefox-bin
Icon=/opt/firefox/browser/chrome/icons/default/default48.png
Terminal=false
Categories=Internet

Files Explained

When we’re in the project’s root directory, we invoke the playlist that orchestrates this all together like so:

ansible-playbook -i inventories/hosts.yml firefox.yml main.yml -K

Let’s break this down.

  1. ansible-playbook is the main command used to invoke an ansible playbook!
  2. main.yml is the said playbook
  3. Inside of main.yml we specify where to run the install (local-machine)
  4. Inside the inventory we define what local-machine is (localhost naturally)
  5. The main.yml says to run the main task in the firefox role.
    1. This is the roles/firefox/tasks/main.yml file
    2. This is also where the bulk of the work is done
  6. The firefox role at one point copies the firefox.desktop to the target destination
    1. This file is needed to start Firefox from typical “start menus” 1

Where the magic happens - the firefox role

The main reason I wanted to make this blogpost was to explain how I solved the problem of only installing firefox if

  1. The program is missing
  2. There is a newer version of firefox

This is both idempotent and more light on resources - I shouldn’t run downloads unless I have to.

First we install pre requirements

- name: Ensure pre reqs exist
  apt:
    name:
      - wget
      - tar
      - curl
      - libdbus-glib-1-2 #Another requirement for firefox to even start
      - jq # Needed for getting the latest FF version online

These are programs needed for both Firefox to start, and for the script to run properly.

- name: Get latest firefox version number from the net
  shell:
    cmd: curl -s https://product-details.mozilla.org/1.0/firefox_versions.json | jq .LATEST_FIREFOX_VERSION | sed 's/"//g'
  register: online_ff_version

Here, we use curl in combination with jq and sed to get the current newest version of firefox. This would print out like 118.0.2. Note the sed statement, that’s because by default it prints like this "118.0.2" which breaks a comparison made with this number later.

- name: See if firefox is installed
  stat:
    path: "/opt/firefox/firefox"
  register: does_ff_exist

- name: See installed version of firefox version
  shell:
    cmd: "/opt/firefox/firefox --version | sed 's/Mozilla Firefox //g'"
  register: installed_ff_version
  when: does_ff_exist.stat.exists

Here we check if firefox already exists on the system. If it does, we also check what our currently installed version is.

- name: Define target firefox version
  set_fact:
    firefox_version: "{{ online_ff_version.stdout }}"

- name: Ensure folder exists
  file:
    path: "/opt/firefox"
    state: "directory"
  when: not does_ff_exist.stat.exists

The first step here just saves the longer variable name (online_ff_version.stdout) to firefox_version for later use. Then we make sure the host folder exists for firefox to be unpacked to if firefox doesn’t already exist

- name: Download firefox
  shell:
    cmd: "wget -O /opt/firefox.tar.bz2 https://download-installer.cdn.mozilla.net/pub/firefox/releases/{{ firefox_version }}/linux-x86_64/en-US/firefox-{{ firefox_version }}.tar.bz2"
  become: yes
  when: "firefox_version  !=  installed_ff_version.stdout"

- name: Unpack firefox
  unarchive:
    src: /opt/firefox.tar.bz2
    dest: /opt
    remote_src: yes
  when: "firefox_version != installed_ff_version.stdout"

Now we get to the work of actually downloading & unpacking the firefox binary - if the version available online is newer

- name: Copy firefox desktop entry to proper location
  copy:
    src: ../files/firefox.desktop
    dest: /usr/share/applications/firefox.desktop
  when: "firefox_version != installed_ff_version.stdout"

- name: Remove unwanted files
  file:
    path: /opt/firefox.tar.bz2
    state: absent
  when: "firefox_version != installed_ff_version.stdout"

Finally we copy over the firefox desktop entry and remove the .tar file left over.

Conclusion

The script above could be optimized down more (I could clean up variable names and standardize how I refer to values that variables hold), but for now this is just fine for me. Hopefully this helps out someone else trying to wrap their mind around how to use ansible properly!

Footnotes


  1. Better known as dmenus, desktop menus, desktop menu entries, etc. ↩︎