Normale Ansicht

Es gibt neue verfügbare Artikel. Klicken Sie, um die Seite zu aktualisieren.
Ältere BeiträgeHaupt-Feeds

Mit Ansible über YAML Lists and Dictionaries iterieren

17. Juli 2023 um 05:00

In diesem Artikel beschreibe ich die beiden Ansible-Variablen-Typen „List variables“ und „Dictionary variables“ sowie die Kombination beider Typen. Ich zeige mit einem einfachen Playbook, wie diese Variablen-Typen in einer Schleife (eng. loop) durchlaufen werden können.

Während der Text mir zur Übung und Erinnerung dient, hoffe ich, dass er für die Einsteiger unter euch eine hilfreiche Einführung bietet. Für weiterführende Informationen verlinke ich im Text direkt auf die Ansible-Dokumentation.

List

Eine Liste ist eine Variable mit einem Namen und einem bis mehreren Werten. Folgender Code-Block zeigt die List-Variable namens list mit ihren zwei Werten:

list:
  - Alice Cooper
  - Bob Marley

Die Einrückung der Werte ist wichtig. Sie erhöht nicht nur die Lesbarkeit, sondern vermeidet auch Lint-Fehler bei der Anwendung von ansible-lint. Bedauerlicherweise läuft euer Playbook auch, wenn ihr die Werte nicht einrückt, doch bitte ich euch, euch diesen schlechten Stil nicht anzugewöhnen.

Listen sind mit Arrays verwand. Sie besitzen einen Index, welcher bei 0 beginnt und für jedes Listen-Element (für jeden Wert) um 1 inkrementiert wird. Folgendes Beispiel zeigt, wie man das Listen-Element mit dem Wert „Alice Cooper“ der einfachen Variable favorit zuweisen kann:

favorit: "{{ list[0] }}"

Dictionary

Ein Dictionary speichert Daten in Schlüssel-Wert-Paaren (excuse my German). Dabei darf der Wert eines Dictionary wiederum ein Dictionary sein.

Ein einfaches Dictionary sieht wie folgt aus:

Felder:
  Feld1: 10ha
  Feld2: 40ha

Möchte man z.B. auf den Wert des Schlüssels Feld2 aus dem Dictionary Felder zugreifen, geht dies wie folgt:

mein_feld: "{{ Felder['Feld2'] }}"
# oder
mein_feld: "{{ Felder.Feld2 }}"

Die beiden folgenden Code-Blöcke zeigen zwei Beispiele für etwas komplexere Dictionaries, über die ich später iterieren werde:

dict:
  Alice:
    last_name: Cooper
    job: singer
  Bob:
    last_name: Marley
    job: singer
virtual_machines_with_params:
  vm1:
    cpu_count: 2
    memory_mb: 2048
    guest_os: rhel8
  vm2:
    cpu_count: 2
    memory_mb: 1024
    guest_os: rhel9

Auch hier ist die Einrückung sehr wichtig. Macht man dabei einen Fehler, fängt man sich einen Syntax-Fehler bei der Ausführung des Playbooks ein.

List of Dictionaries

Beide zuvor beschriebenen Variablen-Typen können miteinander kombiniert werden. Folgendes Beispiel zeigt eine Liste von Dictionaries:

list_of_dicts:
  - first_name: Alice
    last_name: Cooper
    job: singer
  - first_name: Bob
    last_name: Marley
    job: singer

Syntaxfehler

In Ansible und YAML spielt die Einrückung von Code eine sehr wichtige Rolle. Des Weiteren ist bei der Kombination von Variablen-Typen nicht alles erlaubt. Folgender Code-Block zeigt ein fehlerhaftes Beispiel und die Fehlermeldungen, die es generiert:

$ cat nonsense.yml 
---
nonsense:
 - first_name: Alice
     last_name: Cooper
     job: singer
 - first_name: Bob
     last_name: Marley
     job: singer

$ ansible-lint nonsense.yml
WARNING  Listing 1 violation(s) that are fatal
load-failure: Failed to load YAML file
nonsense.yml:1 mapping values are not allowed in this context
  in "<unicode string>", line 4, column 15


             Rule Violation Summary              
 count tag          profile rule associated tags 
     1 load-failure min     core, unskippable    

Failed after : 1 failure(s), 0 warning(s) on 1 files.

Tipp: Um Fehler bei der Eingabe zu vermeiden, habe ich meinen Editor Vim mit folgenden Optionen konfiguriert: set ts=2 sts=2 sw=2 et ai cursorcolumn

Playbook

Das folgende Playbook nutzt das Modul ansible.builtin.debug, um Werte der genannten Variablen-Typen auszugeben. Es zeigt dabei, wie diese Variablen in einer Schleife durchlaufen werden können.

Damit es übersichtlich bleibt, nutze ich Tags, um die Tasks im Playbook einzeln ausführen zu können.

$ cat output_dicts_and_lists.yml 
---
- name: Output content of dicts_and_lists.yml
  hosts: localhost
  gather_facts: false
  become: false
  vars_files:
    - dicts_and_lists.yml
  tasks:
    - name: Task 1 Ouput all vars in dicts_and_lists.yml
      loop:
        - "{{ list }}"
        - "{{ dict }}"
        - "{{ list_of_dicts }}"
      ansible.builtin.debug:
        var: item
      tags:
        - dicts_and_lists

    - name: Task 2 Loop over some list
      loop: "{{ list }}"
      ansible.builtin.debug:
        msg: "Name: {{ item }}"
      tags:
        - list

    - name: Task 3 Loop over some dictionary
      loop: "{{ dict | dict2items }}"
      ansible.builtin.debug:
        msg: "Firstname: {{ item.key }} Lastname: {{ item.value.last_name }}"
      tags:
        - dict

    - name: Task 4 Loop over some list_of_dicts
      loop: "{{ list_of_dicts }}"
      ansible.builtin.debug:
        msg: "Firstname: {{ item.first_name }} Lastname: {{ item.last_name }}"
      tags:
        - list_of_dicts

Task 1: Ouput all vars in dicts_and_lists.yml

Dies ist der erste Task aus obigem Playbook. Er gibt die Werte der Variablen list, dict und list_of_dicts aus. Diese habe ich als Liste an loop übergeben (siehe Loops in der Dokumentation).

$ ansible-playbook output_dicts_and_lists.yml --tags dicts_and_lists
…
PLAY [Output content of dicts_and_lists.yml] ***********************************

TASK [Task 1 Ouput all vars in dicts_and_lists.yml] ****************************
ok: [localhost] => (item=['Alice Cooper', 'Bob Marley']) => {
    "ansible_loop_var": "item",
    "item": [
        "Alice Cooper",
        "Bob Marley"
    ]
}
ok: [localhost] => (item={'Alice': {'last_name': 'Cooper', 'job': 'singer'}, 'Bob': {'last_name': 'Marley', 'job': 'singer'}}) => {
    "ansible_loop_var": "item",
    "item": {
        "Alice": {
            "job": "singer",
            "last_name": "Cooper"
        },
        "Bob": {
            "job": "singer",
            "last_name": "Marley"
        }
    }
}
ok: [localhost] => (item=[{'first_name': 'Alice', 'last_name': 'Cooper', 'job': 'singer'}, {'first_name': 'Bob', 'last_name': 'Marley', 'job': 'singer'}]) => {
    "ansible_loop_var": "item",
    "item": [
        {
            "first_name": "Alice",
            "job": "singer",
            "last_name": "Cooper"
        },
        {
            "first_name": "Bob",
            "job": "singer",
            "last_name": "Marley"
        }
    ]
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

In der Ausgabe ist zu erkennen, dass Listen mit [ ] und Dictonaries mit { } umschlossen werden. Der folgende Code-Block zeigt zum Vergleich den Inhalt der Datei dicts_and_lists.yml.

---
list:
  - Alice Cooper
  - Bob Marley

dict:
  Alice:
    last_name: Cooper
    job: singer
  Bob:
    last_name: Marley
    job: singer

list_of_dicts:
  - first_name: Alice
    last_name: Cooper
    job: singer
  - first_name: Bob
    last_name: Marley
    job: singer

Task 2: Loop over some list

Als Nächstes schauen wir uns die Ausgabe von Task 2 an, welcher lediglich die einzelnen Listenelemente nacheinander ausgibt.

$ ansible-playbook output_dicts_and_lists.yml --tags list
PLAY [Output content of dicts_and_lists.yml] ***********************************

TASK [Task 2 Loop over some list] **********************************************
ok: [localhost] => (item=Alice Cooper) => {
    "msg": "Name: Alice Cooper"
}
ok: [localhost] => (item=Bob Marley) => {
    "msg": "Name: Bob Marley"
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Die Variable item referenziert das jeweils aktuelle Element der Liste, welche mit loop durchlaufen wird.

Task 3: Loop over some dictionary

Um Scrollen zu vermeiden, zeigten die beiden folgenden Code-Blöcke noch einmal den entsprechenden Task aus obigem Playbook und das Dictionary, welches für dieses Beispiel benutzt wird. Der dritte Code-Block zeigt dann die dazugehörige Ausgabe.

    - name: Task 3 Loop over some dictionary
      loop: "{{ dict | dict2items }}"
      ansible.builtin.debug:
        msg: "Firstname: {{ item.key }} Lastname: {{ item.value.last_name }}"
      tags:
        - dict

Bevor ein Dictionary mit loop verarbeitet werden kann, muss es in eine Liste transformiert werden. Dies geschieht mit Hilfe des Filters dict2items.

In einigen älteren Playbooks sieht man statt loop ein Lookup-Plugin in der Form with_dict: "{{ dict }}". Dies ist ebenfalls korrekt, heute jedoch nicht mehr gebräuchlich.

dict:
  Alice:
    last_name: Cooper
    job: singer
  Bob:
    last_name: Marley
    job: singer
$ ansible-playbook output_dicts_and_lists.yml --tags dict

PLAY [Output content of dicts_and_lists.yml] ***********************************

TASK [Task 3 Loop over some dictionary] *****************************************
ok: [localhost] => (item={'key': 'Alice', 'value': {'last_name': 'Cooper', 'job': 'singer'}}) => {
    "msg": "Firstname: Alice Lastname: Cooper"
}
ok: [localhost] => (item={'key': 'Bob', 'value': {'last_name': 'Marley', 'job': 'singer'}}) => {
    "msg": "Firstname: Bob Lastname: Marley"
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Task 4: Loop over some list_of_dicts

Auch hier sind Dictionaries involviert, doch wird kein Filter dict2items benötigt, da es sich bereits um eine Liste handelt, welche an loop übergeben wird.

Die drei folgenden Code-Blöcke zeigen die verwendete Liste, den Task aus obigem Playbook und die Ausgabe.

list_of_dicts:
  - first_name: Alice
    last_name: Cooper
    job: singer
  - first_name: Bob
    last_name: Marley
    job: singer
    - name: Task 4 Loop over some list_of_dicts
      loop: "{{ list_of_dicts }}"
      ansible.builtin.debug:
        msg: "Firstname: {{ item.first_name }} Lastname: {{ item.last_name }}"
      tags:
        - list_of_dicts
$ ansible-playbook output_dicts_and_lists.yml --tags list_of_dicts

PLAY [Output content of dicts_and_lists.yml] ***********************************

TASK [Loop over some list_of_dicts] ********************************************
ok: [localhost] => (item={'first_name': 'Alice', 'last_name': 'Cooper', 'job': 'singer'}) => {
    "msg": "Firstname: Alice Lastname: Cooper"
}
ok: [localhost] => (item={'first_name': 'Bob', 'last_name': 'Marley', 'job': 'singer'}) => {
    "msg": "Firstname: Bob Lastname: Marley"
}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Auch hier ist wieder an den { } zu erkennen, dass item jeweils eine Liste enthält. Der Zugriff auf die Werte geschieht durch die Referenzierung des jeweiligen Schlüssel-Namens. So ist z.B. Alice der Wert des Schlüssels first_name.

Bonusmaterial: Lists in Dicts

Jörn hat ein weiteres Beispiel beigesteuert. Da die Kommentarfunktion von WordPress den Code nicht sauber darstellt, spendiere ich Jörn eine eigene Überrschrift und baue sein Beispiel hier ein.

Jörns Datenstruktur sieht wie folgt aus:

dict_of_lists:
  - name: foo 
    elems:
      - foo one 
      - foo two 
      - foo three
  - name: bar 
    elems:
      - bar one 
      - bar two 
  - name: baz 
    elems:
      - baz one 
      - baz two 
      - baz three

Jörn möchte nun zuerst auf alle Elemente (elems) von foo zugreifen, dann auf jene von bar usw. Dazu nutzt Jörn das Lookup Plugin subelements.

Folgender Code-Block nutzt die Datenstruktur in einem Playbook, um die verschachtelten Elemente auszugeben. Aufruf und Ausgabe zeigt der darauf folgende Block.

---
- name: Lists in dicts
  hosts: localhost
  become: false
  vars:
    dict_of_lists:
      - name: foo
        elems:
          - foo one
          - foo two
          - foo three
      - name: bar
        elems:
          - bar one
          - bar two
      - name: baz
        elems:
          - baz one
          - baz two
          - baz three

  tasks:
    - name: Loop over lists in dicts
      ansible.builtin.debug:
        msg: "Name: {{ item.0.name }}, element {{ item.1 }}"
      loop: "{{ dict_of_lists | subelements('elems') }}

Und hier nun der Playbook-Aufruf mit Ausgabe:

$ ansible-playbook dict_of_lists.yml

PLAY [Lists in dicts] ************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [localhost]

TASK [Loop over lists in dicts] ************************************************************************
ok: [localhost] => (item=[{'name': 'foo', 'elems': ['foo one', 'foo two', 'foo three']}, 'foo one']) => {
    "msg": "Name: foo, element foo one"
}
ok: [localhost] => (item=[{'name': 'foo', 'elems': ['foo one', 'foo two', 'foo three']}, 'foo two']) => {
    "msg": "Name: foo, element foo two"
}
ok: [localhost] => (item=[{'name': 'foo', 'elems': ['foo one', 'foo two', 'foo three']}, 'foo three']) => {
    "msg": "Name: foo, element foo three"
}
ok: [localhost] => (item=[{'name': 'bar', 'elems': ['bar one', 'bar two']}, 'bar one']) => {
    "msg": "Name: bar, element bar one"
}
ok: [localhost] => (item=[{'name': 'bar', 'elems': ['bar one', 'bar two']}, 'bar two']) => {
    "msg": "Name: bar, element bar two"
}
ok: [localhost] => (item=[{'name': 'baz', 'elems': ['baz one', 'baz two', 'baz three']}, 'baz one']) => {
    "msg": "Name: baz, element baz one"
}
ok: [localhost] => (item=[{'name': 'baz', 'elems': ['baz one', 'baz two', 'baz three']}, 'baz two']) => {
    "msg": "Name: baz, element baz two"
}
ok: [localhost] => (item=[{'name': 'baz', 'elems': ['baz one', 'baz two', 'baz three']}, 'baz three']) => {
    "msg": "Name: baz, element baz three"
}

PLAY RECAP ************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Fazit

Das waren nun eine Menge Code-Blöcke. Mir hat es geholfen, dieses Thema noch einmal zu rekapitulieren. Listen können direkt an loop übergeben werden. Dictionaries müssen zuerst den Filter dict2items durchlaufen.

In diesem Text wurden noch nicht alle Fälle besprochen. So wurden nested lists und tiefer verschachtelte Dictionaries ausgespart, um den Artikel nicht noch mehr in die Länge zu ziehen.

Ich hoffe, der Text war auch für die Anfänger und Einsteiger unter euch hilfreich.

❌
❌