Inspirovat se přednostmi, které Ansible má, ale oprostit se od závislosti na dalším skriptovacím jazyce? Ten pokus byl učiněn a jmenuje se Bashible.
Co se dozvíte v článku
- Cíle Bashible
- Ansible
- Bashible
- Skončit dřív, než se něco poškodí
- Přeskakování již vykonaných činností
- Aktuální adresář
- Ošetření hodnot proměnných
- Závislosti
- Odložené úlohy
- Ukončení dceřiných procesů společně s rodičem
- Smyčky
- Moduly
- Neimplementovat to, co lze udělat stávajícími nástroji
- Zatím jen koncept
Cíle Bashible
- dobrou čitelnost skriptů po vzoru Ansible
- být striktní; okamžitě ukončit skript při neošetřeném návratovém kódu
- umožnit snadné přeskakování již vykonaných činností
- nastavovat aktuální adresář
- ošetřovat hodnoty proměnných
- řešit hierarchii závislostí
- implementovat odložené úlohy
- ukončovat dceřinné procesy společně s rodičem
- modularitu; možnost přidávání vlastní funkcionality
- neimplementovat to, co lze udělat jinak (ssh, pdsh, smyčky)
Nejprve si připomeňme, jak funguje nástroj Ansible.
Ansible
Základem jsou playbooky. Každý playbook představuje seznam akcí, které se mají v systému vykonat. Výhodou je, že při dalším spuštění playbooku se již vykonané akce neopakují. Tomu se říká idempotence. Playbook může vypadat takto:
tasks: - name: Install nginx yum: name: nginx state: present - name: Start NgiNX service: name: nginx state: started - command: /bin/false register: result ignore_errors: True - command: /bin/something when: result|failed - command: /bin/something_else when: result|succeeded
Playbook pochopí snad každý na první pohled, což je velkou výhodou. Je ve formátu YAML. Všimněte si, že jsou v něm implementovány i podmínky ( when
), avšak nic se nikde nerozvětvuje – právě tím je možná docíleno lepší čitelnosti.
Jenže tvůrci Ansible nakonec YAML poněkud „znásilnili“. Implementovali totiž smyčky a proměnné, čímž vznikl jakýsi programovací jazyk, avšak, podle mého názoru, nevalného designu. Takovéhle konstrukce, myslím si, čitelnost naopak snižují:
- command: echo {{ item }} with_items: "{{ mylist|default([]) }}" when: item > 5
Nebyla by zde zřetelnější obyčejná smyčka for
- end
?
Bashible
Skript v Bashible může vypadat takto:
#!/usr/local/bin/bashible @ Synchronzuji soubory - result synced rsync -av /foo /bar @ Posilam mail, kdyz se synchronizace nezdarila - when not synced - mail me@me.com <<< "synchronziace se nezdarila" @ Vypinam stroj, pokud ma byt vypinan po synchronizaci - when synced - not when test -f /etc/shutdown-disabled - shutdown -h now
Zavináče (bloky) i pomlčky (příkazy) jsou obyčejné bashové funkce, které spouštějí (či nespouštějí) své argumenty. Nejde tedy o žádný nový jazyk. Skript v Bashible je pořád jenom bashový skript. Lze využívat proměnných prostředí a dalších jazykových konstrukcí Bashe.
Do čistého Bashe by se ukázka výše přepsala přibližně takto. Chci tím demonstrovat onu subjektivně lepší čitelnost zápisu v Bashible:
#!/bin/bash set -eux -o pipefail cd `dirname $0` echo Synchronizuji soubory if rsync -av /foo /bar; then echo Vypinam stroj, pokud ma byt vypinan po synchronizaci if test -f /etc/do-shutdown; then shutdown -h now fi else echo "Posilam mail, kdyz se synchronizace nezdarila" mail me@me.com <<< "synchronziace se nezdarila" fi
Další ukázka: editace souboru sshd_config
a generování my.cnf
ze šablony s využitím modulů template
a edit
.
#!/usr/local/bin/bashible use template edit @ Edituji sshd_config - cd /etc/ssh - uncomment_lines_matching 'UseDNS' sshd_config - uncomment_lines_matching 'X11Forwarding no' sshd_config - replace_lines_matching 'AllowTcpForwarding' 'AllowTcpForwarding yes' sshd_config @ Generuji konfiguraci mysql ze sablony, pokud je mysqld v systemu - when which myslqd - not when test -f /etc/my.cnf - fill_var HOSTNAME not empty hostname - fill_var LISTEN not empty cat /etc/myip.txt - output_to /etc/my.cnf template my.cnf.tpl - delayed unless_already systemctl reload nginx
Skončit dřív, než se něco poškodí
Jakmile příkaz ve skriptu selže, je vhodné skript okamžitě ukončit, než ho nechat běžet, jak je obvyklé. Proto Bashible ve výchozím stavu nastavuje striktní režim set -e -o pipefail
.
Někdy ale návratový kód není důležitý. Třeba když chceme něco smazat a je zbytečné testovat, zda to existuje či nikoli. Pak se hodí funkce may_fail
:
- may_fail rm /neexistujici
Nechceme-li vidět ani případnou chybovou hlášku, předřadíme funkci quiet
:
- quiet may_fail rm /neexistujici
Skript skončí také při použití nedefinované proměnné (unbound variable). Bashible nastavuje striktní režim set -u
. Někdy se však hodí tento režim dočasně vypnout, například když ve skriptu používáme proměnné prostředí, které mohou, ale nemusejí být nastaveny:
set +u echo $nedefinovana_promenna # toto projde set -u echo $nedefinovana_promenna # toto nikoli - vychozi chovani
Přeskakování již vykonaných činností
Bloky ( @
) slouží především k přeskakování činností, které již byly vykonány. Funguje to tak, že funkce „ -
“ spouští příkaz (své argumenty) jen tehdy, nebyl-li v témže bloku již nastaven režim přeskakování.
@ Instaluji nginx, pokud jeste neni v systemu - not when which nginx - aptitude install nginx
Vícewhen
v témže bloku docílí podmínky AND
:
@ Instaluji nginx, pokud jeste neni v systemu a zaroven tam take neni apache - not when which nginx - not when which apache2 - aptitude install nginx
Je lhostejné, píšeme-li not when
či when not
. Někdy zní angličtěji to, jindy ono.
Dále funkce result
zapamatuje návratový kód. Funguje to tak, že nadefinuje novou funkci – zde s názvem installed
:
- result installed true - result installed false - result installed which nginx - result installed test -f /foo/bar
Tuto novou funkci pak použijeme s when
:
- when installed - when not installed - not when installed
Podle zaznamenaného návratového kódu se lze i rozhodovat. V následujícím příkladu skončí příkaz ls
nejspíš s návratovým kódem 2
:
result listed ls /nonexistent @ Podle toho udelam foo - when listed == 0 - foo @ Podle toho udelam bar - when listed == 1 - bar @ Podle toho udelam baz - when listed -gt 1 # větší než 1 - when listed -le 3 # a zároveň menší nebo rovný 3 - baz
Aktuální adresář
Často narážíme na problém, že byl skript spuštěn z jiného adresáře, než měl být. Bashible automaticky na začátku každého bloku ( @
) dělá chdir
. Ve výchozím stavu do téhož adresáře, v němž se skript nachází.
$ cd /tmp $ bashible /home/uzivatel/muj_skript.bash @ Blok 1 - pwd # /home/uzivatel - cd /mnt # zmena @ Blok 2 - pwd # opet /home/uzivatel
Chceme-li přenastavit základní adresář jinam, slouží k tomu funkce base_dir
. Každý další blok v témže skriptu pak bude dělat chdir
právě tam. Funkce reset_base_dir
navrátí počáteční stav a funkce orig_dir
vypíše adresář, odkud byl skript spuštěn.
Ošetření hodnot proměnných
Dalším častým problémem je neošetřování hodnot proměnných. Například očekáváme nějakou hodnotu, jenže příkaz, který by ji měl vrátit, selže:
myhost=` hostnam ` rm -rf /home/$myhost # a vsichni uzivatele zmizi
V Bashible využijeme funkce empty
:
- fill_var myhost not empty hostnam - rm -rf /home/$myhost
Proces by skončil buď už při spuštění hostnam
(které neexistuje), nebo kdyby hostnam
nic nevypsalo.
Závislosti
Řekněme, že naše hypotetická aplikace má webové API a webové rozhraní. API běží nad Node.js, web je generován v PHP. Oboje potřebuje k činnosti Nginx a MySQL. Ty zase závisejí na nějakém základním nastavení systému: system_base
. Závislosti mohou vypadat takhle,
aplikace ├── api │ ├── mysql │ │ └── system_base │ └── nodejs │ └── system_base ├── nginx │ └── system_base └── web └── php └── system_base
Tedy máme celkem osm skriptů:
aplikace.bash
#!/usr/local/bin/bashible - run api.bash - run web.bash
api.bash
#!/usr/local/bin/bashible - run mysql.bash - run nodejs.bash
mysql.bash
#!/usr/local/bin/bashible - unless_already run system_base.bash ...
nodejs.bash
#!/usr/local/bin/bashible - unless_already run system_base.bash ...
Spuštěním konkrétního skriptu se nainstaluje daná část aplikace se všemi závislostmi. Spuštěním aplikace.bash
pak celá aplikace.
Jelikož skript system_base.bash
je volán z více míst, mohlo by dojít k zacyklení. Proto je u něj funkci run
předřazena funkce unless_already
. Ta zaznamenává již spuštěné příkazy (včetně jejich parametrů a aktuálních adresářů) napříč celou hierarchií skriptů. Skript system_base.bash
proběhne jen jednou, poprvé.
Dojde-li ve vnořeném skriptu k chybě, celý proces se ukončí až k nejvyššímu rodiči.
K cílenému ukončení činnosti jednoho skriptu slouží funkce finish
(rodič pokračuje dál). K zastavení celého procesu, tedy včetně nejvyššího rodiče, funkce halt
.
Odložené úlohy
Někdy se hodí, když některé úlohy proběhnou až na úplný závěr. Například reload webserveru. A navíc jen jednou, přestože „událost“ mohou „emitovat“ víckrát různé dceřiné skripty.
K tomuto účelu slouží funkce delayed
, jež shromažďuje odložené příkazy. Interně je zapisuje do dočasného souboru, který je vytvořen nejvyšším rodičem. Opět je vhodné všemu předřadit funkci unless_already
. Navíc may_fail
, neboť také havárie odloženého příkazu by ukončila celý proces, takže by se další odložené příkazy již neprovedly.
- delayed unless_already may_fail quiet rm /foobar - delayed unless_already systemctl reload nginx
Ukončení dceřiných procesů společně s rodičem
Asi jste se setkali s tím, že když zabijete rodičovský proces Bashe, dceřiné procesy zůstanou „viset“. Stačí například, když v dceřiném skriptu spustíte sleep 600
a zabijete rodiče. Bash se totiž chová jinak v interaktivním a neinteraktivním režimu.
Zatímco v interaktivním zabíjí všechno, v neinteraktivním nepřeposílá signály TERM
ani INT
. V neinteraktivním režimu navíc nelze odchytávat signály ( trap
), dokud neskončí spuštěný příkaz (třeba ten sleep
).
V praxi ale spíš požadujeme, aby nikde nic nezůstalo „viset“. Proto Bashible reaguje ihned na INT
/ HUP
/ QUIT
/ INT
/ TERM
a posílá TERM
všem procesům se stejným PGID
.
Smyčky
Bashible smyčky neimplementuje, na rozdíl od Ansible. Smyčky se píší normálně v Bashi. Buď takhle:
@ Kopiruji soubory - when true - || for i in foo bar baz; do - cp "$i" /dest done
Nebo jednořádkově:
@ Kopiruji soubory - when true - || for i in foo bar baz; do - cp "$i" /dest; done
Díky konstrukci - ||
smyčka neproběhne, pokud je aktivní režim přeskakování.
Podobně while
- done
:
@ Kopiruji soubory - when true - || while read path; do - cp "$path" /dest done < list.txt
Či s použitím roury místo přesměrování:
@ Kopiruji soubory - when true - || cat list.txt | grep foo | while read path; do - cp "$path" /dest done
Jednořádková verze téhož:
@ Kopiruji soubory - when true - || cat list.txt | grep foo | while read path; do cp "$path" /dest; done
Výchozí nastavení set -o pipefail
skript ukončí také v případě, že nějaký příkaz v rouře selže.
Moduly
Moduly jsou kolekce bashových funkcí. Bashible moduly hledá ve stejném adresáři, jako je samo, ve formátu bashible.jmeno_modulu.bash
.
Například modul edit
obsahuje následující funkce:
add_line | přidání řádky na konec souboru (není-li řádka již jinde v souboru) |
append_line | přidání řádky na konec souboru (není-li řádka již na konci souboru) |
comment_lines_matching | zakomentování řádek, které odpovídají regulárnímu výrazu |
uncomment_lines_matching | odkomentování řádek, které odpovídají regulárnímu výrazu |
prepend_line | přidání řádky na začátek souboru (není-li již řádka na začátku) |
remove_lines_matching | odstranění řádek, které odpovídají regulárnímu výrazu |
replace_matching | nahrazení řetězců odpovídajících regulárnímu výrazu |
replace_lines_matching | nahrazení celých řádků, na nichž se vyskytuje řetězec, odpovídající regulárnímu výrazu |
Pozor, zatím to nemusí fungovat na všech platformách. Interně je používán GNU/sed, grep a další příkazy, které se mohou chovat na různých platformách různě.
Existuje také jednoduchý „template engine“ modul napsaný v Bashi na 18 řádcích. V šablonách lze provádět jakékoli příkazy, tisknout proměnné a šablona může volat také podšablony (parts):
<html> <head> $( template parts/head.tpl ) </head> <body> Právě je: $( date )<br /> Aktuální uživatel: $( echo $USER )<br /> $( template parts/body.tpl ) </body> </html>
Díky striktnímu režimu generování šablony selže, vyskytne-li se v ní proměnná, která nebyla nadefinována. Až v šabloně je dále možné ošetřit prázdné řetězce, tedy namísto echo $USER
psát not empty echo $USER
.
Šablony asi nikdo nebude používat ke generování HTML, přesto se hodí například ke generování konfiguračních souborů:
use template @ Generuji my.cnf - fill_var HOSTNAME hostname - fill_var MYIP evaluate "host foobar.com | grep 'has address' | cut -d ' ' -f 4" - output_to /etc/mysql/my.cnf template my.cnf.tpl
Neimplementovat to, co lze udělat stávajícími nástroji
Bashible neumí přístup přes SSH, na rozdíl od Ansible. Neimplementuje ani žádný repozitář s definicemi, co se má na jakém stroji udělat. Takovou funkcionalitu lze doplnit v podobě externího nástroje, který zkopíruje skripty na cílový stroj a tam je spustí skrze ssh
či pdsh
.
Bashible dále neimplementuje „facts“, čili abstrakci vlastností systému. Fakta by mohl nastavovat modul v podobě bashových proměnných, třeba takhle:
use system_facts echo $architecture echo $ipv4_addresses
Jenže nebylo by lepší, namísto faktů spouštět jednoúčelové systémové příkazy? Proč nastavovat proměnnou $architecture
, když máme příkaz uname -a
? Nebo proměnnou $hostname
, získáme-li hodnotu spuštěním příkazu hostname
? V případě ip adresy už je ale situace složitější. V systému může být buď ifconfig
nebo ip
, navíc je nutno zapojit grep
, cut
či awk
, abychom zjistili jediný údaj.
Myslím si, že toto je zásadní nedostatek, kvůli němuž vlastně vzniklo Ansible. V Pythonu totiž řeší to, co by mělo fungovat už na příkazové řádce. V Linuxu nejen že neexistuje standardní kolekce malých, jednoúčelových nástrojů, které by měly jednotné rozhraní, ale neexistuje ani jednotný komunikační formát mezi nimi, jenž by umožnil snadnou a spolehlivou výměnu metadat. Je nanejvýš pošetilé, kvůli pár IP adresám parsovat text, který ani nebyl určen ke strojovému zpracování. A svým způsobem je také pošetilé dělat shellové věci v Pythonu.
Zatím jen koncept
Bashible je zatím koncept. Cílem je, aby s verzí 1.0 nebylo nutné už nic měnit. Veškerá přídavná funkcionalita by měla spočívat v modulech a nebo ještě lépe, v jednoúčelových systémových příkazech, napsaných třeba v C, Rustu či Go.
Dalo by se pokračovat třeba tak, že by byla vytvořena standardní kolekce těchto jednoúčelových příkazů, nejlépe multiplatformních, využitelná nejen v Bashible. A hlavně, s jednotným formátem pro výměnu metadat a jednotným dotazovacím jazykem. Je to utopie? Pokud ano, Bashible zůstane jen dalším zbytečným frameworkem. Přesto snad aspoň někoho inspiruje.