galaxy-uninstall: Ansible-Collections entfernen

Ein Befehl, der installierte Ansible-Collections entfernt, anzeigt, welche Kopie Ansible lädt, und Collections nicht löscht, von denen andere noch abhängen.

Das Problem

ansible-galaxy kann Collections installieren, hat aber keinen Befehl, um sie zu entfernen. Collections können an mehreren Orten liegen: in den Pfaden aus COLLECTIONS_PATHS (gesetzt in ansible.cfg oder ANSIBLE_COLLECTIONS_PATH), in collections/-Verzeichnissen von Projekten und in ansible_collections unter site-packages. Wer eine Collection entfernen will, löscht ihr Verzeichnis mit rm -rf an jedem Ort, an dem eine Kopie liegen könnte. Ob eine andere Collection noch davon abhängt, prüft dabei niemand.

Der Bedarf ist belegt:

Wie es funktioniert

galaxy-uninstall fragt Ansible, wo Collections liegen: ansible-config dump liefert die konfigurierten Suchpfade in ihrer Reihenfolge, ansible-galaxy collection list --format json jede installierte Kopie. Ansible wird nicht importiert, ansible.cfg, ANSIBLE_COLLECTIONS_PATH und -p gelten also so, wie Ansible selbst sie liest. Daraus bestimmt das Werkzeug, welche Kopie Ansible lädt und welche von einem früheren Pfad verdeckt werden. Vor dem Entfernen liest es die Abhängigkeiten aus MANIFEST.json und bricht ab, wenn eine andere installierte Collection davon abhängt und keine weitere Kopie installiert bleibt. --deps entfernt zusätzlich Abhängigkeiten, die keine verbleibende Collection mehr braucht, und --shadowed entfernt Kopien, die Ansible nie lädt.

Beim Entfernen einer Kopie werden ansible_collections/<ns>/<name>, das von ansible-galaxy angelegte .info-Verzeichnis und, falls es danach leer ist, das Namespace-Verzeichnis gelöscht. Collections, die zu einem pip-Paket gehören, und Verzeichnisse mit einem .git-Checkout werden nicht gelöscht. Bei Collections, die per Symlink eingebunden sind, wird nur der Link entfernt.

Das README zeigt einen echten Lauf (Pfade gekürzt) für ein Projekt mit demo.app, das von demo.base abhängt, und zwei weiteren Kopien von demo.base:

$ galaxy-uninstall --list
demo.app   1.0.0  ~/proj/collections/ansible_collections  [active]
demo.base  1.0.0  ~/proj/collections/ansible_collections  [active]
demo.base  2.0.0  ~/.ansible/collections/ansible_collections  [shadowed by ~/proj/collections/ansible_collections]
demo.base  2.0.0  /venv/lib/python3.12/site-packages/ansible_collections  [shadowed by ~/proj/collections/ansible_collections]

$ galaxy-uninstall demo.base --all
refusing: demo.app 1.0.0 in ~/proj/collections/ansible_collections depends on demo.base
remove the dependents too, or pass --force

$ galaxy-uninstall demo.app --deps -n
Will remove:
  demo.app 1.0.0  ~/proj/collections/ansible_collections/demo/app
  demo.base 1.0.0  ~/proj/collections/ansible_collections/demo/base
Afterwards Ansible loads demo.base 2.0.0 in ~/.ansible/collections/ansible_collections

Das Werkzeug ist eine Python-CLI, die nur die Standardbibliothek verwendet.

Tests

Die Unit-Tests legen Verzeichnisbäume mit Collections an und ersetzen die Ansible-Befehle durch die Ausgabe, die diese liefern würden. Sie decken die Suchreihenfolge und das Verdecken ab, die Auswahl einer Kopie bei mehreren Installationen, die Abhängigkeitsprüfung, transitiv verwaiste Abhängigkeiten für --deps, das Erkennen von pip-eigenen Collections und git-Checkouts, das Entfernen der .info- und Namespace-Verzeichnisse, Symlinks, Probeläufe und den Abbruch ohne -y, wenn stdin kein Terminal ist.

Der End-to-End-Test verwendet ein echtes ansible-core. Er baut demo.base 1.0.0 und 2.0.0 sowie demo.app mit ansible-galaxy, installiert sie offline in zwei Pfade, entfernt Kopien mit galaxy-uninstall und prüft mit ansible localhost, aus welcher Kopie ein Filter-Plugin geladen wird. Das CI führt alle Tests mit ansible-core 2.15, 2.17 und dem neuesten Release (2.21) aus. Dabei fiel ein Unterschied auf: ansible-core 2.15 und 2.17 brechen ansible-galaxy collection list mit einem Fehler ab, wenn noch keiner der Collection-Pfade existiert, während neuere Versionen eine leere Liste ausgeben. Das Werkzeug behandelt diesen Fehler als leere Liste.

Grenzen

  • Die Suchreihenfolge ist die, die ansible und ansible-playbook außerhalb eines Playbook-Verzeichnisses verwenden. Ein collections/-Verzeichnis neben einem Playbook wird nur berücksichtigt, wenn es mit -p angegeben wird.
  • Versionsbereiche in Abhängigkeiten werden nicht ausgewertet. Eine Abhängigkeit gilt als erfüllt, solange irgendeine Kopie installiert bleibt.
  • --deps kann nicht wissen, welche Collections absichtlich installiert wurden, weil ansible-galaxy das nicht festhält. -n zeigt vorher den Plan, und --keep schützt eine Collection.
  • Rollen und requirements.yml-Dateien werden nicht behandelt.

Code und Installationsanleitung liegen unter github.com/Jwrede/galaxy-uninstall.