Ansible basa il suo funzionamento sulle funzionalità dei moduli che lo compongono. La versatilità e la disponibilità di questi ultimi sono quindi gli unici limiti dello strumento di automazione. I moduli sono i componenti che gli permettono di interfacciarsi con applicativi e sistemi eterogenei in modo trasparente ed idempotente: devono quindi essere compatibili con la sintassi YAML classica dei playbook che li richiamano e dare i medesimi risultati indipendentemente dal numero di volte in cui vengono richiamati.
Analizziamo un semplice playbook per capire di cosa stiamo parlando:
---
- name: Controlla il corretto funzionamento di un sito internet locale vuoto
hosts: thiscomputer
- name: Check availability
uri:
url: http://thiscomputer
status_code: 200
In questo playbook appena descritto ci rivolgiamo al modulo uri, il cui scopo fondamentale è effettuare chiamate ad un indirizzo web ed aspettarsi una risposta. Questo modulo deve quindi essere presente “su disco” per essere raggiunto da Ansible durante il suo funzionamento, dove?
[ansible@thiscomputer modules]$ locate module | grep uri.py
/usr/lib/python3.6/site-packages/ansible/modules/net_tools/basics/uri.py
/usr/lib/python3.6/site-packages/ansible/modules/windows/win_uri.py
Possiamo utilizzare questi files come modello per creare il nostro modulo custom, appoggianoci anche alla libreria ufficiale “ansible.module_utils” fatta proprio per questo scopo.
Iniziamo la parte di sviluppo tenendo a mente che Ansible localizza i moduli come i ruoli: secondo una gerarchia specifica. Per rendere disponibile al tool di automazione quanto stiamo per creare possiamo inserire il nostro nuovo modulo in una cartella già utilizzata per questo scopo, definirne di nuove nel file di configurazione, oppure indicare ad Ansible dove cercare:
[ansible@cos1 modules]$ ansible --version
[...]
configured module search path = ['/opt/ansible/modules']
ansible python module location = /usr/lib/python3.6/site-packages/ansible
[...]
[ansible@cos1 modules]$ export ANSIBLE_LIBRARY=. #PERCORSO DEL NOSTRO MODULO CUSTOM
Necessiteremo di almeno due files: il modulo vero e proprio ed un playbook dal quale richiamarlo. Cominciamo con la creazione del playbook, passando un ipotetico parametro “name” al nostro modulo:
---
- name: Playbook che carica modulo custom
hosts: thiscomputer
tasks:
- nuovo_modulo_custom: name=Flow
Definiamo poi il modulo vero e proprio, tenendo a mente che in questo caso ci limiteremo ad assicurarne il funzionamento, senza entrare nel dettaglio di caratteristiche fondamentali di un modulo ben scritto da condividere con la community, come i metadati estesi e lo header di documentazione, oltre alla definizione del comportamento in “check mode”, per cui comunque riporto la corretta sintassi:
#!/usr/bin/python3
ANSIBLE_METADATA = {
'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
'''
EXAMPLES = '''
'''
RETURN = '''
'''
from ansible.module_utils.basic import AnsibleModule
def run_module():
# definisci come trattare gli argomenti passati al modulo
module_args = dict(
name=dict(type='str', required=True),
new=dict(type='bool', required=False, default=False)
)
# Inseriamo i valori passati al modulo in un dizionario "result"
# è obbigatorio valorizzare il campo 'changed'
# ovvero se il modulo ha cambiato qualcosa sul target
result = dict(
changed=False,
original_message='',
message=''
)
# Utilizziamo l'oggetto AnsibleModule fornito dalla relativa libreria
# importata come livello di astrazione per lavorare con Ansible
# funzionerà da entrypoint e conterrà attributi come l'eventuale
# supporto alla check-mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=False
)
# Cominciamo lavorando sul dizionario di risposta “result”
# qui di seguito definiamo il vero comportamento del modulo
# e salveremo l' output
result['original_message'] = module.params['name']
result['message'] = 'Risposta del modulo: '
# Determiniamo come più ci piace se il modulo è intervenuto (changed?)
# per questo caso specifico avevamo predisposto il modulo
# per accettare un secondo argomento booleano ad hoc
if module.params['new']:
result['changed'] = True
# Utilizzo di AnsibleModule.fail_json() per gestire quando e
# come far fallire il modulo, in questo caso se il "name"
# passato sarà "errore"
if module.params['name'] == 'errore':
module.fail_json(msg='You had one job', **result)
# Infine predisponiamo la restituzione del dizionario
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
Prima di testare l’esecuzione facciamo un breve recap:
- Un modulo deve essere compatibile con la classica sintassi YAML: abbiamo quindi predisposto il codice Python per aspettarsi degli argomenti, salvarli in un dizionario e restituirlo a fine esecuzione
- Abbiamo utilizzato la libreria “AnsibleModule” per istanziare il dizionario di cui sopra, ed i suoi metodi per valorizzarne i parametri più utili
- Il codice core del modulo opera subito dopo la definizione del dizionario di risposta e vi scrive all’interno i parametri ritornati dall’esecuzione, questi verranno passati ad Ansible sotto forma di dizionario al termine dell task
- Posteriormente all’esecuzione, valorizziamo i parametri principali di un modulo tra cui il “changed” status ed il fallimento del task attraverso la valorizzazione dei relativi campi nel dizionario “AnsibleModule”, salvando gli errori nel campo chiave “.fail_json”
Salviamo il tutto e testiamo l’esecuzione richiamando il playbook definito precedentemente. Prima in modalità pulita, utilizzando l’opzione -v per visualizzare il dizionario di risposta che altrimenti passerebbe inosservato…
[ansible@cos1 modules]$ ansible-playbook simple_carica_modulo_test.yml -v
Using /opt/ansible/config/ansible.cfg as config file
PLAY [TestCustomModule] *******************************************************************
TASK [Gathering Facts] *******************************************************************
ok: [thiscomputer]
TASK [simple_modulo_test] *******************************************************************
ok: [thiscomputer] => {"changed": false, "message": "Risposta del modulo", "original_message": "Flow"}
PLAY RECAP *******************************************************************
thiscomputer : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
…sia sollevando un’eccezione matchando la parola chiave “errore” nella definizione del fail_json:
[ansible@cos1 modules]$ ansipla simple_carica_modulo_test.yml
PLAY [TestCustomModule] *******************************************************************
TASK [Gathering Facts] *******************************************************************
ok: [thiscomputer]
TASK [simple_modulo_test] *******************************************************************
fatal: [thiscomputer]: FAILED! => {"changed": false, "message": "Risposta del modulo", "msg": "You had one job", "original_message": "errore"}
PLAY RECAP *******************************************************************
thiscomputer : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
Questo è quanto, estendendo le sezioni del codice riportato qui sopra è possibile creare un modulo custom per implementare l’automazione in qualsiasi nuovo applicativo o sistema.
Vi riporto alla pagina di manuale ufficiale per qualsiasi dubbio: https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html