Demonstration von Ansible Lint am Beispiel einer Ansible Collection
In diesem Beitrag erkläre ich kurz, was Ansible Lint ist und demonstriere dessen Anwendung am Beispiel meiner Ansible Collection tronde/nextcloud.
Er richtet sich primär an Personen, die mit Ansible Lint noch nicht vertraut sind. Linting-Profis werden vermutlich keine neuen Erkenntnisse gewinnen.
Was ist Ansible Lint und wofür ist es gut?
Lint (englisch für „Fussel“) ist eine Software zur statischen Code-Analyse. Davon abgeleitet hat sich das Verb linten (englisch to lint) für das Durchführen der statischen Code-Analyse etabliert.
https://de.wikipedia.org/wiki/Lint_(Programmierwerkzeug)
Dem obigen Zitat und der Projektdokumentation folgend, ist Ansible Lint dementsprechend ein Werkzeug zur statischen Code-Analyse von Ansible Playbooks, Roles und Collections. Mit der Anwendung dieses Werkzeugs auf die eigenen Ansible-Inhalte kann sichergestellt werden, dass diese gängigen Konventionen und Standards entsprechen.
Wie wird Ansible Lint installiert?
Die Dokumentation beschreibt verschiedene Installationsverfahren. Ich habe ansible-lint
als Bestandteil der Ansible Development Tools (ADT) installiert. Dies ist ein Werkzeugkasten mit weiteren Programmen wie z.B. `ansible-core` und Ansible Molecule, welche ich für die Entwicklung meiner Ansible Collection nutze.
Auf meiner Fedora Workstation habe ich die ADT wie folgt installiert:
~]$ mkdir venv
~]$ cd venv
venv]$ ]$ python -m venv adt
venv]$ source adt/bin/activate
(adt) venv]$ pip install pip --upgrade
Requirement already satisfied: pip in ./adt/lib64/python3.12/site-packages (23.3.2)
Collecting pip
Using cached pip-24.2-py3-none-any.whl.metadata (3.6 kB)
Using cached pip-24.2-py3-none-any.whl (1.8 MB)
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 23.3.2
Uninstalling pip-23.3.2:
Successfully uninstalled pip-23.3.2
Successfully installed pip-24.2
(adt) venv]$ pip install ansible-dev-tools
Collecting ansible-dev-tools
Using cached ansible_dev_tools-24.7.2-py3-none-any.whl.metadata (11 kB)
… Ausgabe gekürzt
(adt) venv]$ adt --version
ansible-builder 3.1.0
ansible-core 2.17.3
ansible-creator 24.7.1
ansible-dev-environment 24.7.0
ansible-dev-tools 24.7.2
ansible-lint 24.7.0
ansible-navigator 24.8.0
ansible-sign 0.1.1
molecule 24.8.0
pytest-ansible 24.8.0
tox-ansible 24.8.0
Ansible Lint liefert eine ganze Reihe von Profilen mit, welche Autoren unterstützen, die Code-Qualität schrittweise zu verbessern. Der Befehl ansible-lint --list-profiles
gibt die verfügbaren Profile mit einer Beschreibung aus.
Ich werde im Folgenden mit dem Profil shared arbeiten, welches für Autoren gedacht ist, die ihre Collection auf https://galaxy.ansible.com veröffentlichen möchten.
Eine Collection linten
Bevor es zur Sache geht, wechsel ich in das Projektverzeichnis meiner Ansible Collection und erstelle einen neuen Branch, mit dem Befehl git checkout -b lint
. Die in meinem Repo vorhandene Datei .ansible-lint-ignore
lösche ich, da ich im folgenden alle Fehler und Regelverstöße sehen möchte. Zu Beginn stellt sich mein Arbeitsverzeichnis wie folgt dar:
(adt) nextcloud]$ git status
On branch lint
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: .ansible-lint-ignore
no changes added to commit (use "git add" and/or "git commit -a")
Das Programm ansible-lint
besitzt mit der Option --fix
die Fähigkeit, Fehler automatisch zu korrigieren und auch YAML-Dateien neu zu formatieren. Der folgende Code-Block umfasst die gekürzte Ausgabe, wenn das Kommando ansible-lint --profile=shared --fix
im Arbeitsverzeichnis ausgeführt wird.
(adt) nextcloud]$ ansible-lint --profile=shared --fix
WARNING Listing 37 violation(s) that are fatal
galaxy[no-changelog]: No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.
galaxy.yml:1
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex. (NC_HTML) (vars: NC_HTML)
roles/backup_restore_nextcloud/defaults/main.yml:4
…
risky-file-permissions: File permissions unset or incorrect.
roles/backup_restore_nextcloud/tasks/main.yml:18 Task/Handler: Copy backup files to container host
no-changed-when: Commands should not change things if nothing needs doing.
roles/backup_restore_nextcloud/tasks/main.yml:40 Task/Handler: Import tarball contents into an existing podman volume
no-changed-when: Commands should not change things if nothing needs doing.
roles/backup_restore_nextcloud/tasks/main.yml:54 Task/Handler: Export podman volumes to tarballs
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex. (MYSQL_DATABASE) (vars: MYSQL_DATABASE)
roles/deploy_nextcloud_with_mariadb_pod/defaults/main.yml:13
…
Read documentation for instructions on how to ignore specific rule violations.
Modified 6 files.
Rule Violation Summary
count tag profile rule associated tags
33 var-naming[pattern] basic idiom
1 risky-file-permissions safety unpredictability
1 galaxy[no-changelog] shared metadata
2 no-changed-when shared command-shell, idempotency
Failed: 37 failure(s), 0 warning(s) on 25 files. Profile 'shared' was required, but 'min' profile passed.
Obige Ausgabe:
- Benennt Funde mit Pfadangabe und Zeilennummer
- Führt 37 Fehler auf, inkl. der Regeln, die nicht eingehalten werden; Beispiele
galaxy[no-changelog]: No changelog found. Please add a changelog file. Refer to the galaxy.md file for more info.
risky-file-permissions: File permissions unset or incorrect.
no-changed-when: Commands should not change things if nothing needs doing.
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex.
- Informiert, dass
ansible-lint
Änderungen an 6 Dateien vorgenommen hat
Mit git diff
verschaffe ich mir einen Überblick, welche Änderungen ansible-lint
vorgenommen hat. Dies sind in meinem Fall:
- Quoting von Strings
- Einrückung von Kommentaren
Als Nächstes sehe ich mir die übrigen Fehler der Reihe nach an. Die Dokumentation beinhaltet eine Übersicht mit Beschreibungen der einzelnen Regeln. Dies ist nützlich, wenn der kurze Text in der Ausgabe von ansible-lint
nicht ausreichend ist.
galaxy[no-changelog]: No changelog found. Please add a changelog file.
Unter Galaxy: Changelog Details finden sich Hinweise, wie dieser Fehler zu beheben ist. Ich erstelle die leere Datei CHANGELOG.md
im Wurzelverzeichnis meiner Collection und der Fehler ist abgestellt.
Natürlich werde ich diese Datei zukünftig nutzen, um die wichtigsten Änderungen zu dokumentieren. ;-)
risky-file-permissions: File permissions unset or incorrect.
Auch hier habe ich kurz in der Dokumentation unter risky-file-permissions nachgesehen. Den Fehler habe ich abgestellt, indem ich den Parameter mode: 0600
zum Task hinzugefügt habe.
Dies war ein Flüchtigkeitsfehler, wie er häufig passieren kann, wenn man seinen Code nicht konsequent überprüft. Ohne den Dateimode explizit zu setzen, kann dies zu unvorhersehbaren bzw. überraschenden Verhalten führen.
no-changed-when: Commands should not change things if nothing needs doing.
An zwei Stellen bin ich leider nicht herumgekommen, das ansible.builtin.command
Modul zu verwenden, da kein natives Modul für diese Aufgabe existiert. Betrachtet man sich die beiden Tasks fällt auf, dass diese jedes Mal Daten verarbeiten werden. Sie sind nicht idempotent. Im Ergebnis können sie erfolgreich sein oder fehlschlagen, aber sie werden immer Daten verarbeiten und dadurch ändern.
41 - name: Import tarball contents into an existing podman volume
42 ansible.builtin.command:
43 cmd: |
44 podman volume import
45 {{ item }} {{ backup_restore_nextcloud_import_path }}/{{ item }}.tar
46 loop:
47 - "{{ NC_HTML }}"
48 - "{{ NC_APPS }}"
49 - "{{ NC_CONFIG }}"
50 - "{{ NC_DATA }}"
51 - "{{ MYSQL_DATA }}"
52
53 # I need to use the command module as the volume module lacks the functionality
54 # to export podman volumes.
55 - name: Export podman volumes to tarballs
56 ansible.builtin.command:
57 cmd: podman volume export {{ item }} --output {{ backup_restore_nextcloud_export_path }}/{{ item }}.tar
58 loop:
59 - "{{ NC_HTML }}"
60 - "{{ NC_APPS }}"
61 - "{{ NC_CONFIG }}"
62 - "{{ NC_DATA }}"
63 - "{{ MYSQL_DATA }}"
64 tags:
65 - never
66 - backup
Um herauszufinden, wie ich ansible-lint
zufriedenstellen kann, schaue ich wieder in der Doku unter no-changed-when nach. Nach der dortigen Beschreibung ist mein Fehler, dass ich den Rückgabewert des Kommandos nicht behandel. Daher registriere ich nun eine Variable je Task, die die Task-Ausgabe aufnimmt und prüfe den Rückgabewert. Ist der Rückgabewert gleich 0 wird der Task-Status auf changed
gesetzt, ist der Rückgabewert ungleich 0 wird der Status entsprechend auf failed
gesetzt. Das ganze sieht nun wie folgt aus:
41 - name: Import tarball contents into an existing podman volume
42 ansible.builtin.command:
43 cmd: |
44 podman volume import
45 {{ item }} {{ backup_restore_nextcloud_import_path }}/{{ item }}.tar
46 register: __import_tar_output
47 changed_when: __import_tar_output.rc == 0
48 failed_when: __import_tar_output.rc != 0
49 loop:
50 - "{{ NC_HTML }}"
51 - "{{ NC_APPS }}"
52 - "{{ NC_CONFIG }}"
53 - "{{ NC_DATA }}"
54 - "{{ MYSQL_DATA }}"
55
56 # I need to use the command module as the volume module lacks the functionality
57 # to export podman volumes.
58 - name: Export podman volumes to tarballs
59 ansible.builtin.command:
60 cmd: podman volume export {{ item }} --output {{ backup_restore_nextcloud_export_path }}/{{ item }}.tar
61 register: __import_tar_output
62 changed_when: __import_tar_output.rc == 0
63 failed_when: __import_tar_output.rc != 0
64 loop:
65 - "{{ NC_HTML }}"
66 - "{{ NC_APPS }}"
67 - "{{ NC_CONFIG }}"
68 - "{{ NC_DATA }}"
69 - "{{ MYSQL_DATA }}"
70 tags:
71 - never
72 - backup
Collection-intern verwendete Variablen leite ich mit zwei Unterstrichen (‚_‘) ein, um mir zu verdeutlichen, dass diese nicht durch den Nutzer gesetzt werden und daher auch nicht im README.md
oder defaults/main.yml
dokumentiert sind.
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex.
Hier brauche ich nicht weiter nachzuschlagen. Ich verstoße gegen diese Regel, da ich meine Variablen-Namen großgeschrieben habe. Die Ausgabe von ansible-lint
zeigt dies deutlich:
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex. (NC_HTML) (vars: NC_HTML)
roles/backup_restore_nextcloud/defaults/main.yml:4
Diese Meldungen lassen sich mit folgendem Bash-Einzeiler abstellen:
$ for text in $(cut -d':' -f 1 roles/deploy_nextcloud_with_mariadb_pod/defaults/main.yml | grep -v '^$\|^#\|---'); do find roles -type f -iname "*.yml" | xargs sed -i -e "s/$text/\L&/g"; done
Aus verschiedenen Gründen hebe ich mir die Überarbeitung für später auf und nutze die Meldung, um zu demonstrieren, wie man ansible-lint
dazu bringt, bestimmte Fehler zu ignorieren.
Um Regeln für ausgewählte Dateien zu ignorieren, spezifiziert man den jeweiligen Dateinamen und den Namen der Regel in der Datei .ansible-lint-ignore
, welche im Wurzelverzeichnis der Collection erstellt wird:
nextcloud]$ cat .ansible-lint-ignore
roles/deploy_nextcloud_with_mariadb_pod/defaults/main.yml var-naming[pattern]
roles/backup_restore_nextcloud/defaults/main.yml var-naming[pattern]
Zweiter Durchgang mit ansible-lint
Damit habe ich alle Probleme, die im ersten Durchlauf von ansible-lint
gefunden wurden, adressiert. Ein zweiter Durchlauf zeigt das Ergebnis meiner Arbeit:
(adt) nextcloud]$ ansible-lint --profile=shared
WARNING Listing 33 violation(s) marked as ignored, likely already known
var-naming[pattern]: Variables names should match ^[a-z_][a-z0-9_]*$ regex. (NC_HTML) (vars: NC_HTML) (warning) # ignored
roles/backup_restore_nextcloud/defaults/main.yml:4
…Ausgabe gekürzt
WARNING Listing 1 violation(s) that are fatal
yaml[octal-values]: Forbidden implicit octal value "0600"
roles/backup_restore_nextcloud/tasks/main.yml:22
Read documentation for instructions on how to ignore specific rule violations.
Rule Violation Summary
count tag profile rule associated tags
1 yaml[octal-values] basic formatting, yaml
Failed: 1 failure(s), 33 warning(s) on 27 files. Profile 'shared' was required, but 'min' profile passed.
Die ignorierten Regelverstöße werden als Warnung weiterhin ausgegeben, nehmen jedoch keinen Einfluss auf die abschließende Bewertung. Dafür habe ich einen neuen Fehler (yaml[octal-values]) eingebaut. Nach dem aktuellen Regelwerk erfordern oktale Werte ein explizites Quoting, um als Strings verarbeitet zu werden.
Nachdem ich das Problem mit mode: '0600'
behoben habe, endet ein weiterer Lauf von ansible-lint
schlussendlich mit:
Passed: 0 failure(s), 33 warning(s) on 27 files. Profile 'shared' was required, but 'production' profile passed.
Damit erfüllt meine Collection aktuell sogar die Anforderungen des nächst höheren Profils production; allerdings nur, weil ich einige Regeln bewusst ignoriere. Daher ist aktuell noch nicht sichergestellt, dass meine Collection tatsächlich bei einem Import auf Ansible Galaxy akzeptiert wird.
Zusammenfassung
- Ansible Lint ist ein Werkzeug zur statischen Analyse von Ansible Playbooks, Roles und Collections
- Das Werkzeug unterstützt Autoren dabei, gängige Konventionen und Standards einzuhalten und die Qualität des eigenen Codes auf einem Mindest-Niveau zu halten
- Ansible Lint bietet mehrere Profile für verschiedene Anwendungsfälle
- Regeln können bei Bedarf ignoriert werden, was zwar das Ergebnis des Linting beeinflusst, die Qualität jedoch nicht steigert
- Linting sollte fester Bestandteil des eigenen Entwicklungsworkflows sein und stets nach Änderungen durchgeführt werden
Ich persönlich führe ansible-lint
gern in einem eigenständigen Schritt aus. Es besteht jedoch auch die Möglichkeit, dies in den verwendeten Editor, die genutzte IDE oder Molecule zu integrieren und bei Änderungen automatisch laufen zu lassen.
Ich freue mich, wenn euch dieser Artikel gefallen hat.