452 lines
13 KiB
Markdown
452 lines
13 KiB
Markdown
# Ansible-arch ↔ Machine Reconciliation Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Bring the `ansible-arch` repo and the machine it manages back into alignment: commit the in-flight working-tree changes, fix the `become=True` global-default bug that caused root-owned dotfiles symlinks, and codify already-installed-but-undeclared packages into the right roles.
|
|
|
|
**Architecture:** No new files or abstractions. This is a reconciliation pass over existing `roles/*/tasks/*.yml` files and `ansible.cfg`. Every privileged task in the repo already carries an explicit `become: true` — the audit confirmed the *only* structural bug is the global default in `ansible.cfg`, so that fix is a one-line change, not a per-task sweep. Verification is `ansible-playbook --syntax-check` / `--check --diff` where the modules support check mode, and one real `ansible-playbook site.yml` run at the end (safe: pacman tasks are no-ops for already-installed packages, dotfiles-role symlink recreation is idempotent by design).
|
|
|
|
**Tech Stack:** Ansible, Arch Linux (pacman, AUR via `kewlfft.aur` + `paru`), GNU Stow.
|
|
|
|
---
|
|
|
|
### Task 1: Commit the in-flight working-tree diff
|
|
|
|
**Files:**
|
|
- Modify (already modified in working tree, no further edits needed): `dotfiles/hypr/.config/hypr/hyprland.conf`, `roles/ai/tasks/main.yaml`, `roles/common/defaults/main.yml`, `roles/common/tasks/main.yml`, `roles/devops/tasks/main.yml`, `roles/dotfiles/defaults/main.yml`, `roles/dotfiles/tasks/main.yml`, `roles/hyprland/tasks/main.yml`, `roles/multimedia/tasks/main.yml`
|
|
- Add (untracked): `dotfiles/hypr/.config/hypr/hyprlock.conf`, `dotfiles/hypr/.config/hypr/monitors.sh`, `dotfiles/kanshi/`, `dotfiles/starship/`
|
|
|
|
This working-tree diff already reflects the intended current state (verified during brainstorming — it's the hyprland.conf tweaks, new dotfiles stow packages, and new role packages that led to this reconciliation). Nothing to write here, just commit it.
|
|
|
|
- [ ] **Step 1: Review the diff one more time**
|
|
|
|
```bash
|
|
git status
|
|
git diff --stat
|
|
```
|
|
|
|
Expected: the same 9 modified files and 4 untracked paths listed in the design spec's "Context" section.
|
|
|
|
- [ ] **Step 2: Stage and commit**
|
|
|
|
```bash
|
|
git add dotfiles/hypr/.config/hypr/hyprland.conf \
|
|
dotfiles/hypr/.config/hypr/hyprlock.conf \
|
|
dotfiles/hypr/.config/hypr/monitors.sh \
|
|
dotfiles/kanshi/ dotfiles/starship/ \
|
|
roles/ai/tasks/main.yaml \
|
|
roles/common/defaults/main.yml \
|
|
roles/common/tasks/main.yml \
|
|
roles/devops/tasks/main.yml \
|
|
roles/dotfiles/defaults/main.yml \
|
|
roles/dotfiles/tasks/main.yml \
|
|
roles/hyprland/tasks/main.yml \
|
|
roles/multimedia/tasks/main.yml
|
|
git commit -m "feat: formalize in-flight hyprland/dotfiles/role changes"
|
|
git status
|
|
```
|
|
|
|
Expected: `git status` shows "nothing to commit, working tree clean" (modulo files touched by later tasks).
|
|
|
|
---
|
|
|
|
### Task 2: Fix the `become=True` global default
|
|
|
|
**Files:**
|
|
- Modify: `ansible.cfg:11-15`
|
|
|
|
**Context:** `ansible.cfg` currently has:
|
|
|
|
```ini
|
|
[privilege_escalation]
|
|
become = True
|
|
become_method = sudo
|
|
become_user = root
|
|
become_ask_pass = False
|
|
```
|
|
|
|
Every task across every role that actually needs root (`pacman:`, `systemd:`, `user:`, `getent:`, `chsh`) already has an explicit `become: true` set on it — confirmed by reading every `roles/*/tasks/*.yml` file. Every task that must run unprivileged (AUR installs via `kewlfft.aur.aur`, the `paru` makepkg bootstrap, `oh-my-zsh` install, dotfiles `stow`/`file` tasks, `blockinfile` edits to `.zshrc`/`.profile`) already correctly omits `become`. The global `become = True` default was silently overriding all of those correctly-unprivileged tasks to root — that's why `~/.config/hypr`, `~/.config/zellij`, and `~/.config/starship.toml` symlinks are owned by `root`.
|
|
|
|
So the fix is exactly one line.
|
|
|
|
- [ ] **Step 1: Remove the global become default**
|
|
|
|
Edit `ansible.cfg`, changing:
|
|
|
|
```ini
|
|
[privilege_escalation]
|
|
become = True
|
|
become_method = sudo
|
|
become_user = root
|
|
become_ask_pass = False
|
|
```
|
|
|
|
to:
|
|
|
|
```ini
|
|
[privilege_escalation]
|
|
become_method = sudo
|
|
become_user = root
|
|
become_ask_pass = False
|
|
```
|
|
|
|
- [ ] **Step 2: Syntax-check the playbook**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --syntax-check
|
|
```
|
|
|
|
Expected: `playbook: site.yml` with no errors.
|
|
|
|
- [ ] **Step 3: Dry-run the parts that support check mode**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --check --diff
|
|
```
|
|
|
|
Expected: pacman/systemd/user tasks report no changes (packages already installed, services already enabled). The `stow`/`command` tasks in the dotfiles role will show as `skipped` (partial check-mode support) rather than simulate the symlink recreation — that's expected and covered by the real run in Task 6.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add ansible.cfg
|
|
git commit -m "fix: stop forcing become=True globally, rely on per-task become"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Add missing packages to the `ctf` role
|
|
|
|
**Files:**
|
|
- Modify: `roles/ctf/tasks/main.yml`
|
|
|
|
Current content:
|
|
|
|
```yaml
|
|
# tasks file for ctf
|
|
- name: Install CTF tools (official repos)
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- wireshark-qt
|
|
- nmap
|
|
- openbsd-netcat
|
|
- binwalk
|
|
- gobuster
|
|
state: present
|
|
```
|
|
|
|
`gitleaks`, `picocom`, and `python-pyserial` are official-repo packages. `nuclei-bin` is AUR-only.
|
|
|
|
- [ ] **Step 1: Add the official-repo packages**
|
|
|
|
Edit `roles/ctf/tasks/main.yml` to:
|
|
|
|
```yaml
|
|
# tasks file for ctf
|
|
- name: Install CTF tools (official repos)
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- wireshark-qt
|
|
- nmap
|
|
- openbsd-netcat
|
|
- binwalk
|
|
- gobuster
|
|
- gitleaks
|
|
- picocom
|
|
- python-pyserial
|
|
state: present
|
|
|
|
- name: Install CTF AUR packages
|
|
kewlfft.aur.aur:
|
|
name: "{{ item }}"
|
|
use: "{{ aur_helper | default('paru') }}"
|
|
state: present
|
|
loop:
|
|
- nuclei-bin
|
|
```
|
|
|
|
- [ ] **Step 2: Syntax-check**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --syntax-check
|
|
```
|
|
|
|
Expected: no errors.
|
|
|
|
- [ ] **Step 3: Dry-run just this role**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --check --diff --tags ctf
|
|
```
|
|
|
|
Expected: `pacman` task reports `ok` (all packages already installed, since the audit confirmed they're present via `pacman -Qeq`). The AUR task will show as `skipped` in check mode (module doesn't support check mode) — that's fine, it's covered by the Task 6 real run.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add roles/ctf/tasks/main.yml
|
|
git commit -m "feat(ctf): add gitleaks, nuclei-bin, picocom, python-pyserial"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Add missing packages to the `common` role
|
|
|
|
**Files:**
|
|
- Modify: `roles/common/tasks/main.yml:19-53`
|
|
|
|
Current "Install base development tools" task (lines 19-53):
|
|
|
|
```yaml
|
|
- name: Install base development tools
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- base-devel
|
|
- git
|
|
- curl
|
|
- wget
|
|
- rsync
|
|
- unzip
|
|
- zip
|
|
- less
|
|
- man-db
|
|
- texinfo
|
|
- which
|
|
- zsh
|
|
- stow
|
|
- ansible
|
|
- zoxide
|
|
- atuin
|
|
- broot
|
|
- bottom
|
|
- hyperfine
|
|
- tokei
|
|
- git-delta
|
|
- yazi
|
|
- ueberzugpp
|
|
- tealdeer
|
|
- ledger
|
|
- btop
|
|
- htop
|
|
- bind
|
|
- socat
|
|
- iperf3
|
|
state: present
|
|
```
|
|
|
|
Add `nano`, `vim`, `sshpass`, `smartmontools` here. `cargo-audit` isn't a pacman package (it's installed via `cargo install`), so it goes in the separate "Install Rust toolchain and Python uv" task as a follow-up `cargo` step.
|
|
|
|
- [ ] **Step 1: Add nano, vim, sshpass, smartmontools**
|
|
|
|
Edit the `name:` list in "Install base development tools" (`roles/common/tasks/main.yml:22-51`), adding after `iperf3`:
|
|
|
|
```yaml
|
|
- iperf3
|
|
- nano
|
|
- vim
|
|
- sshpass
|
|
- smartmontools
|
|
state: present
|
|
```
|
|
|
|
- [ ] **Step 2: Add cargo-audit via cargo install**
|
|
|
|
Current "Install Rust toolchain and Python uv" task (`roles/common/tasks/main.yml:55-68`):
|
|
|
|
```yaml
|
|
- name: Install Rust toolchain and Python uv
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- rust
|
|
- eza
|
|
- bat
|
|
- ripgrep
|
|
- fd
|
|
- starship
|
|
- cargo
|
|
- cargo-binstall
|
|
- uv
|
|
state: present
|
|
```
|
|
|
|
Add a new task immediately after it:
|
|
|
|
```yaml
|
|
- name: Install cargo-audit
|
|
command: cargo install cargo-audit --locked
|
|
args:
|
|
creates: "{{ archsetup_user_home }}/.cargo/bin/cargo-audit"
|
|
```
|
|
|
|
No `become` — `cargo install` must run as the invoking user, installing into `~/.cargo/bin`.
|
|
|
|
- [ ] **Step 3: Syntax-check**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --syntax-check
|
|
```
|
|
|
|
Expected: no errors.
|
|
|
|
- [ ] **Step 4: Dry-run**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --check --diff --tags common
|
|
```
|
|
|
|
Expected: pacman task `ok`, no changes (all four packages already installed per the audit). The `cargo-audit` command task will show as `skipped` in check mode — verified for real in Task 6 (`cargo-audit` is already on the machine, per the audit, so it'll be a no-op there too via the `creates` guard).
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add roles/common/tasks/main.yml
|
|
git commit -m "feat(common): add nano, vim, sshpass, smartmontools, cargo-audit"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Remove duplicate `docker` package from `cs_student` role
|
|
|
|
**Files:**
|
|
- Modify: `roles/cs_student/tasks/main.yml:1-17`
|
|
|
|
`devops` role (`roles/devops/tasks/main.yml:6`) already declares `docker`. Current `cs_student` task:
|
|
|
|
```yaml
|
|
# tasks file for cs_student
|
|
- name: Install CS student tools
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- neovim
|
|
- emacs
|
|
- python
|
|
- nodejs
|
|
- npm
|
|
- texlive-bin
|
|
- texlive-basic
|
|
- jupyter-nbconvert
|
|
- docker
|
|
- code
|
|
- github-cli
|
|
state: present
|
|
```
|
|
|
|
- [ ] **Step 1: Remove the `docker` line**
|
|
|
|
Edit to:
|
|
|
|
```yaml
|
|
# tasks file for cs_student
|
|
- name: Install CS student tools
|
|
become: true
|
|
pacman:
|
|
name:
|
|
- neovim
|
|
- emacs
|
|
- python
|
|
- nodejs
|
|
- npm
|
|
- texlive-bin
|
|
- texlive-basic
|
|
- jupyter-nbconvert
|
|
- code
|
|
- github-cli
|
|
state: present
|
|
```
|
|
|
|
- [ ] **Step 2: Syntax-check**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --syntax-check
|
|
```
|
|
|
|
Expected: no errors.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add roles/cs_student/tasks/main.yml
|
|
git commit -m "refactor(cs_student): drop duplicate docker package, already in devops role"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Remove the dead duplicate handler file
|
|
|
|
**Files:**
|
|
- Delete: `roles/common/handlers/main.yaml`
|
|
|
|
**Context:** `roles/common/handlers/` contains both `main.yml` and `main.yaml`. Ansible's role loader (`ansible/playbook/role/__init__.py:_load_role_yaml`, confirmed by reading the installed Ansible source) searches extensions in the fixed order `['.yml', '.yaml', '.json']` and stops at the first match for handlers (`allow_dir=False`). That means `main.yml` — which already has `become: true` and is the one referenced correctly — is the only file Ansible ever loads. `main.yaml` is silently dead: it's never parsed, and its font-cache handler task (which lacks `become: true` and would break under the Task 2 fix if it were ever loaded) is orphaned.
|
|
|
|
Since `main.yml` is confirmed live and already correct, `main.yaml` is pure dead weight causing confusion about which handler is authoritative — remove it.
|
|
|
|
- [ ] **Step 1: Confirm main.yml is the active file**
|
|
|
|
```bash
|
|
cat roles/common/handlers/main.yml
|
|
```
|
|
|
|
Expected output:
|
|
|
|
```yaml
|
|
- name: Update font cache
|
|
become: true
|
|
command: fc-cache -fv
|
|
```
|
|
|
|
- [ ] **Step 2: Delete the dead duplicate**
|
|
|
|
```bash
|
|
git rm roles/common/handlers/main.yaml
|
|
```
|
|
|
|
- [ ] **Step 3: Syntax-check**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --syntax-check
|
|
```
|
|
|
|
Expected: no errors (confirms `main.yml` alone still satisfies the `notify: Update font cache` handler reference in `roles/common/tasks/main.yml`).
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git commit -m "chore(common): remove dead duplicate handlers/main.yaml (main.yml is the one Ansible loads)"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Real playbook run and final verification
|
|
|
|
This is the only step in the plan that touches live machine state beyond `--check` dry-runs. It's expected to be safe and mostly a no-op: every package involved is already installed (confirmed via `pacman -Qeq` during the audit), and the dotfiles role's "Remove existing config files before stowing" task unconditionally removes and recreates its symlink targets on every run by design — that's what fixes the root-ownership bug, with no manual `rm`/`chown` needed.
|
|
|
|
- [ ] **Step 1: Run the full playbook**
|
|
|
|
```bash
|
|
ansible-playbook site.yml
|
|
```
|
|
|
|
Expected: playbook completes with `failed=0`. Some tasks report `changed` (dotfiles symlinks recreated, any packages that were pulled in by Tasks 3-4 but not yet actually reconciled by pacman state).
|
|
|
|
- [ ] **Step 2: Verify symlink ownership**
|
|
|
|
```bash
|
|
ls -la ~/.config/hypr ~/.config/zellij ~/.config/starship.toml ~/.config/kanshi
|
|
```
|
|
|
|
Expected: all four owned by `tudattr`, not `root`.
|
|
|
|
- [ ] **Step 3: Verify no drift remains**
|
|
|
|
```bash
|
|
ansible-playbook site.yml --check --diff
|
|
git status
|
|
```
|
|
|
|
Expected: second `--check` run reports no further changes (idempotent), `git status` shows a clean working tree.
|