5 modi per elaborare i dati JSON in Ansible
I dati strutturati sono facili da automatizzare e puoi trarne il massimo vantaggio con Ansible.
L'esplorazione e la convalida dei dati da un ambiente è una pratica comune per prevenire interruzioni del servizio. Puoi scegliere di eseguire il processo periodicamente o su richiesta e i dati che stai controllando possono provenire da diverse fonti: telemetria, output dei comandi, ecc.
Se i dati sono non strutturati, è necessario eseguire alcune espressioni regolari personalizzate per recuperare gli indicatori chiave di prestazione (KPI) rilevanti per scenari specifici. Se i dati sono strutturati, puoi sfruttare un'ampia gamma di opzioni per renderne l'analisi più semplice e coerente. I dati strutturati sono conformi a un modello di dati che consente l'accesso a ciascun campo dati separatamente. I dati per questi modelli vengono scambiati come coppie chiave/valore e codificati utilizzando formati diversi. JSON, ampiamente utilizzato in Ansible, è uno di questi.
Sono disponibili molte risorse in Ansible per lavorare con i dati JSON e questo articolo ne presenta cinque. Anche se negli esempi tutte queste risorse vengono utilizzate insieme in sequenza, probabilmente è sufficiente utilizzarne solo una o due nella maggior parte degli scenari di vita reale.
(Geralt, licenza Pixabay)
Il seguente frammento di codice è un breve documento JSON utilizzato come input per gli esempi in questo articolo. Se vuoi solo vedere il codice, è disponibile nel mio repository GitHub.
Questo è un esempio di output pyATS da un comando show ip ospf neighbors
su un dispositivo Cisco IOS-XE:
{
"parsed": {
"interfaces": {
"Tunnel0": {
"neighbors": {
"203.0.113.2": {
"address": "198.51.100.2",
"dead_time": "00:00:39",
"priority": 0,
"state": "FULL/ -"
}
}
},
"Tunnel1": {
"neighbors": {
"203.0.113.2": {
"address": "192.0.2.2",
"dead_time": "00:00:36",
"priority": 0,
"state": "INIT/ -"
}
}
}
}
}
}
Questo documento elenca varie interfacce da un dispositivo di rete che descrive lo stato OSPF (Open Shortest Path First) di qualsiasi vicino OSPF presente per interfaccia. L'obiettivo è verificare che lo stato di tutte queste sessioni OSPF sia buono (ovvero, FULL).
Questo obiettivo è visivamente semplice, ma se hai molte voci, non lo sarebbe. Fortunatamente, come dimostrano gli esempi seguenti, è possibile farlo su larga scala con Ansible.
[ Migliora la tua esperienza nell'automazione. Ottieni l'elenco di controllo Ansible: 5 motivi per migrare a Red Hat Ansible Automation Platform 2 ]
1. Accedi a un sottoinsieme di dati
Se sei interessato solo a un ramo specifico dell'albero dei dati, un riferimento al suo percorso ti porterà giù nella gerarchia della struttura JSON e ti consentirà di selezionare solo quella parte dell'oggetto JSON. Il percorso è costituito da nomi di chiavi separati da punti.
Per iniziare, crea una variabile (input
) in Ansible che legga il messaggio in formato JSON da un file.
Per scendere di due livelli, ad esempio, è necessario seguire la gerarchia dei nomi delle chiavi fino a quel punto, che in questo caso si traduce in input.parsed.interfaces
. input
è la variabile che memorizza i dati JSON, parsed
la chiave di livello superiore e interfaces
è quella successiva. In un playbook, questo sarà simile a:
- name: Go down the JSON file 2 levels
hosts: localhost
vars:
input: "{{ lookup('file','output.json') | from_json }}"
tasks:
- name: Create interfaces Dictionary
set_fact:
interfaces: "{{ input.parsed.interfaces }}"
- name: Print out interfaces
debug:
var: interfaces
Fornisce il seguente output:
TASK [Print out interfaces] *************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"Tunnel0": {
"neighbors": {
"203.0.113.2": {
"address": "198.51.100.2",
"dead_time": "00:00:39",
"priority": 0,
"state": "FULL/ -"
}
}
},
"Tunnel1": {
"neighbors": {
"203.0.113.2": {
"address": "192.0.2.2",
"dead_time": "00:00:36",
"priority": 0,
"state": "INIT/ -"
}
}
}
}
}
La vista non è cambiata molto; hai tagliato solo i bordi. Piccoli passi!
2. Appiattisci il contenuto
Se l'output precedente non aiuta o desideri una migliore comprensione della gerarchia dei dati, puoi produrre un output più compatto con il filtro to_paths
:
- name: Print out flatten interfaces input
debug:
msg: "{{ lookup('ansible.utils.to_paths', interfaces) }}"
Questo verrà stampato come:
TASK [Print out flatten interfaces input] ***********************************************************************************************************************
ok: [localhost] => {
"msg": {
"Tunnel0.neighbors['203.0.113.2'].address": "198.51.100.2",
"Tunnel0.neighbors['203.0.113.2'].dead_time": "00:00:39",
"Tunnel0.neighbors['203.0.113.2'].priority": 0,
"Tunnel0.neighbors['203.0.113.2'].state": "FULL/ -",
"Tunnel1.neighbors['203.0.113.2'].address": "192.0.2.2",
"Tunnel1.neighbors['203.0.113.2'].dead_time": "00:00:36",
"Tunnel1.neighbors['203.0.113.2'].priority": 0,
"Tunnel1.neighbors['203.0.113.2'].state": "INIT/ -"
}
}
3. Utilizza il filtro json_query (JMESPath)
Se hai familiarità con un linguaggio di query JSON come JMESPath, il filtro json_query di Ansible è tuo amico perché è basato su JMESPath e puoi utilizzare la stessa sintassi. Se questo è nuovo per te, ci sono molti esempi JMESPath da cui puoi imparare negli esempi JMESPath. È una buona risorsa da avere nella tua cassetta degli attrezzi.
Ecco come usarlo per creare un elenco dei vicini per tutte le interfacce. La query eseguita in questo è *.neighbors
:
- name: Create neighbors dictionary (this is now per interface)
set_fact:
neighbors: "{{ interfaces | json_query('*.neighbors') }}"
- name: Print out neighbors
debug:
msg: "{{ neighbors }}"
Che restituisce un elenco su cui è possibile eseguire l'iterazione:
TASK [Print out neighbors] **************************************************************************************************************************************
ok: [localhost] => {
"msg": [
{
"203.0.113.2": {
"address": "198.51.100.2",
"dead_time": "00:00:39",
"priority": 0,
"state": "FULL/ -"
}
},
{
"203.0.113.2": {
"address": "192.0.2.2",
"dead_time": "00:00:36",
"priority": 0,
"state": "INIT/ -"
}
}
]
}
Altre opzioni per interrogare JSON sono jq o Dq (per pyATS).
4. Accedere a campi dati specifici
Ora puoi scorrere l'elenco dei vicini in un ciclo per accedere ai singoli dati. Questo esempio è interessato allo state
di ciascuno. In base al valore del campo, puoi attivare un'azione.
Verrà generato un messaggio per avvisare l'utente se lo stato di una sessione non è PIENO. In genere, si avvisano gli utenti tramite meccanismi come la posta elettronica o un messaggio di chat anziché semplicemente una voce di registro, come in questo esempio.
Mentre esegui il loop sull'elenco neighbors
generato nel passaggio precedente, esegue le attività descritte in tasks.yml
per indicare ad Ansible di stampare un WARNING messaggio solo se lo stato del vicino non è FULL (ovvero, info.value.state non è match("FULL.*")
):
- name: Loop over neighbors
include_tasks: tasks.yml
with_items: "{{ neighbors }}"
Il file tasks.yml
considera info
come l'elemento del dizionario prodotto per ciascun vicino nell'elenco su cui esegui l'iterazione:
- name: Print out a WARNING if OSPF state is not FULL
debug:
msg: "WARNING: Neighbor {{ info.key }}, with address {{ info.value.address }} is in state {{ info.value.state[0:4] }}"
vars:
info: "{{ lookup('dict', item) }}"
when: info.value.state is not match("FULL.*")
Ciò produce un messaggio generato su misura con diversi campi dati per ciascun vicino che non è operativo:
TASK [Print out a WARNING if OSPF state is not FULL] ************************************************************************************************************
ok: [localhost] => {
"msg": "WARNING: Neighbor 203.0.113.2, with address 192.0.2.2 is in state INIT"
}
Nota: filtra i dati JSON in Ansible utilizzando json_query.
5. Utilizza uno schema JSON per convalidare i tuoi dati
Un modo più sofisticato per convalidare i dati da un messaggio JSON consiste nell'utilizzare uno schema JSON. Ciò offre maggiore flessibilità e una gamma più ampia di opzioni per convalidare diversi tipi di dati. Uno schema per questo esempio dovrebbe specificare che state
è una string
che inizia con FULL se è l'unico stato che desideri sia valido (tu può accedere a questo codice nel mio repository GitHub):
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"neighbor" : {
"type" : "object",
"properties" : {
"address" : {"type" : "string"},
"dead_time" : {"type" : "string"},
"priority" : {"type" : "number"},
"state" : {
"type" : "string",
"pattern" : "^FULL"
}
},
"required" : [ "address","state" ]
}
},
"type": "object",
"patternProperties": {
".*" : { "$ref" : "#/definitions/neighbor" }
}
}
Mentre esegui il loop sui vicini, legge questo schema (schema.json
) e lo utilizza per convalidare ogni elemento vicino con il modulo validate
e il motore jsonschema:
- name: Validate state of the neighbor is FULL
ansible.utils.validate:
data: "{{ item }}"
criteria:
- "{{ lookup('file', 'schema.json') | from_json }}"
engine: ansible.utils.jsonschema
ignore_errors: true
register: result
- name: Print the neighbor that does not satisfy the desired state
ansible.builtin.debug:
msg:
- "WARNING: Neighbor {{ info.key }}, with address {{ info.value.address }} is in state {{ info.value.state[0:4] }}"
- "{{ error.data_path }}, found: {{ error.found }}, expected: {{ error.expected }}"
when: "'errors' in result"
vars:
info: "{{ lookup('dict', item) }}"
error: "{{ result['errors'][0] }}"
Salva l'output di quelli che non superano la convalida in modo da poter avvisare l'utente con un messaggio:
TASK [Validate state of the neighbor is FULL] *******************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "errors": [{"data_path": "203.0.113.2.state", "expected": "^FULL", "found": "INIT/ -", "json_path": "$.203.0.113.2.state", "message": "'INIT/ -' does not match '^FULL'", "relative_schema": {"pattern": "^FULL", "type": "string"}, "schema_path": "patternProperties..*.properties.state.pattern", "validator": "pattern"}], "msg": "Validation errors were found.\nAt 'patternProperties..*.properties.state.pattern' 'INIT/ -' does not match '^FULL'. "}
...ignoring
TASK [Print the neighbor that does not satisfy the desired state] ***********************************************************************************************
ok: [localhost] => {
"msg": [
"WARNING: Neighbor 203.0.113.2, with address 192.0.2.2 is in state INIT",
"203.0.113.2.state, found: INIT/ -, expected: ^FULL"
]
}
Se desideri un'immersione più profonda:
- È possibile trovare un esempio e riferimenti più elaborati in Utilizzo delle nuove utilità Ansible per la gestione e la correzione dello stato operativo.
- Una buona risorsa per esercitarsi nella generazione di schemi JSON è il validatore e generatore di schemi JSON.
- Un approccio simile è Schema Enforcer, che ti consente di creare lo schema in YAML (utile se preferisci quella sintassi).
Conclusione
I dati strutturati sono facili da automatizzare e puoi trarne il massimo vantaggio con Ansible. Quando determini i tuoi KPI, puoi automatizzarne i controlli per tranquillità in situazioni come prima e dopo una finestra di manutenzione.