Wybierz swój język

Podstawy automatyzacji z Ansible: Pętle, wyrażenia warunkowe i uchwyty

We wcześniejszym artykule mówiliśmy o zbieraniu faktów na temat zarządzanych węzłów oraz możliwości ich wykorzystania w naszych playbookach. Pokazaliśmy także, jak można się do nich potem odwołać. Idąc o krok dalej, zajmiemy się teraz ich wykorzystaniem do warunkowego wykonywania zadań. Stosowana jest do tego celu klauzula "when". Można nią także warunkować wykonywanie kolejnych zadań, od poprzednich rezultatów. Klauzula "when" wykorzystuje bezpośrednią składnię (ang. raw syntax) Jinja2, dając w ten sposób możliwość stosowania rozbudowanych wyrażeń z użyciem testów (porównywanie) i filtrów (manipulacja danymi) Jinja2 oraz wyrażeń logicznych, jak m.in. "OR" czy "AND". Oznacza to też, że mamy bezpośredni dostęp do zmiennych, bez potrzeby stosowania podwójnych nawiasów klamrowych "{{ }}".

[msleczek@vm0-net projekt_A]$ cat playbook.yaml
---
- hosts: all
  tasks:
  - name: "Informacje o hostach"
    ansible.builtin.debug: 
      msg:
      - "Host: {{ansible_hostname}}, Family: {{ansible_os_family}}"
      - "IPv4: {{ansible_default_ipv4.address}}"
  - name: "Dodatkowa informacje o {{ HOST }}"
    ansible.builtin.shell: /usr/bin/uptime
    register: uptime_result
    when: ansible_default_ipv4.address == HOST
  - ansible.builtin.debug:
      var: uptime_result
    when: ansible_default_ipv4.address == HOST or ansible_default_ipv4.address == '10.8.232.124'
...

[msleczek@vm0-net projekt_A]$

Tym razem, dla opcji "msg" modułu "ansible.builtin.debug" zdefiniowaliśmy listę. Warto porównać jej wynik z tym, jak wyglądał on w poprzednim artykule. Trochę niżej, wykorzystaliśmy także omówioną tam opcję "var" modułu "ansible.builtin.debug", a także parametr "register". Warto wrócić do tych informacji, jeżeli zastosowanie tych parametrów nie jest dla nas jasne.

W naszym playbooku, jak i inwentarzu nie ma nigdzie zadeklarowanej zmiennej HOST.

[msleczek@vm0-net projekt_A]$ cat inventory
10.8.232.121
10.8.232.122
10.8.232.123
10.8.232.124
[msleczek@vm0-net projekt_A]$

Zmienna HOST jest definiowana w trakcie wywoływania naszego playbooka, za pomocą opcji "--extra-vars".

[msleczek@vm0-net projekt_A]$ ansible-playbook playbook.yaml --extra-vars="HOST=10.8.232.122"

PLAY [all] ********************************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [10.8.232.121]
ok: [10.8.232.124]
ok: [10.8.232.122]
ok: [10.8.232.123]

TASK [Informacje o hostach] ***************************************************************************
ok: [10.8.232.121] => {
    "msg": [
        "Host: vm1-net, Family: RedHat",
        "IPv4: 10.8.232.121"
    ]
}
ok: [10.8.232.122] => {
    "msg": [
        "Host: vm2-net, Family: RedHat",
        "IPv4: 10.8.232.122"
    ]
}
ok: [10.8.232.123] => {
    "msg": [
        "Host: ms3-net, Family: RedHat",
        "IPv4: 10.8.232.123"
    ]
}
ok: [10.8.232.124] => {
    "msg": [
        "Host: vm4-net, Family: RedHat",
        "IPv4: 10.8.232.124"
    ]
}

TASK [Dodatkowa informacje o 10.8.232.122] **********************************************************
skipping: [10.8.232.121]
skipping: [10.8.232.123]
skipping: [10.8.232.124]
changed: [10.8.232.122]

TASK [debug] ****************************************************************************************
skipping: [10.8.232.121]
ok: [10.8.232.122] => {
    "uptime_result": {
        "changed": true,
        "cmd": "/usr/bin/uptime",
        "delta": "0:00:00.006720",
        "end": "2020-05-11 12:16:21.952928",
        "failed": false,
         "rc": 0,
         "start": "2020-05-11 12:16:21.946208",
         "stderr": "",
         "stderr_lines": [],
         "stdout": " 12:16:21 up 2 days, 3:17, 1 user, load average: 0.22, 0.05, 0.02",
         "stdout_lines": [
             " 12:16:21 up 2 days, 3:17, 1 user, load average: 0.22, 0.05, 0.02"
         ]
     }
}
skipping: [10.8.232.123]
ok: [10.8.232.124] => {
    "uptime_result": {
        "changed": false,
        "skip_reason": "Conditional result was False",
        "skipped": true
    }
}

PLAY RECAP ****************************************************************************************
10.8.232.121     : ok=2   changed=0   unreachable=0   failed=0   skipped=2   rescued=0   ignored=0 
10.8.232.122     : ok=4   changed=1   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0 
10.8.232.123     : ok=2   changed=0   unreachable=0   failed=0   skipped=2   rescued=0   ignored=0 
10.8.232.124     : ok=3   changed=0   unreachable=0   failed=0   skipped=1   rescued=0   ignored=0

[msleczek@vm0-net projekt_A]$

Wykonanie dwóch ostatnich zadań naszego playbooka zostało dodatkowo uwarunkowane z użyciem klauzuli "when". Przedostatnie zadanie wykonane zostało tylko dla "10.8.232.122", a ostatnie dla "10.8.232.122" i "10.8.232.124". Warto też zauważyć, że ostatnie zadanie daje różny rezultat dla tych dwóch hostów jako, iż zależny jest on od tego, co wydarzyło się w trakcie przedostatniego zadania. Klauzula "when" jest bardzo przydatna przy tworzeniu uniwersalnych playbooków, warunkujących wykonywanie poszczególnych zadań od rodzaju czy stanu węzła na którym będą one pracować.

Dla przykładu, jeżeli system operacyjny węzła będzie należał do rodziny "Debian", to do instalacji oprogramowania użyjemy modułu "ansible.builtin.apt", a jeżeli będzie nią "RedHat", to modułu "ansible.builtin.dnf". W naszym przykładzie dodatkowo użyjemy warunku logicznego "AND", aby ograniczyć tą instalację tylko do jednego węzła, o określonym adresie IP.

[msleczek@vm0-net projekt_A]$ cat condicion.yaml 
---
- name: START WEB SERVERS
  hosts: all
  tasks:
  - name: Debian Family
    ansible.builtin.apt:
      name: apache2
      state: present
    become: true
    when: ansible_os_family == 'Debian' and ansible_default_ipv4.address == '10.8.232.123'

 - name: RedHat Family
   ansible.builtin.dnf:
     name: httpd
     state: present
   become: true
   when: ansible_os_family == 'RedHat' and ansible_default_ipv4.address == '10.8.232.123'

[msleczek@vm0-net projekt_A]$

Cały czas pracujemy na tym samym inwentarzu, który zawiera cztery hosty.

Posłużymy się teraz opcją "--limit" polecenia "ansible-playbook", aby ograniczyć wykonanie playbooka tylko dla dwóch z nich:

[msleczek@vm0-net projekt_A]$ ansible-playbook condicion.yaml --limit 10.8.232.122,10.8.232.123

PLAY [START WEB SERVERS] **************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [10.8.232.122]
ok: [10.8.232.123]

TASK [Debian Family] ******************************************************************************
skipping: [10.8.232.122]
skipping: [10.8.232.123]

TASK [RedHat Family] ******************************************************************************
skipping: [10.8.232.122]
changed: [10.8.232.123]

PLAY RECAP ****************************************************************************************
10.8.232.122     : ok=1   changed=0   unreachable=0   failed=0   skipped=2   rescued=0   ignored=0 
10.8.232.123     : ok=2   changed=1   unreachable=0   failed=0   skipped=1   rescued=0   ignored=0

[msleczek@vm0-net projekt_A]$

W wyniku wykonania naszego playbooka, do instalacji oprogramowania doszło tylko na 1 hoście "10.8.232.123".

Więcej na temat wyrażeń warunkowych i logicznych można znaleźć w dokumentacji Ansible.


Wyrażenia warunkowe w szablonach Jinja2

Z poprzedniego artykułu wiemy już, że w szablonach Jinja2 możemy odwoływać się do wartości zmiennych poprzez użycie podwójnych nawiasów klamrowych "{{ }}". W szablonach Jinja2 da się także wstawiać komentarze. Umieszcza się je wewnątrz nawiasów klamrowych, pomiędzy znakami hasha "{# #}". Ponadto, w szablonach Jinja2 mogą być też stosowane wyrażenia warunkowe. Umieszcza się je wewnątrz nawiasów klamrowych i znaków procenta - "{% %}". Wyrażenie te mogą być dość rozbudowane i oprócz porównań, mogą zawierać operatory logiczne AND i OR.

[msleczek@vm0-net projekt_A]$ cat index.html.j2 
Połączyłeś się do adresu IP: {{ ansible_default_ipv4.address }}.
{# To jest komentarz #}
{% if ansible_os_family == 'Debian' %}
Działa tu system z rodziny debianowatych.
{% elif ansible_os_family == 'RedHat' %}
Działa tu system z rodziny redhatowatych.
{% else %}
Działa tu system z rodziny nietypowej.
{% endif %} 
Zapraszamy do przeglądnięcia strony https://networkers.pl.

[msleczek@vm0-net projekt_A]$

Sekcja "{% elif %}" oraz "{% else %}" jest opcjonalna. Za to sekcje "{% if %}" i "{% endif %}" są obowiązkowe. W wyniku wykonania playbooka, z naszego szablonu Jinja2 zostaną wstawione do pliku docelowego tylko te wiersze, które spełniają postawione warunki.

[msleczek@vm0-net projekt_A]$ cat jinja2-cond.yaml 
---
- name: FILE CUSTOMIZATION
  hosts: all
  tasks:
  - name: CUSTOMIZE index.html FILE
    ansible.builtin.template:
      src: ./index.html.j2
      dest: /var/www/html/index.html

msleczek@vm0-net projekt_A]$ ansible-playbook jinja2-cond.yaml --limit 10.8.232.121,10.8.232.122

PLAY [FILE CUSTOMIZATION] *************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [10.8.232.122]
ok: [10.8.232.121]

TASK [CUSTOMIZE index.html FILE] ******************************************************************
changed: [10.8.232.121]
changed: [10.8.232.122]

PLAY RECAP ****************************************************************************************
10.8.232.121     : ok=2   changed=1   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0 
10.8.232.122     : ok=2   changed=1   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0

[msleczek@vm0-net projekt_A]$ curl http://10.8.232.122
Połączyłeś się do adresu IP: 10.8.232.122.

Działa tu system z rodziny redhatowatych.
 
Zapraszamy do przeglądnięcia strony https://networkers.pl.

[msleczek@vm0-net projekt_A]$

Po nawiązaniu połączenia HTTP widać powstałą z naszego szablonu "index.html.j2" zawartość pliku "index.html".


Pętle w szablonach Jinja2

Zarówno na poziomie języka YAML, jak i szablonów Jinja2, można także stosować pętle (ang. loops). Umożliwiają one cykliczne powtarzanie tych samych zadań na różnych elementach. Zwykle kończą swoje działanie po przejściu wszystkich elementów z listy lub na skutek zajścia jakiegoś zdarzenia czy też spełnienia jakiegoś warunku.

Poniżej widać użycie pętli "for" wewnątrz szablonu Jinja2. Do tego celu stosowane jest wyrażenie "for .. in .. endfor" odpowiednio umieszczone wewnątrz nawiasów klamrowych i znaków procenta - "{% %}".

[msleczek@vm0-net projekt_A]$ cat index.html.j2
Połączyłeś sie do adresu IP: {{ ansible_default_ipv4.address }}.

{% for item in ansible_dns.nameservers %}
Korzystam z serwera DNS: {{ item }}.
{% endfor %}

Zapraszamy do przeglądnięcia strony https://networkers.pl.

[msleczek@vm0-net projekt_A]$

W trakcie zbierania faktów została zbudowana dla każdego z węzłów lista "ansible_dns.nameservers", zawierająca wszystkie skonfigurowane na nim serwery DNS. W szablonie Jinja2 wykorzystaliśmy wyrażenie "for" do wypisania wszystkich serwerów DNS z tej listy. O ile zmianie uległ szablon Jinja2, to nasz playbook wygląda dokładnie tak samo, jak w poprzednim przykładzie.

Rezultat jego pracy można zobaczyć poniżej:

[msleczek@vm0-net projekt_A]$ cat jinja2-cond.yaml
---
- name: FILE CUSTOMIZATION
  hosts: all
  tasks:
  - name: CUSTOMIZE index.html FILE
    ansible.builtin.template:
      src: ./index.html.j2
      dest: /var/www/html/index.html

[msleczek@vm0-net projekt_A]$ ansible-playbook jinja2-cond.yaml --limit 10.8.232.123,10.8.232.124

PLAY [FILE CUSTOMIZATION] *************************************************************************

TASK [Gathering Facts] ****************************************************************************
ok: [10.8.232.123]
ok: [10.8.232.124]

TASK [CUSTOMIZE index.html FILE] ******************************************************************
changed: [10.8.232.123]
changed: [10.8.232.124]

PLAY RECAP ****************************************************************************************
10.8.232.123     : ok=2   changed=1   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0 
10.8.232.124     : ok=2   changed=1   unreachable=0   failed=0   skipped=0   rescued=0   ignored=0

[msleczek@vm0-net projekt_A]$ curl http://10.8.232.123
Połączyłeś sie do adresu IP: 10.8.232.123.

Korzystam z serwera DNS: 10.8.252.243.
Korzystam z serwera DNS: 10.8.252.244.
Korzystam z serwera DNS: 10.8.232.61.

Zapraszamy do przeglądnięcia strony https://networkers.pl.

[msleczek@vm0-net projekt_A]$

Po nawiązaniu połączenia HTTP widać powstałą z naszego szablonu "index.html.j2" zawartość pliku "index.html".


Red Hat Ansible Automation Platform zawiera w sobie szereg dodatkowych komponentów, które sprawiają, iż nawet najbardziej złożona automatyzacja może być wdrażana wygodnie, efektywnie i skalowalnie w ramach średnich i większych organizacji oraz przedsiębiorstw. Zainteresowanych tą platformą Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript..

Jesteśmy oficjalnym partnerem firmy Red Hat i za naszym pośrednictwem można zakupić ich produkty na polskim rynku.


Pętle wewnątrz Playbook-a (YAML)

W poprzednim artykule pokazaliśmy pierwsze wykorzystanie pętli wewnątrz playbooka. Do obsługi takich pętli zalecane jest wyrażenie "loop" lub "until". Istnieje także wyrażenie "with_<lookup>", które powstało jako pierwsze i było niegdyś szerzej stosowane - obecnie powinniśmy go unikać. Zastosowanie pętli wewnątrz playbooków jest dość szerokie. Korzystamy z nich, kiedy chcemy założyć więcej niż jedno konto użytkownika, stworzyć więcej niż jeden VLAN czy dostosować uprawnienia więcej niż jednego pliku. To co jest w wspólne dla tych wszystkich zadań, to kierunek "więcej niż jeden". Zatem, z pętli będziemy korzystać wtedy, kiedy będziemy chcieli wykonać jakieś zadanie więcej niż jeden raz, ale niekoniecznie z użyciem tych samych wartości zmiennych.


Uwaga! Nie należy przesadzać ze stosowaniem pętli. Każdy obieg pętli powoduje ponowne wywołanie danego modułu. Zatem, jeżeli danemu modułowi da się wskazać listę i tym sposobem wykonać go raz, to należy wybrać tą drogę. Przykładem jest użycie modułu "ansible.builtin.dnf". Zamiast korzystać z pętli, o wiele wydajniej będzie wskazać mu listę pakietów do zainstalowania.

W końcu, o wiele szybciej wykona się polecenie:

# date; dnf install -y httpd php-fpm mariadb-server; date;

niż:

# date; dnf install -y httpd; dnf install -y php-fpm; dnf install -y mariadb-server; date;

W związku z powyższym, dobrze jest się zapoznać dokładniej z dokumentacją do każdego modułu, jaki będziemy stosować. Szczególnie, jeżeli mamy wrażenie, że nasza automatyzacja działa zadziwiająco wolno.


Z wyrażenia "until" korzystamy, kiedy pętla ma wykonywać się do momentu uzyskania jakiegoś określonego rezultatu, a z wyrażenia "loop", kiedy ma wykonać się dla każdego przedmiotu ze wskazanej listy. W praktyce, pętla "loop" jest o wiele częściej stosowana, dlatego przejdziemy przez kilka różnych przykładów jej zastosowania.

Zaczniemy od użycia pętli "loop" dla prostych list (ang. lists). Domyślnie, zmienna "{{ item }}" jest symbolem zastępczym, który przyjmuje kolejno wartości elementów listy, będącej parametrem pętli "loop".

[msleczek@vm0-net projekt_A]$ cat loop_list.yaml
---
- name: "Play with loops"
  hosts: localhost
  gather_facts: false
  vars:
    devices:
    - router
    - switch
    - hub
    - firewall
  tasks:
  - name: "Loop through list"
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ devices }}"

  - name: "Loop through list"
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop:
    - Linux
    - Red Hat
    - Cisco Systems
    - networkers.pl
[msleczek@vm0-net projekt_A]$

Kiedy chcemy wskazać listę zdefiniowaną w sekcji "vars" lub w specjalnym pliku YAML ze zmiennymi, należy odwołać się do niej poprzez podwójny nawias klamrowy "{{ }}". Istnieje też możliwość zdefiniowania listy bezpośrednio w wyrażeniu "loop".

[msleczek@vm0-net projekt_A]$ ansible-playbook loop_list.yaml

PLAY [Play with loops] ***************************************************************************

TASK [Loop through list] *************************************************************************
ok: [localhost] => (item=router) => {
    "msg": "router"
}
ok: [localhost] => (item=switch) => {
    "msg": "switch"
}
ok: [localhost] => (item=hub) => {
    "msg": "hub"
}
ok: [localhost] => (item=firewall) => {
    "msg": "firewall"
}

TASK [Loop through list] ************************************************************************
ok: [localhost] => (item=Linux) => {
    "msg": "Linux"
}
ok: [localhost] => (item=Red Hat) => {
    "msg": "Red Hat"
}
ok: [localhost] => (item=Cisco Systems) => {
    "msg": "Cisco Systems"
}
ok: [localhost] => (item=networkers.pl) => {
    "msg": "networkers.pl"
}

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

[msleczek@vm0-net projekt_A]$

Powyżej widać efekt działania naszego playbooka i wyświetlenie wszystkich elementów zdefiniowanych list. Dla każdej z nich, pętla "loop" wykonała się tyle razy, ile widać komunikatów "msg".


Zdarza się, że chcemy wykonać pętlę "loop" na obiekcie będącym słownikiem (ang. dictionary).

Słownik grupuje nam kilka zmiennych, nazywanych kluczami, do których przypisane są jakieś wartości. Każda z takich grup opisuje czy parametryzuje nam zwykle jakiś jeden obiekt czy element. W naszym przykładzie będzie nim VLAN. W związku z tym, że wyrażenie "loop" spodziewa się argumentu będącego listą, zastosujemy do manipulacji danymi filtr "ansible.builtin.dict2items". Skonwertuje on nasz słownik do postaci listy, z jaką poradzi sobie pętla "loop".

[msleczek@vm0-net projekt_A]$ cat loop_dict.yaml 
---
- name: "Play with loops"
  hosts: localhost
  gather_facts: false
  vars:
    vlans:
      100:
        name: "MGMT"
        ip4: "10.8.100.1"
      200:
        name: "DATA"
        ip4: "10.8.200.1"
      208:
        name: "VOICE"
        ip4: "10.8.208.1"
  tasks:
  - name: "Loop through dictionary"
    ansible.builtin.debug:
      msg: >
           VLAN={{vlan.key}}
           NAME={{vlan.value.name}}
           IPv4={{vlan.value.ip4}}
    loop: "{{ vlans|ansible.builtin.dict2items }}"
    loop_control:
      loop_var: vlan

[msleczek@vm0-net projekt_A]$

Domyślnie, wewnątrz pętli kolejne wartości przypisywane są do zmiennej "item". W naszym przykładzie, zmieniliśmy tą nazwę na "vlan" za pomocą zmiennej "loop_var" sekcji "loop_control". Sekcja "loop_control" służy do manipulacji zachowaniem pętli, w tym ilością i rodzajem udostępnianych przez nią informacji. Więcej na temat "loop_control" znajduje się w dokumentacji Ansible.

W naszym przykładzie nazwa "vlan" wydaje się bardziej intuicyjna. Niemniej, w niektórych przypadkach możemy być zmuszeni do zmiany tej nazwy, ze względu na zazębianie się pętli i kolizje w nazwach. Przykładem może być użycie w pętli "loop" zadania, które samo korzysta z pętli. Nie można korzystać ze zmiennej o tej samej nazwie w pętli zewnętrznej i wewnętrznej równocześnie.

[msleczek@vm0-net projekt_A]$ ansible-playbook loop_dict.yaml

PLAY [Play with loops] *************************************************************************

TASK [Loop through dictionary] *****************************************************************
ok: [localhost] => (item={'key': 100, 'value': {'name': 'MGMT', 'ip4': '10.8.100.1'}}) => {
    "msg": "VLAN=100 NAME=MGMT IPv4=10.8.100.1\n"
}
ok: [localhost] => (item={'key': 200, 'value': {'name': 'DATA', 'ip4': '10.8.200.1'}}) => {
    "msg": "VLAN=200 NAME=DATA IPv4=10.8.200.1\n"
}
ok: [localhost] => (item={'key': 208, 'value': {'name': 'VOICE', 'ip4': '10.8.208.1'}}) => {
    "msg": "VLAN=208 NAME=VOICE IPv4=10.8.208.1\n"
}

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

[msleczek@vm0-net projekt_A]$

Powyżej widać efekt wywołania naszego playbooka (wyświetlenie elementów zdefiniowanego słownika z konfiguracją VLAN).


Zapraszamy do Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript. zainteresowanych rozwiązaniami Red Hat, w tym w szczególności Red Hat Enterprise Linux, Red Hat Satellite, Red Hat OpenShift Container Platform, Red Hat Ansible Automation Platform oraz Red Hat OpenStack Platform. Jesteśmy partnerem firmy Red Hat i za naszym pośrednictwem można zakupić ich produkty na polskim rynku.


Innym zastosowaniem pętli "loop" może być wykonanie operacji na kolejnych plikach danego katalogu.

W naszym przykładzie użyjemy katalogu o zawartości widocznej poniżej:

[msleczek@vm0-net projekt_A]$ ls loop_*.yaml
loop_dict.yaml  loop_dir.yaml  loop_file.yaml  loop_list.yaml
[msleczek@vm0-net projekt_A]$ pwd
/home/msleczek/projekt_A
[msleczek@vm0-net projekt_A]$

Aby dostarczyć listę interesujących nas plików na wejście pętli "loop", posłużymy się funkcją "lookup". Jej pierwszym argumentem będzie opcja "ansible.builtin.fileglob", która listuje pliki ze wskazanego katalogu, pasujące do wskazanego w drugim argumencie tej funkcji wzorca (tylko pliki, nie listuje katalogów). Wyszukiwanie odbywa się lokalnie na stacji zarządzającej, gdzie uruchomiony został playbook. Domyślnie, na wyjściu otrzymamy listę bezwzględnych ścieżek do plików, w której separatorem będzie przecinek ",". Jeżeli chcemy na wyjściu otrzymać typową listę, należy ustawić wewnątrz tej funkcji zmienną "wantlist" na "true".

Zostało to pokazane w niżej zamieszczonym przykładzie:

[msleczek@vm0-net projekt_A]$ cat loop_dir.yaml 
---
- name: "Play with loops"
  hosts: localhost
  gather_facts: false
  tasks:
  - name: "Loop through directory files"
    ansible.builtin.debug:
      msg: "{{ item }}"
    loop: "{{ lookup('ansible.builtin.fileglob', 'loop_*.yaml', wantlist=true) }}"

[msleczek@vm0-net projekt_A]$ ansible-playbook loop_dir.yaml

PLAY [Play with loops] **************************************************************************

TASK [Loop through directory files] *************************************************************
ok: [localhost] => (item=/home/msleczek/projekt_A/loop_dir.yaml) => {
    "msg": "/home/msleczek/projekt_A/loop_dir.yaml"
}
ok: [localhost] => (item=/home/msleczek/projekt_A/loop_list.yaml) => {
    "msg": "/home/msleczek/projekt_A/loop_list.yaml"
}
ok: [localhost] => (item=/home/msleczek/projekt_A/loop_dict.yaml) => {
    "msg": "/home/msleczek/projekt_A/loop_dict.yaml"
}
ok: [localhost] => (item=/home/msleczek/projekt_A/loop_file.yaml) => {
    "msg": "/home/msleczek/projekt_A/loop_file.yaml"
}

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

[msleczek@vm0-net projekt_A]$

Da się również wykonywać operacje na plikach, które zlokalizowane są na zarządzanym hoście. Do tego celu należy zastosować moduł "ansible.builtin.find". Więcej o nim znaleźć można w dokumentacji Ansible.


Pętla "loop" daje także możliwość wykonania określonych operacji dla każdego wiersza wskazanego pliku.

[msleczek@vm0-net projekt_A]$ cat ./inventory
10.8.232.121
10.8.232.122
10.8.232.123
10.8.232.124
[msleczek@vm0-net projekt_A]$ cat loop_file.yaml
---
- name: "Play with loops"
  hosts: localhost
  gather_facts: false
  tasks:
  - name: "Loop through file content"
    ansible.builtin.debug:
      msg: "IP_ADDRESS={{ item }}"
    loop: "{{ lookup('ansible.builtin.file', './inventory').splitlines() }}"

[msleczek@vm0-net projekt_A]$

Aby dostarczyć zawartość interesującego nas pliku na wejście pętli "loop", posłużymy się funkcją "lookup". Jej pierwszym argumentem będzie opcja "ansible.builtin.file", która listuje zawartość wskazanego pliku.

Jeżeli w trakcie przypisywania tej zawartości do zmiennej, chcemy otrzymać listę, której oddzielnym elementem będzie każdy kolejny wiersz pliku, to należy skorzystać z metody języka Python "splitlines()".

[msleczek@vm0-net projekt_A]$ ansible-playbook loop_file.yaml

PLAY [Play with loops] **************************************************************************

TASK [Loop through file content] ****************************************************************
ok: [localhost] => (item=10.8.232.121) => {
    "msg": "IP_ADDRESS=10.8.232.121"
}
ok: [localhost] => (item=10.8.232.122) => {
    "msg": "IP_ADDRESS=10.8.232.122"
}
ok: [localhost] => (item=10.8.232.123) => {
    "msg": "IP_ADDRESS=10.8.232.123"
}
ok: [localhost] => (item=10.8.232.124) => {
    "msg": "IP_ADDRESS=10.8.232.124"
}

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

[msleczek@vm0-net projekt_A]$

Pętla "loop" wykonała się tyle razy ile widać komunikatów "msg", co odpowiada ilości wierszy pliku "./inventory".


Zastosowanie metod Python w Ansible

Należy pamiętać, że Ansible został stworzony w języku Python, a w nim każda zmienna jest obiektem posiadającym pewne metody. Dzięki temu, z metod tych możemy korzystać w Ansible.

Poniżej zostało pokazane przykładowe użycie kilku metod na tym samym tekście:

[msleczek@vm0-net projekt_A]$ cat string_methods.yaml
---
- name: "Play with methods"
  hosts: localhost
  gather_facts: false
  vars:
    TEKST: > 
      aAbB:cCdD:eEfF
      aaaa:aaaa:aaaa
      BBBB-BBBB-BBBB
      AaBb CcDd EeFf
  tasks:

  - name: "String whitout method"
    ansible.builtin.debug:
      msg: "{{ TEKST }}"

  - name: "String method swapcase()"
    ansible.builtin.debug:
      msg: "{{ TEKST.swapcase() }}"

  - name: "String method lower()"
    ansible.builtin.debug:
      msg: "{{ TEKST.lower() }}"

  - name: "String method capitalize()"
    ansible.builtin.debug:
      msg: "{{ TEKST.capitalize() }}"

   - name: "String method split(':')"
     ansible.builtin.debug:
       msg: "{{ TEKST.split(':') }}"

   - name: "String method count(BBBB)"
     ansible.builtin.debug:
       msg: "{{ TEKST.count('BBBB') }}"

[msleczek@vm0-net projekt_A]$

Lista dostępnych metod jest bardzo duża. My zastosowaliśmy kilka przydatnych do operacji na tekście:

  • "swapcase()" - odwraca wielkość liter, czyli małe litery stają się dużymi, a duże małymi.
  • "lower()" - sprawia, że wszystkie litery zamieniamy na małe litery.
  • "capitalize()" - sprawia, że tylko pierwsza litera całego ciągu jest duża.
  • "split()" - dzieli tekst na podstawie podanego separatora.
  • "count()" - zlicza ilość wystąpień danego wzorca.

W naszym przykładzie, najpierw wyświetliliśmy oryginalny tekst, a potem kolejno jego wariacje z użyciem różnych metod. W związku z tym, że zastosowaliśmy operator ">", będzie to jeden wiersz ciągłego tekstu. Oto wynik działania naszego playbooka:

[msleczek@vm0-net projekt_A]$ ansible-playbook string_methods.yaml

PLAY [Play with methods] ************************************************************************

TASK [String whitout method] ********************************************************************
ok: [localhost] => {
    "msg": "aAbB:cCdD:eEfF aaaa:aaaa:aaaa BBBB-BBBB-BBBB AaBb CcDd EeFf\n"
}

TASK [String method swapcase()] *****************************************************************
ok: [localhost] => {
    "msg": "AaBb:CcDd:EeFf AAAA:AAAA:AAAA bbbb-bbbb-bbbb aAbB cCdD eEfF\n"
}

TASK [String method lower()] ********************************************************************
ok: [localhost] => {
    "msg": "aabb:ccdd:eeff aaaa:aaaa:aaaa bbbb-bbbb-bbbb aabb ccdd eeff\n"
}

TASK [String method capitalize()] ***************************************************************
ok: [localhost] => {
    "msg": "Aabb:ccdd:eeff aaaa:aaaa:aaaa bbbb-bbbb-bbbb aabb ccdd eeff\n"
}

TASK [String method split(':')] *****************************************************************
ok: [localhost] => {
    "msg": [
        "aAbB",
        "cCdD",
        "eEfF aaaa",
        "aaaa",
        "aaaa BBBB-BBBB-BBBB AaBb CcDd EeFf\n"
     ]
}

TASK [String method count(BBBB)] ***************************************************************
ok: [localhost] => {
    "msg": "3"
}

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

[msleczek@vm0-net projekt_A]$

Istnieje o wiele więcej zastosowań pętli, dlatego po więcej odsyłamy do dokumentacji Ansible.


Zastosowanie uchwytów w ramach Playbook-a

Uchwyty (ang. handlers) są typowymi zadaniami, których wykonanie jest wyzwalane poprzez powiadomienia (ang. notifications) o zmianie stanu z innych zadań. Użycie ich przekłada się na lepszą efektywność i wydajność playbooka. Zadanie w uchwycie zostanie wykonane tylko raz, bez względu na to, ile razy zostanie wywołane przez różne inne zadania. Dodatkowo, jeżeli dla danego węzła zadanie w uchwycie nie otrzyma żadnego powiadomienia, to nie będzie w ogóle uruchamiane.

[msleczek@vm0-net projekt_A]$ cat playbook.yaml 
---
- name: START WEB SERVERS
  hosts: all
  tasks:
  - name: PACKAGE INSTALLATION
    ansible.builtin.dnf:
      name: httpd
      state: present
    notify:
    - START HTTPD SERVICE
    - OPEN HTTP TRAFFIC

  handlers:
  - name: START HTTPD SERVICE
    ansible.builtin.service:
      name: httpd
      state: started
      enabled: true
  - name: OPEN HTTP TRAFFIC
    ansible.posix.firewalld:
      service: "{{ item }}"
      permanent: true
      immediate: true
      state: enabled
    loop:
    - http
    - https

[msleczek@vm0-net projekt_A]$

W naszym przykładzie, lista uchwytów została zdefiniowana przy zadaniu korzystającym z modułu "ansible.builtin.dnf". Służy do tego celu sekcja "notify", gdzie wskazuje się nazwy uchwytów, które mają zostać powiadomione, w przypadku wprowadzania zmiany. Zadania obsługujące uchwyty zdefiniowane są w specjalnej sekcji playbooka, o nazwie "handlers".

[msleczek@vm0-net projekt_A]$ ansible-playbook playbook.yaml

PLAY [START WEB SERVERS] ********************************************************************

TASK [Gathering Facts] **********************************************************************
ok: [10.8.232.124]
ok: [10.8.232.123]
ok: [10.8.232.122]
ok: [10.8.232.121]

TASK [PACKAGE INSTALLATION] *****************************************************************
ok: [10.8.232.122]
ok: [10.8.232.121]
changed: [10.8.232.124]
changed: [10.8.232.123]

RUNNING HANDLER [START HTTPD SERVICE] *******************************************************
changed: [10.8.232.123]
changed: [10.8.232.124]

RUNNING HANDLER [OPEN HTTP TRAFFIC] *********************************************************
changed: [10.8.232.124] => (item=http)
changed: [10.8.232.123] => (item=http)
changed: [10.8.232.124] => (item=https)
changed: [10.8.232.123] => (item=https)

PLAY RECAP **********************************************************************************
10.8.232.121     : ok=2  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0 
10.8.232.122     : ok=2  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0 
10.8.232.123     : ok=4  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0 
10.8.232.124     : ok=4  changed=3  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

[msleczek@vm0-net projekt_A]$

Zastosowanie uchwytów (ang. handlers) w naszym playbooku sprawia, że jeżeli na jakimś węźle znajduje się już zainstalowany pakiet "httpd", to nie będzie on próbował już nic więcej robić. Czyli, nie będzie próbował otwierać portów dla ruchu HTTP i HTTPS oraz uruchamiać usługi "httpd". Natomiast, gdy na węźle nie ma pakietu "httpd", to zostaną zainstalowane odpowiednie pakiety, co z kolei wyzwoli powiadomienie (ang. notify) i uruchomi uchwyty (ang. handlers), których zadaniem jest zadbanie o to, aby usługa "httpd" została uruchomiona i działała po restarcie, a systemowa zapora sieciowa wpuszczała do niej ruch HTTP i HTTPS.

W naszym przykładzie widać, że tylko 2 z 4 serwerów nie miały zainstalowanego pakietu "httpd" i tylko dla nich zostały uruchomione dodatkowe zadania z sekcji "handlers".


Zapraszamy do Ten adres pocztowy jest chroniony przed spamowaniem. Aby go zobaczyć, konieczne jest włączenie w przeglądarce obsługi JavaScript. zainteresowanych rozwiązaniami Red Hat, w tym w szczególności Red Hat Enterprise Linux, Red Hat Satellite, Red Hat OpenShift Container Platform, Red Hat Ansible Automation Platform oraz Red Hat OpenStack Platform. Jesteśmy partnerem firmy Red Hat i za naszym pośrednictwem można zakupić ich produkty na polskim rynku.


Przed kolejną porcją wiedzy zachęcamy do przećwiczenia i utrwalenia tej poznanej tutaj. Skorzystaj z naszych ćwiczeń!