- name: configure system hosts: localhost connection: local become: false tasks: - name: read machine-specific variables include_vars: file: _machines/{{ ansible_hostname }}.yml name: machine tags: - always - name: read variables include_vars: file: variables.yml tags: - always - set_fact: distro: "{{ ansible_distribution|lower }}" tags: - always - name: check for valid distro assert: that: distro in ('ubuntu', 'archlinux') - block: - block: - name: update apt cache apt: update_cache: true become: true changed_when: false - name: upgrade system apt: upgrade: dist become: true - name: remove unused packages apt: autoremove: true become: true - name: clean apt cache apt: autoclean: true become: true when: distro == 'ubuntu' - block: - name: enable multilib repository blockinfile: path: /etc/pacman.conf block: | [multilib] Include = /etc/pacman.d/mirrorlist marker: "# {mark} ANSIBLE MANAGED multilib" become: true - name: enable parallel download blockinfile: path: /etc/pacman.conf insertafter: '\[options\]' block: | ParallelDownloads = 5 marker: "# {mark} ANSIBLE MANAGED parallel_download" become: true - name: upgrade system pacman: upgrade: true update_cache: true become: true changed_when: false - name: install pacman-contrib for paccache package: name: pacman-contrib state: present become: true - name: clean cache command: paccache -rk2 -ruk0 become: true changed_when: false when: distro == 'archlinux' tags: [system-update] - block: - name: install sudo package: state: present name: sudo - name: install dependencies for paru package: state: present name: - base-devel - git become: true - name: create build user on arch user: name: makepkg home: /var/lib/makepkg create_home: true shell: /bin/bash system: true become: true - name: create paru user on arch user: name: paru home: /var/lib/paru create_home: true shell: /bin/bash system: true become: true - name: configure passwordless sudo for paru user copy: owner: root group: root mode: "0600" dest: /etc/sudoers.d/paru content: | paru ALL=(ALL) NOPASSWD: /usr/bin/pacman become: true - name: check if paru is already installed shell: | set -o errexit if pacman -Qi paru-bin >/dev/null 2>&1; then exit 100 fi exit 0 args: executable: /bin/bash changed_when: false check_mode: false failed_when: result.rc not in (0, 100) register: result - name: build paru on arch shell: | set -o errexit mkdir -p /tmp/paru-build cd /tmp/paru-build curl -L -O https://aur.archlinux.org/cgit/aur.git/snapshot/paru-bin.tar.gz tar xvf paru-bin.tar.gz cd paru-bin makepkg args: executable: /bin/bash become: true # do not build as root! become_user: makepkg when: result.rc != 100 - name: install paru shell: | set -o errexit pacman --noconfirm -U /tmp/paru-build/paru-bin/paru-bin-*.pkg.tar.zst rm -rf /tmp/paru-build args: executable: /bin/bash become: true when: result.rc != 100 when: distro == 'archlinux' - block: - name: load package list include_vars: file: packages.yml - name: force-update iptables to iptables-nft on arch shell: pacman -Q iptables && yes | pacman -S iptables-nft changed_when: false become: true when: distro == 'archlinux' - set_fact: defined_packages: "{{ packages|json_query('keys(list)') }}" - set_fact: distro_packages: "{{ packages|json_query('list.*.%s'|format(distro)) }}" - name: check list assert: that: "defined_packages|length == distro_packages|length" - set_fact: defined_packages_remove: "{{ packages|json_query('keys(remove)') }}" - set_fact: distro_packages_remove: "{{ packages|json_query('remove.*.%s'|format(distro)) }}" - name: check list assert: that: "defined_packages_remove|length == distro_packages_remove|length" - name: remove packages package: name: "{{ packages|json_query(query) }}" state: absent become: true vars: query: "{{ 'remove.*.%s[]'|format(distro) }}" when: distro != 'ubuntu' - name: install packages package: name: "{{ packages|json_query(query) }}" state: present become: true vars: query: "{{ 'list.*.%s[]'|format(distro) }}" - name: remove packages apt: name: "{{ packages|json_query(query) }}" state: absent purge: true become: true vars: query: "{{ 'remove.*.%s[]'|format(distro) }}" when: distro == 'ubuntu' - name: install machine-specific packages package: name: "{{ machine.packages }}" state: present when: machine.packages is defined become: true tags: [packages] - block: - name: configure timesyncd on arch copy: owner: root group: root mode: "0644" dest: /etc/systemd/timesyncd.conf content: | [Time] NTP=0.arch.pool.ntp.org 1.arch.pool.ntp.org 2.arch.pool.ntp.org 3.arch.pool.ntp.org FallbackNTP=0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org become: true - name: install lz4 package: name: lz4 state: present become: true - name: use lz4 for mkinitcpio compression lineinfile: path: /etc/mkinitcpio.conf regexp: '^#?COMPRESSION=.*$' line: 'COMPRESSION="lz4"' become: true notify: - rebuild initrd when: distro == 'archlinux' - set_fact: disable_services: - ssh when: distro == 'ubuntu' - set_fact: disable_services: - sshd when: distro == 'archlinux' - name: disable services service: state: stopped enabled: false name: "{{ item }}" with_items: "{{ disable_services }}" become: true when: manage_services|default(true)|bool - set_fact: enable_services: - NetworkManager - docker - libvirtd when: distro == 'ubuntu' - set_fact: enable_services: - NetworkManager - docker - libvirtd - systemd-timesyncd - pcscd when: distro == 'archlinux' - name: enable services service: state: started enabled: true name: "{{ item }}" with_items: "{{ enable_services }}" become: true when: manage_services|default(true)|bool - name: get systemd boot target command: systemctl get-default register: systemd_target changed_when: false check_mode: false - set_fact: default_target: multi-user.target - name: set systemd boot target command: systemctl set-default {{ default_target }} when: systemd_target.stdout != default_target become: true - name: handle lid switch lineinfile: path: /etc/systemd/logind.conf regexp: '^HandleLidSwitch=' line: 'HandleLidSwitch=ignore' become: true - name: handle power key lineinfile: path: /etc/systemd/logind.conf regexp: '^HandlePowerKey=' line: 'HandlePowerKey=suspend' become: true - block: - name: create sudonopw group group: name: sudonopw system: true - name: configure passwordless sudo copy: owner: root group: root mode: "0600" dest: /etc/sudoers.d/sudonopw content: | %sudonopw ALL=(ALL) NOPASSWD: ALL become: true when: distro == 'archlinux' - block: - name: install AMDGPU packages package: name: - mesa - lib32-mesa - xf86-video-amdgpu - vulkan-radeon - lib32-vulkan-radeon - libva-mesa-driver - lib32-libva-mesa-driver - mesa-vdpau - lib32-mesa-vdpau state: present become: true - name: set AMDGPU options copy: owner: root group: root mode: "0600" dest: /etc/X11/xorg.conf.d/20-amdgpu.conf content: | Section "Device" Identifier "AMD" Driver "amdgpu" Option "VariableRefresh" "true" Option "TearFree" "true" EndSection become: true when: - distro == 'archlinux' - machine.gpu is defined and machine.gpu == 'amd' - block: - name: create rust build user user: name: rust_build home: /var/lib/rust_build create_home: true shell: /bin/bash system: true become: true - set_fact: cargo_env: . ~/.cargo/env when: distro == 'ubuntu' - set_fact: # Do NOT just use `"true"`. Due to some YAML fuckery, it will be # capitalized and the commands will fail. cargo_env: "/bin/true" when: distro == 'archlinux' - name: install rustup on ubuntu shell: curl https://sh.rustup.rs -sSf | sh -s -- -y --profile=minimal args: creates: /var/lib/rust_build/.cargo/bin/rustup become: true become_user: rust_build when: distro == 'ubuntu' - name: add rustup stable toolchain shell: "{{ cargo_env }} && rustup toolchain install stable && rustup default stable" become: true become_user: rust_build changed_when: false - name: update rustup stable toolchain shell: "{{ cargo_env }} && rustup update stable" become: true become_user: rust_build changed_when: false - name: add rustup nightly toolchain shell: "{{ cargo_env }} && rustup toolchain install nightly" become: true become_user: rust_build changed_when: false - name: update rustup nightly toolchain shell: "{{ cargo_env }} && rustup update nightly" become: true become_user: rust_build changed_when: false - name: add rustup additional toolchains shell: "{{ cargo_env }} && rustup toolchain install {{ item }}" become: true become_user: rust_build changed_when: false loop: "{{ cargo_crate_list|map(attribute='toolchain', default='none')|reject('in', ['none', 'stable', 'nightly']) }}" - name: update rustup additional toolchains shell: "{{ cargo_env }} && rustup update {{ item }}" become: true become_user: rust_build changed_when: false loop: "{{ cargo_crate_list|map(attribute='toolchain', default='none')|reject('in', ['none', 'stable', 'nightly']) }}" - name: add additional targets shell: "{{ cargo_env }} && rustup +{{ item.toolchain|default('stable') }} target add {{ item.target }}" become: true become_user: rust_build changed_when: false when: - '"target" in item' loop: "{{ cargo_crate_list }}" - name: assert that nofeatures is not actually a flag assert: that: '"nofeatures" not in item.features|default([])' msg: Wow, "nofeatures" is actually a feature. Update the playbook! loop: "{{ cargo_crate_list }}" - name: assert that only proper sources are defined assert: that: item.source|default("crates.io") in ("crates.io", "git") msg: "Invalid source: {{ item.source|default('x') }}" loop: "{{ cargo_crate_list }}" - name: install required packages package: state: present name: "{{ item.required_packages|map(attribute=distro) }}" when: item.required_packages is defined become: true loop: "{{ cargo_crate_list }}" - name: build rust crates from crates.io shell: | set -o errexit {{ cargo_env }} rustup run {{ item.toolchain|default('stable') }} cargo install --features "{{ item.features|default([])|join(' ') }}" {{ "--target " ~ item.target if item.target is defined else '' }} --version {{ item.version }} {{ item.crate }} mv /var/lib/rust_build/.cargo/bin/{{ item.binary }} /var/lib/rust_build/.cargo/bin/{{ binary_id_cratesio }} args: creates: /var/lib/rust_build/.cargo/bin/{{ binary_id_cratesio }} become: true # do not build as root! become_user: rust_build loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'crates.io' - name: build rust crates from git shell: | set -o errexit {{ cargo_env }} rustup run {{ item.toolchain|default('stable') }} cargo install --features "{{ item.features|default([])|join(' ') }}" {{ "--target " ~ item.target if item.target is defined else '' }} --git {{ item.url }} --branch {{ item.branch }} mv /var/lib/rust_build/.cargo/bin/{{ item.binary }} /var/lib/rust_build/.cargo/bin/{{ binary_id_git }} args: creates: /var/lib/rust_build/.cargo/bin/{{ binary_id_git }} become: true # do not build as root! become_user: rust_build loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'git' - name: create target directory file: state: directory path: /usr/local/lib/binaries/ owner: root group: root mode: '0775' become: true - name: move binaries for crates.io shell: | mv /var/lib/rust_build/.cargo/bin/{{ binary_id_cratesio }} /usr/local/lib/binaries/{{ binary_id_cratesio }} ln -s /usr/local/lib/binaries/{{ binary_id_cratesio }} /var/lib/rust_build/.cargo/bin/{{ binary_id_cratesio }} args: creates: /usr/local/lib/binaries/{{ binary_id_cratesio }} become: true loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'crates.io' - name: move binaries for git shell: | mv /var/lib/rust_build/.cargo/bin/{{ binary_id_git }} /usr/local/lib/binaries/{{ binary_id_git }} ln -s /usr/local/lib/binaries/{{ binary_id_git }} /var/lib/rust_build/.cargo/bin/{{ binary_id_git }} args: creates: /usr/local/lib/binaries/{{ binary_id_git }} become: true loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'git' - name: link binaries for crates.io file: src: /usr/local/lib/binaries/{{ binary_id_cratesio }} dest: /usr/local/bin/{{ item.binary }} owner: root group: root state: link force: true become: true loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'crates.io' - name: link binaries for git file: src: /usr/local/lib/binaries/{{ binary_id_git }} dest: /usr/local/bin/{{ item.binary }} owner: root group: root state: link force: true become: true loop: "{{ cargo_crate_list }}" when: item.source|default('crates.io') == 'git' # Important: clean up the symlinks BEFORE the binaries they point to. # Otherwise, ansible will skip them because `ansible.builtin.fileglob` # does not match broken symlinks for some reason. - name: clean up old binaries for creates.io in /var/lib/rust_build command: rm -- {{ binaries | join (" ") }} vars: binaries: "{{ lookup('ansible.builtin.fileglob', '/var/lib/rust_build/.cargo/bin/' ~ item.binary ~ '.*', wantlist=True) | reject('eq', '/var/lib/rust_build/.cargo/bin/' ~ binary_id_cratesio) }}" changed_when: binaries | length > 0 loop: "{{ cargo_crate_list }}" become: true when: - item.source|default('crates.io') == 'crates.io' - binaries | length > 0 - name: clean up old binaries for creates.io in /usr/local/lib command: rm -- {{ binaries | join (" ") }} vars: binaries: "{{ lookup('ansible.builtin.fileglob', '/usr/local/lib/binaries/' ~ item.binary ~ '.*', wantlist=True) | reject('eq', '/usr/local/lib/binaries/' ~ binary_id_cratesio) }}" changed_when: binaries | length > 0 loop: "{{ cargo_crate_list }}" become: true when: - item.source|default('crates.io') == 'crates.io' - binaries | length > 0 - name: clean up old binaries for git in /var/lib/rust_build command: rm -- {{ binaries | join (" ") }} vars: binaries: "{{ lookup('ansible.builtin.fileglob', '/var/lib/rust_build/.cargo/bin/' ~ item.binary ~ '.*', wantlist=True) | reject('eq', '/var/lib/rust_build/.cargo/bin/' ~ binary_id_git) }}" changed_when: binaries | length > 0 loop: "{{ cargo_crate_list }}" become: true when: - item.source|default('crates.io') == 'git' - binaries | length > 0 - name: clean up old binaries for git in /usr/local/lib command: rm -- {{ binaries | join (" ") }} vars: binaries: "{{ lookup('ansible.builtin.fileglob', '/usr/local/lib/binaries/' ~ item.binary ~ '.*', wantlist=True) | reject('eq', '/usr/local/lib/binaries/' ~ binary_id_git) }}" changed_when: binaries | length > 0 loop: "{{ cargo_crate_list }}" become: true when: - item.source|default('crates.io') == 'git' - binaries | length > 0 vars: binary_id_cratesio: "{{ item.binary }}.{{ item.toolchain|default('stable') }}.{{ item.features|default(['nofeatures'])|join('_') }}.{{ item.version }}" binary_id_git: "{{ item.binary }}.{{ item.toolchain|default('stable') }}.{{ item.features|default(['nofeatures'])|join('_') }}.{{ item.branch }}" tags: - rust_binaries - block: - name: stat go target directory stat: path: /usr/local/go-v{{ go_version }} register: go_target_stat - block: - name: create temporary directory for go download tempfile: state: directory register: go_download - name: download go get_url: url: https://golang.org/dl/go{{ go_version }}.linux-amd64.tar.gz dest: "{{ go_download.path }}/go{{ go_version }}.linux-amd64.tar.gz" - name: unpack go unarchive: src: "{{ go_download.path }}/go{{ go_version }}.linux-amd64.tar.gz" owner: root group: root mode: '0755' dest: "{{ go_download.path }}" remote_src: true become: true - name: install new go version command: mv "{{ go_download.path }}/go/" /usr/local/go-v{{ go_version }} become: true - name: clean up go download directory file: path: "{{ go_download.path }}" state: absent when: not go_target_stat.stat.exists - name: link to the current go version file: src: /usr/local/go-v{{ go_version }} dest: /usr/local/go owner: root group: root state: link force: true become: true - name: add go directory to PATH and set GOROOT copy: dest: /etc/profile.d/go.sh content: | export PATH=/usr/local/go/bin:$PATH export GOROOT=/usr/local/go owner: root group: root mode: '0644' become: true tags: [go] - block: - name: install alacritty build dependencies package: state: present # https://github.com/alacritty/alacritty/blob/master/INSTALL.md#debianubuntu name: - cmake - pkg-config - libfreetype6-dev - libfontconfig1-dev - libxcb-xfixes0-dev - libxkbcommon-dev - python3 become: true - name: stat alacritty binary stat: path: /usr/local/bin/alacritty register: alacritty_binary - name: create temporary directory for alacritty build tempfile: state: directory register: alacritty_build_tempdir become: true become_user: rust_build when: not alacritty_binary.stat.exists - name: build alacritty shell: . ~/.cargo/env && rustup run stable cargo install alacritty --root ./out args: chdir: "{{ alacritty_build_tempdir.path }}" become: true become_user: rust_build when: not alacritty_binary.stat.exists - name: install alacritty command: mv "{{ alacritty_build_tempdir.path }}/out/bin/alacritty" /usr/local/bin/alacritty become: true when: not alacritty_binary.stat.exists - name: clean up build directory file: path: "{{ alacritty_build_tempdir.path }}" state: absent become: true become_user: rust_build when: not alacritty_binary.stat.exists when: distro == 'ubuntu' tags: [alacritty] - block: - name: stat yubikey-touch-detector binary stat: path: /usr/local/bin/yubikey-touch-detector register: yubikey_touch_detector_binary - name: create temporary directory for yubikey-touch-detector build tempfile: state: directory register: yubikey_touch_detector_build_tempdir when: not yubikey_touch_detector_binary.stat.exists - name: build yubikey-touch-detector shell: sh -c 'PATH=/usr/local/go/bin:$PATH env GOROOT=/usr/local/go GOPATH=$(pwd) go get -u github.com/maximbaz/yubikey-touch-detector' args: chdir: "{{ yubikey_touch_detector_build_tempdir.path }}" when: not yubikey_touch_detector_binary.stat.exists - name: install yubikey-touch-detector command: > mv "{{ yubikey_touch_detector_build_tempdir.path }}/bin/yubikey-touch-detector" /usr/local/bin/yubikey-touch-detector become: true when: not yubikey_touch_detector_binary.stat.exists - name: clean up build directory file: path: "{{ yubikey_touch_detector_build_tempdir.path }}" state: absent become: true when: not yubikey_touch_detector_binary.stat.exists when: distro == 'ubuntu' tags: [yubikey-touch-detector] - block: - block: - name: add spotify apt key apt_key: url: "https://download.spotify.com/debian/pubkey_5E3C45D7B312C643.gpg" id: "5E3C45D7B312C643" become: true - name: add spotify repository apt_repository: repo: "deb http://repository.spotify.com stable non-free" filename: spotify become: true - name: install spotify apt: name: spotify-client update_cache: true become: true when: distro == 'ubuntu' - block: - name: install spotify from AUR via paru shell: | curl -sS https://download.spotify.com/debian/pubkey_5E3C45D7B312C643.gpg | gpg --import yes 1 | paru --skipreview --aur --batchinstall --noconfirm -S spotify become: true become_user: paru args: creates: /usr/bin/spotify when: distro == 'archlinux' tags: [spotify] - name: create dotfiles group group: name: dotfiles state: present become: true become_user: root - name: create dotfiles user user: name: dotfiles group: dotfiles home: /var/lib/dotfiles create_home: false shell: /bin/bash system: true become: true become_user: root - name: create dotfiles directory file: state: directory path: /var/lib/dotfiles owner: dotfiles group: dotfiles mode: '0775' # group needs write access! become: true become_user: root - name: fix permissions for dotfiles directory shell: | chown --changes --recursive dotfiles:dotfiles /var/lib/dotfiles chmod --changes --recursive g+wX /var/lib/dotfiles register: dotfiles_permission_change become: true become_user: root changed_when: dotfiles_permission_change.stdout_lines|length > 0 - set_fact: users: "{{ machine.users }}" tags: - always - include_tasks: user.yml args: apply: become: true become_user: "{{ user.name }}" with_items: "{{ users }}" no_log: True # less spam loop_control: loop_var: user tags: - always handlers: - name: rebuild initrd command: mkinitcpio -P become: true register: mkinitcpio_cmd failed_when: > mkinitcpio_cmd.rc != 0 and not (mkinitcpio_cmd.rc == 1 and "file not found: `fsck.overlay'" in mkinitcpio_cmd.stderr)