[megoldva] Ansible probléma: map filter használata string tömbön

Sziasztok!

Egy érdekes problémába futottam bele mostanában. Egy kicsit összetettebb adatstruktúrával dolgozom és a lényeg az, hogy ki szeretném nyerni belőle a benne levő fájl path-okból egy csak fájl path-okból álló tömböt, aminek az elemein egyenként (de egy lépésben) végre szeretném hajtani a dirname filtert, vagyis megkapni a path könyvtárra vonatkozó részét. Ezt sajnos nem sikerült abszolválnom eddig. A második map hívás - úgy tűnik - a sztringekből álló tömböt különálló karakterekre bontja, és arra hajtaná végre a dirname filtert, és az nekem nem lesz jó.

Íme egy playbook ami nálam ezt a viselkedést produkálta:

---
- name: "map() test"
  hosts: localhost
  vars:
    test_array:
      first:
        - "./a/b/c.json"
        - "./a/b/d.json"
      second:
        - "./a/x/e.json"
        - "./a/x/f.json"
        - "./a/x/g.json"
      third:
        - "./a/y/h.json"
        - "./a/y/i.json"
      fourth:
        - "./a/z/j.json"
        - "./a/z/k.json"
  tasks:

    - name: debug vars 
      ansible.builtin.debug:
        msg:
          - item: "{{ item  }}"
      loop: "{{ test_array | dict2items | map(attribute='value') | map('dirname') | join }}"

A kérdés: hogyan kellene ezt rendesen csinálni?

Hozzászólások

Mr. Ai csak úgy vágja :

A playbook célja, hogy az `ansible.builtin.debug` modullal kiírja a `test_array` változó értékeit. Nézzük meg lépésről lépésre, mit csinál:

1. A playbook neve "map() test".
2. Csak a localhoston fog futni.
3. Egy `test_array` változóban tárol különböző elérési útvonalakat (fájlokat) négy kulcs alatt (first, second, third, fourth).

A tasks részben:

1. Az "debug vars" task neve.
2. Az `ansible.builtin.debug` modult használja üzenetek kiírásához.
3. A loop-al végigiterál a `test_array` változón az alábbi módon:
   - A `dict2items` filterrel a változó egy listává konvertálódik, ahol minden elem egy dict, amelynek kulcsa és értéke az eredeti dict kulcsai és értékei.
   - A `map(attribute='value')` segítségével kiválasztja az értékeket (ezek az elérési útvonalak listái).
   - A `map('dirname')` segítségével minden elérési útvonalat a tartalmazó könyvtár elérési útvonalára alakít át.
   - A `join` funkción keresztül az elérési útvonalakat egyetlen stringgé fűzi össze.

Végeredményként a playbook kiírja az összes fájl tartalmazó könyvtárának az elérési útvonalát egy stringben. Azonban itt egy hiba is van: a `map('dirname')` csak a listák első elemét veszi figyelembe, így az eredmény nem teljesen pontos lesz.

 

A hiba abból adódik, hogy a `map('dirname')` a listák első elemét veszi csak figyelembe, így nem kapjuk meg az összes könyvtár elérési útját. 

Ahhoz, hogy minden könyvtár elérési útját megkapjuk, a beágyazott listákon is végig kell iterálnunk. Az alábbi módosítás szükséges az Ansible playbookban:

```yaml
- name: "map() test"
  hosts: localhost
  vars:
    test_array:
      first:
        - "./a/b/c.json"
        - "./a/b/d.json"
      second:
        - "./a/x/e.json"
        - "./a/x/f.json"
        - "./a/x/g.json"
      third:
        - "./a/y/h.json"
        - "./a/y/i.json"
      fourth:
        - "./a/z/j.json"
        - "./a/z/k.json"
  tasks:
    - name: debug vars 
      ansible.builtin.debug:
        msg:
          - item: "{{ item }}"
      loop: "{{ test_array | dict2items | map(attribute='value') | flatten | map('dirname') | unique }}"
```

Ebben a változatban:

- A `flatten` használatával az összes beágyazott listát egyetlen listává alakítjuk.
- A `map('dirname')` ezt az egy listát fogja feldolgozni, így minden elérési útvonalat a tartalmazó könyvtár elérési útvonalára alakít át.
- A `unique` használatával eltávolítjuk az ismétlődő könyvtár elérési utakat, így minden könyvtár csak egyszer jelenik meg az eredményben.

Ha tartós rendszert építesz és okos csapatot nevelsz, akkor száz kiadásban sem érheti baj; ha csak a gépekre hagyatkozol, akkor egyszer jól jársz, máskor rosszul; de ha sem a rendszer nem bírja a terhet, sem a csapat nem tanul a hibákból, akkor minden egyes kiadás kockázat.

Ez esetben miért van az, hogy "a `map('dirname')` a listák első elemét veszi csak figyelembe"?

Ha a loop-ba csak ennyit írok:

loop: "{{ test_array | dict2items | map(attribute='value')"

Ezt kapom:

ok: [localhost] => (item=['./a/b/c.json', './a/b/d.json']) => {
    "msg": [
        {
            "item": [
                "./a/b/c.json",
                "./a/b/d.json"
            ]
        }
    ]
}
ok: [localhost] => (item=['./a/x/e.json', './a/x/f.json', './a/x/g.json']) => {
    "msg": [
        {
            "item": [
                "./a/x/e.json",
                "./a/x/f.json",
                "./a/x/g.json"
            ]
        }
    ]
}
ok: [localhost] => (item=['./a/y/h.json', './a/y/i.json']) => {
    "msg": [
        {
            "item": [
                "./a/y/h.json",
                "./a/y/i.json"
            ]
        }
    ]
}
ok: [localhost] => (item=['./a/z/j.json', './a/z/k.json']) => {
    "msg": [
        {
            "item": [
                "./a/z/j.json",
                "./a/z/k.json"
            ]
        }
    ]

Ez nyilvánvaló, hogy itt a tömb részeken iterálunk. Ez még nem az igazi. Ha ezt megküldenénk a map('dirname') filterrel, akkor az miért csak az első elemeken működne?
No mindegy, ez nem is annyira lényeg.
A következő lépés:

loop: "{{ test_array | dict2items | map(attribute='value') | flatten | map('dirname') | unique }}"

Az eredmény:

ok: [localhost] => (item=./a/b) => {
    "msg": [
        {
            "item": "./a/b"
        }
    ]
}
ok: [localhost] => (item=./a/x) => {
    "msg": [
        {
            "item": "./a/x"
        }
    ]
}
ok: [localhost] => (item=./a/y) => {
    "msg": [
        {
            "item": "./a/y"
        }
    ]
}
ok: [localhost] => (item=./a/z) => {
    "msg": [
        {
            "item": "./a/z"
        }
    ]
}

Ez már majdnem jó.
A végcélom az lenne, hogy az itt szereplő könyvtárlistából előállítsam az egyedi könytárak neveit, hogy aztán azokat egy file modullal másutt létre tudjam hozni.

Tehát egy olyan tömböt szeretnék jelen esetben a

loop: "{{ test_array | dict2items | map(attribute='value') | flatten | map('dirname') | unique | ... }}"

eredmlényeként, ami a következőképpen nézne ki:

[
    "./a",
    "./a/b",
    "./a/x",
    "./a/y",
    "./a/z"
]

Erre mi volna a legmegfelelőbb megközelítés?

Az Mr. Elvesziamunkánkat Ai Úr hülyesége, hogy csak a lista első elemét veszi figyelembe, a teljes listával kellene dolgoznia. Ránézek élesben , ha végre elaludtak a gyerekek, így mobilról egy kopipasztára telt a chatgpt ablakba. 

Ha tartós rendszert építesz és okos csapatot nevelsz, akkor száz kiadásban sem érheti baj; ha csak a gépekre hagyatkozol, akkor egyszer jól jársz, máskor rosszul; de ha sem a rendszer nem bírja a terhet, sem a csapat nem tanul a hibákból, akkor minden egyes kiadás kockázat.

A cél már kicsit arrébb mozdult: a legutóbbi kommentemben vastaggal szedtem a módosult célkitűzést:

A végcélom az lenne, hogy az itt szereplő könyvtárlistából előállítsam az egyedi könytárak neveit, hogy aztán azokat egy file modullal másutt létre tudjam hozni.

Tehát egy olyan tömböt szeretnék jelen esetben a

loop: "{{ test_array | dict2items | map(attribute='value') | flatten | map('dirname') | unique | ... }}"

eredmlényeként, ami a következőképpen nézne ki:

[
    "./a",
    "./a/b",
    "./a/x",
    "./a/y",
    "./a/z"
]

Erre mi volna a legmegfelelőbb megközelítés?

Végül sikerült megoldanom. Nem lett szép, és az Ansible több számomra bonyolultabb/magasabb szintű funkcionalitását kellett igénybe vennem, de azt hiszem működik...
Használjátok egészséggel.

---
- name: "get an ordered list of all directories that need to be created"
  hosts: localhost
  vars:
    test_array:
      first:
        - "./a/b/c.json"
        - "./a/b/d.json"
      second:
        - "./a/x/e.json"
        - "./a/x/f.json"
        - "./a/x/g.json"
      third:
        - "./a/y/h.json"
        - "./a/y/i.json"
      fourth:
        - "./a/z/j.json"
        - "./a/z/k.json"
  tasks:
    - name: set top_level_content_dirs
      ansible.builtin.set_fact:
        top_level_content_dirs: "{{ test_array | dict2items | map(attribute='value') | flatten | map('regex_replace', '^(\\./)?(.*?)/.*$', '\\2') | unique }}"

    - name: look for directories among the agent content
      ansible.builtin.find:
        paths: "{{ top_level_content_dirs }}" 
        file_type: "directory"
        recurse: true
      register: dirs_found
      
    - name: set all_dirs_from_fs
      ansible.builtin.set_fact:
        all_dirs_from_fs: "{{ dirs_found.files | map(attribute='path') + top_level_content_dirs }}"

    - name: set consolidated_dirs
      ansible.builtin.set_fact:
        consolidated_dirs: "{{ ( consolidated_dirs | d([]) ) + dirs_needed | unique }}"
      loop: "{{ test_array | dict2items | map(attribute='value') | flatten | map('dirname') | unique }}"
      vars:
        dirs_needed: "{{ [ './' ] | product(all_dirs_from_fs) | map('join') | select('in', item) }}"

    - name: finalize consolidated_dirs
      ansible.builtin.set_fact:
        consolidated_dirs: "{{ consolidated_dirs | unique | sort }}"

    - name: debug vars 
      ansible.builtin.debug:
        msg:
          - consolidated_dirs: "{{ consolidated_dirs }}"

Az volt a gondolat, hogy egy rövidebb, egyedi elemeket tartalmazó listát könnyebb rendezni. De valóban, az egyedi elemek megtalálásához is tulajdonképpen rendezni kell a listát, tehát jobb ha előbb rendezem, és aztán szűkítem le egyedi elemekre.

Köszi az ötletet!

https://docs.ansible.com/ansible/latest/collections/ansible/builtin/uni…

oldalon lévő példa alapján: a,b,c lesz az eredmény, ott egy nem rendezett listát "deduplikál" és az eredmény meg nem lesz rendezett.

"Az élet tele van kérdésekkel. Az idióták tele vannak válaszokkal."

"Its easier to fool a man than it is to convince they have been fooled"

A megoldásod a körülményekhez képest:
- jól szervezett
- jól dokumentált
- érthető, olvasható

Ettől függetlenül nagyon "tájidegen", olyanra használod az ansible-t amire abszolút nem való.
Hol van itt a state-store, az idempotency? 
Ez abszolút egy job-control feladatnak látszik, ha sok ilyen van érdemes az apache airflow-val megismerkedni.
 

zászló, zászló, szív

Igazából könyvtárakat akarok létrehozni másutt (de csak azokat amikre szükségem van), és ez a rész csak arra kell, hogy összeszedjem, hogy mit is szeretnék létrehozni. A konkrét könyvtárakat létrehozó rész később következik.

Alapvetően egy fájllistám van, arról amit át kell másolnom. A forrás oldalon a könyvtárszerkezetben lehetnek extra dolgok amik nem kellenek. Nyilván egy rekurzív másoláskor a könytárak is létrejönnének, de ha a copy-nak egy listát adok fájl elérési utakkal, akkor az gondolom nem hozza létre a teljes könytárszerkezetet. Vagy esetleg rosszul gondolom és ha a célkönytár "alap/nincsilyen/ilyense/cél.fájl", akkor a könytárak is rendben létrejönnek?

TL;DR ha file, akkor nem. 

Remote absolute path where the file should be copied to.

If src is a directory, this must be a directory too.

If dest is a non-existent path and if either dest ends with “/” or src is a directory, dest is created.

If dest is a relative path, the starting directory is determined by the remote host.

If src and dest are files, the parent directory of dest is not created and the task fails if it does not already exist.

Mr. AI vicces gyerek. Szórakozásból próbáltam egy szimpla programot íratni vele. Megcsinálta. Persze nem működött. Leírtam a kiírt hibát és hogy javítsa. Erre közli a pernahajder, hogy igen, ez természetesen hiba és ez rá a megoldás. No az sem ment. Leírtam mi a hiba és javítsd. Ismét közölte, hogy hát persze hogy az hibás és itt a megoldás. Nem működött. És ez körbe és körbe. És nem volt megoldás sok-sok körön keresztül. Ami a legjobban tetszett, hogy adott egy megoldást, az hibát adott, majd teljes természetességgel közölte a saját programjára, hogy persze hogy hibás és mit kell javítani, ami persze ismét nem működött. Szóval Mr. AI, ja. :-)

"Az élet tele van kérdésekkel. Az idióták tele vannak válaszokkal."

"Its easier to fool a man than it is to convince they have been fooled"

Pontosan emiatt húzodozok tőle. Olyan lázálmai vannak, amik akár valósnak is tűnhetnek, de működni nem működnek. A végén meg több időt töltesz el a debuggolásával, mintha magad írnád meg a kódot.

Szerkesztve: 2023. 10. 06., p – 21:51

Hülyét kapok, a lányom vigyorog rám és egy csepp álmosság sincs a szemében. A chatgpt szerint ez a gond, hátha ér valamit 

Rendben, nézzük meg lépésről lépésre a hibás működést a példa alapján:

Vegyük az eredeti playbook-odat és használjuk csak a `test_array` "first" kulcsát példaként:

Eredeti kód:
```yaml
loop: "{{ test_array | dict2items | map(attribute='value') | map('dirname') | join }}"
```

Ezen lépéseket fogja végigcsinálni:

1. `dict2items` használata:
A `test_array` változó a következő formában van:
```yaml
test_array:
  first:
    - "./a/b/c.json"
    - "./a/b/d.json"
```
`dict2items` alkalmazásával a következőre alakítja:
```yaml
[
  {"key": "first", "value": ["./a/b/c.json", "./a/b/d.json"]}
]
```

2. `map(attribute='value')` használata:
A következőt kapsz:
```yaml
[
  ["./a/b/c.json", "./a/b/d.json"]
]
```

3. `map('dirname')` használata:
Most jön a hiba. Ezen a ponton a `map('dirname')` a beágyazott lista első elemének könyvtárát adja vissza, mert egy listán belüli listán van alkalmazva, így nem az összes elem könyvtára lesz kiválasztva, csak az elsőé:
```yaml
[
  "./a/b"
]
```

Tehát, a fő probléma az, hogy a `map('dirname')` egy beágyazott listán lett alkalmazva és csak az első elem könyvtárát adta vissza, az összes többi elemet a listában figyelmen kívül hagyva. Ezért nem kaptuk az összes könyvtár elérési útvonalát, hanem csak az elsőket.

Ha tartós rendszert építesz és okos csapatot nevelsz, akkor száz kiadásban sem érheti baj; ha csak a gépekre hagyatkozol, akkor egyszer jól jársz, máskor rosszul; de ha sem a rendszer nem bírja a terhet, sem a csapat nem tanul a hibákból, akkor minden egyes kiadás kockázat.

Ezzel kapcsolatban pedig az alapvető bajom az volt, hogy amennyire a kimenetből vissza tudtam következtetni, hogy a dirname-et a különböző path-ok egyes karaktereire hajtotta végre az Ansible valamiért.

Példa kód és kimenet:

---
- name: "map() test"
  hosts: localhost
  vars:
    test_array:
      first:
        - "./a/b/c.json"
        - "./a/b/d.json"
      second:
        - "./a/x/e.json"
        - "./a/x/f.json"
        - "./a/x/g.json"
      third:
        - "./a/y/h.json"
        - "./a/y/i.json"
      fourth:
        - "./a/z/j.json"
        - "./a/z/k.json"
  tasks:

    - name: debug vars 
      ansible.builtin.debug:
        msg:
          - stuff: "{{ test_array | dict2items | map(attribute='value') |join| map('dirname') }}"

Kimenet:

TASK [debug vars] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "stuff": [
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                "/",
                "",
                "/",
                "",
                "/",
                "",
                "",
                "",
                "",
                "",
                "",
                "",
                ""
            ]
        }
    ]
}

Ha

- stuff: "{{ test_array | dict2items | map(attribute='value') }}"

Akkor

TASK [debug vars] ***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "stuff": [
                [
                    "./a/b/c.json",
                    "./a/b/d.json"
                ],
                [
                    "./a/x/e.json",
                    "./a/x/f.json",
                    "./a/x/g.json"
                ],
                [
                    "./a/y/h.json",
                    "./a/y/i.json"
                ],
                [
                    "./a/z/j.json",
                    "./a/z/k.json"
                ]
            ]
        }
    ]
}

Oké, így már értem:

Ha

- stuff: "{{ test_array | dict2items | map(attribute='value') | join }}"

Akkor

ok: [localhost] => {
    "msg": [
        {
            "stuff": "['./a/b/c.json', './a/b/d.json']['./a/x/e.json', './a/x/f.json', './a/x/g.json']['./a/y/h.json', './a/y/i.json']['./a/z/j.json', './a/z/k.json']"
        }
    ]
}

Tehát a join a korábbi tömbökből álló tömbből stringet csinált. (Hogy miért, azt nem teljesen értem.) Ezért egy stringből képzett karaktertömb karakterein hajtódott végre a dirname.
Rendben, most már értem. Ilyen szempontból ez hasznos volt.

ha ertesz pythonul, akkor csinalj erre egy sajat filter plugint. ott raadasul jobban tudsz hibatkezelni. (esetleg kiszurni a hulyeseget, stb)

A vegtelen ciklus is vegeter egyszer, csak kelloen eros hardver kell hozza!

Mivel nem csak a saját gépemen kellene ennek mennie, ezért konyhakész dolgokat használnék inkább, kevésbé saját custom kutyulmányokat.
De köszönöm az ötletet. Adott esetben megfontolás tárgya lehet.

Az extra modulod berakható az Ansible playbookod mellé, és akkor lehet vele együtt disztributálni.

Rengeteg dolog van, ami baromi nehezen rakható össze kifejezéssel Ansible-ben (órákat lehet vele szopni), úgy sem lesz atomgyors, ha meg ciklusban rakod össze (ez általában egyszerű), akkor konkrétan kurvalassú lesz sok adaton, ezzel szemben python modullal pikk-pakk összedobható, és még gyors is lesz. Szóval megvan ennek a helye...

Lehet félreértem mit akarsz, de szerintem ez megteszi:

"{{ test_array.values() | flatten | map('dirname') | unique }}"

Output:
 

["./a/b","./a/x","a/y","./a/z"]

Igen, köszönöm a segítő szándékot. Idáig már eljutottunk a másik hozzászólásláncban.
A scope azóta már némileg módosult: https://hup.hu/comment/2975103#comment-2975103

akkor a listát set_fact, aztán többször fel lehet használni, pl a file modul bemeneteként.

https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fil…

valahogy így:
 

- name: recurse_dirs
  ansible.builtin.file:
    path: "{{ item }}"
    owner: foo
    group: foo
    mode: '0644'
    recurse: true
  with_items: "{{ dir_list }}"

Igen, viszont én egy fájlban egy listát kapok olyan fájlok path-jaival amik kellenek. Ellenben a fájlrendszerben lehetnek extra fájlok és könyvtárak amik nem biztos, hogy kellenek. Tehát ez önmagában nem garantálja, hogy jó listát kapok.

Ezzel együtt tegnap én is arra jutottam, hogy végül ez is kell: innen kapok egy könyvtárlistát, amit össze kell vessek a fájlból kapott path-okkal, és ebből egy harmadik (immár végleges) listát generálni. A megvalósításig még nem jutottam el, mivel most más dolgom van. Majd pár óra múlva...

Off:  nagy szerencse, hogy vannak ilyen megkönnyítőszoftverek, különben shell-scriptekkel és ssh-val kellene szopni.

---
- name: Extract directories from test_array
  hosts: localhost
  connection: local
  gather_facts: no
  vars:
    test_array:
      first:
        - "./a/b/c.json"
        - "./a/b/d.json"
      second:
        - "./a/x/e.json"
        - "./a/x/f.json"
        - "./a/x/g.json"
      third:
        - "./a/y/h.json"
        - "./a/y/i.json"
      fourth:
        - "./a/z/j.json"
        - "./a/z/k.json"
    destination: "b"
  tasks:
    - name: Extract files from test_array
      set_fact:
        unique_files: "{{ test_array | json_query('*[]') | unique }}"

    - name: Stat files on the filesystem
      stat:
        path: "{{ item }}"
      register: file_status
      loop: "{{ unique_files }}"

    - name: Create tree in the destination directory
      file:
        path: "{{ destination }}/{{ item.stat.path | dirname }}"
        state: directory
      loop: "{{ file_status | json_query('results[?stat.exists]') }}"
      when: item.stat.isreg

    - name: Copy existing files to destination directory
      copy:
        src: "{{ item.item }}"
        dest: "{{ destination }}/{{ item.item }}"
      with_items: "{{ file_status.results }}"
      when: item.stat.exists == true

Nálam működik.
Két kérdés: az utolsó task-nál miért with_item-t használtál, miért nem loop-ot?
Ezek szerint a file tud olyat, hogy

    - name: Create tree in the destination directory
      file:
        path: "{{ destination }}/nincsilyen/ilyensem/ez-sem"
        state: directory
      

és ez megcsinálja az egész struktúrát, mint az mkdir -p ?

Wow, én abból indultam ki, hogy nem. Innen is jött néhány constraint, amit igyekeztem figyelembe is venni. Azért akartam a közbülső könyvtárakra is egy entry-t a listámba, hogy azok is létrejöjjenek.
Tény, hogy ennyi erővel ki is próbálhattam volna, ha már nem voltam biztos benne.
Ma is tanultam valamit...