Aller au contenu
Infrastructure as Code medium

Script custom d'inventaire Ansible : format JSON, --list/--host, exemples bash/Python

12 min de lecture

Logo Ansible

Quand aucun plugin officiel n’existe (CMDB maison, fichier exotique, API legacy), un script qui retourne le bon JSON fait l’affaire. Bash, Python, Go, Rust — peu importe le langage tant que le format de sortie respecte le contrat Ansible. C’est le fallback universel : si vous savez programmer, vous savez créer un inventaire dynamique.

Cette page couvre le format JSON exact attendu, les hooks --list et --host que votre script doit implémenter, des exemples concrets en bash et Python, et les garde-fous sécurité indispensables.

  • Le format JSON exact d’un inventaire Ansible.
  • Implémenter les hooks --list et --host dans un script.
  • Écrire un script bash simple (~30 lignes).
  • Écrire un script Python avec gestion d’erreurs et cache (~80 lignes).
  • Sécuriser un script custom (permissions, validation entrée).

Un script custom est justifié quand :

  • Aucun plugin officiel ne couvre votre source (CMDB maison, fichier ERP, base de données interne).
  • Le plugin officiel est trop limité pour vos besoins (ex: NetBox sans le filtre que vous voulez).
  • Vous voulez prototyper rapidement avant d’écrire un vrai plugin Python.

À l’inverse, utilisez un plugin officiel quand :

  • Le plugin existe (libvirt, AWS, Azure, GCP, NetBox, Proxmox).
  • Vous travaillez en équipe — un script maison est plus difficile à maintenir.
  • La sécurité prime — un plugin signé Galaxy est plus fiable qu’un script local.

Tout script d’inventaire doit retourner ce format quand il est appelé avec --list :

{
"_meta": {
"hostvars": {
"web1.lab": {
"ansible_host": "10.10.20.21",
"ansible_user": "ansible",
"custom_var": "valeur"
},
"web2.lab": {
"ansible_host": "10.10.20.22"
}
}
},
"all": {
"children": ["webservers", "dbservers"]
},
"webservers": {
"hosts": ["web1.lab", "web2.lab"],
"vars": {
"http_port": 8080
}
},
"dbservers": {
"hosts": ["db1.lab"]
}
}

Sections :

  • _meta.hostvars : variables par host (équivalent host_vars/<host>.yml).
  • <groupe>.hosts : liste des hôtes du groupe.
  • <groupe>.children : sous-groupes.
  • <groupe>.vars : variables du groupe (équivalent group_vars/<groupe>.yml).

Retourne tout l’inventaire en une fois. C’est l’appel principal de Ansible :

Fenêtre de terminal
$ ./mon_script.py --list
{ ... JSON complet ... }

Retourne les variables d’un seul host. Si non implémenté, Ansible utilise _meta.hostvars :

Fenêtre de terminal
$ ./mon_script.py --host web1.lab
{ "ansible_host": "10.10.20.21", "ansible_user": "ansible" }

Préférer _meta.hostvars dans --list : c’est plus rapide (un seul appel au lieu de N), et c’est le pattern moderne. --host reste là pour compatibilité.

Cas d’usage : un fichier CSV avec hostname, IP, role :

data/hosts.csv
web1.lab,10.10.20.21,webserver
web2.lab,10.10.20.22,webserver
db1.lab,10.10.20.31,dbserver

Script inventory/from_csv.sh :

#!/usr/bin/env bash
# Inventaire Ansible depuis un fichier CSV.
# Format CSV : hostname,ip,role
set -euo pipefail
CSV="$(dirname "$0")/../data/hosts.csv"
if [[ "${1:-}" == "--list" ]]; then
jq -nR --rawfile csv "$CSV" '
[ ($csv | split("\n") | map(select(length > 0))) | .[] | split(",") | {
host: .[0],
ip: .[1],
role: .[2]
} ] as $rows |
{
"_meta": {
"hostvars": ([$rows[] | { (.host): { ansible_host: .ip } }] | add)
},
"webservers": { "hosts": [$rows[] | select(.role == "webserver") | .host] },
"dbservers": { "hosts": [$rows[] | select(.role == "dbserver") | .host] },
"all": { "children": ["webservers", "dbservers"] }
}
'
elif [[ "${1:-}" == "--host" ]]; then
echo '{}'
else
echo "Usage: $0 --list|--host <hostname>" >&2
exit 1
fi

Rendre exécutable :

Fenêtre de terminal
chmod 0750 inventory/from_csv.sh

Tester :

Fenêtre de terminal
ansible-inventory -i inventory/from_csv.sh --graph

Sortie :

@all:
|--@webservers:
| |--web1.lab
| |--web2.lab
|--@dbservers:
| |--db1.lab

Avantage : 30 lignes, dépendance unique à jq (souvent déjà installé). Limite : un bug dans le CSV (virgule dans un champ) casse tout.

Plus robuste, gestion d’erreurs, cache simple :

#!/usr/bin/env python3
"""Inventaire dynamique Ansible depuis une API maison."""
import argparse
import json
import os
import sys
import time
from pathlib import Path
from urllib.request import urlopen, Request
API_URL = os.environ.get("CMDB_API_URL", "https://cmdb.example.com/api/hosts")
API_TOKEN = os.environ.get("CMDB_API_TOKEN")
CACHE_FILE = Path("/tmp/ansible_cmdb_cache.json")
CACHE_TTL = 300 # 5 minutes
def fetch_from_api():
"""Interroge l'API CMDB et retourne la liste brute."""
if not API_TOKEN:
print("ERROR: CMDB_API_TOKEN non défini", file=sys.stderr)
sys.exit(1)
req = Request(API_URL, headers={"Authorization": f"Bearer {API_TOKEN}"})
with urlopen(req, timeout=10) as response:
return json.load(response)
def load_cache():
"""Retourne le cache si frais, sinon None."""
if not CACHE_FILE.exists():
return None
if time.time() - CACHE_FILE.stat().st_mtime > CACHE_TTL:
return None
return json.loads(CACHE_FILE.read_text())
def save_cache(data):
"""Sauve l'inventaire en cache."""
CACHE_FILE.write_text(json.dumps(data))
def build_inventory(hosts):
"""Transforme la liste API en format JSON Ansible."""
inventory = {
"_meta": {"hostvars": {}},
"all": {"children": []},
}
for host in hosts:
name = host["hostname"]
# host_vars
inventory["_meta"]["hostvars"][name] = {
"ansible_host": host["ip"],
"ansible_user": host.get("ssh_user", "ansible"),
"datacenter": host.get("dc", "unknown"),
"owner": host.get("owner", "ops"),
}
# Groupes par role
role_group = f"role_{host['role']}"
if role_group not in inventory:
inventory[role_group] = {"hosts": []}
inventory["all"]["children"].append(role_group)
inventory[role_group]["hosts"].append(name)
# Groupe par DC
dc_group = f"dc_{host.get('dc', 'unknown')}"
if dc_group not in inventory:
inventory[dc_group] = {"hosts": []}
inventory["all"]["children"].append(dc_group)
inventory[dc_group]["hosts"].append(name)
return inventory
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--list", action="store_true")
parser.add_argument("--host", type=str)
args = parser.parse_args()
if args.list:
# Cache d'abord
inventory = load_cache()
if inventory is None:
try:
hosts = fetch_from_api()
inventory = build_inventory(hosts)
save_cache(inventory)
except Exception as e:
print(f"ERROR fetching API: {e}", file=sys.stderr)
sys.exit(1)
print(json.dumps(inventory, indent=2))
elif args.host:
# Avec _meta.hostvars dans --list, ce hook retourne {}
print(json.dumps({}))
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
main()

Rendre exécutable et tester :

Fenêtre de terminal
chmod 0750 inventory/from_cmdb.py
export CMDB_API_TOKEN="le-token"
ansible-inventory -i inventory/from_cmdb.py --graph

Vous obtenez un inventaire dynamique complet, caché, avec gestion d’erreurs. Soit 80 lignes pour quelque chose qui en vaudrait 200 sans Python.

Un script d’inventaire s’exécute à chaque commande Ansible. Risque de payload caché : permissions strictes mandatory.

Fenêtre de terminal
chmod 0750 inventory/mon_script.py # rwxr-x--- (owner + group seulement)
chown ansible:ansible inventory/mon_script.py

Jamais 0755 (world-readable + executable) ni 0777. Un attaquant ne doit pas pouvoir lire votre script (qui peut contenir des chemins, URLs internes) ni le modifier.

# ❌ NE PAS FAIRE
API_TOKEN = "abcdef1234567890"
# ✅ Variables d'env
API_TOKEN = os.environ.get("CMDB_API_TOKEN")
# ✅ Ou Vault Ansible (lookup au runtime)
# nécessite une intégration plus complexe — préférer var d'env
# Si l'API retourne du contenu malveillant, validation
hostname = host["hostname"]
if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
sys.stderr.write(f"Hostname invalide: {hostname}\n")
continue # skip ce host plutôt que crasher

Une CMDB compromise pourrait retourner des hostnames avec ;rm -rf / — votre script doit refuser proprement.

import logging
logging.basicConfig(
filename="/var/log/ansible_inventory.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logging.info(f"Inventaire généré: {len(hosts)} hosts")

Audit en cas d’incident : qui a appelé l’inventaire, quand, avec quel résultat.

Tout script d’inventaire ajouté au repo doit passer une code review dédiée. Vérifier :

  • Pas de credentials en clair.
  • Permissions correctes.
  • Validation des entrées.
  • Pas de eval(), exec(), os.system() non justifié.
  • Logging activé.
SymptômeCauseFix
Failed to parse inventory scriptPermissions trop laxistes ou non exécutablechmod 0750 + chmod +x
Format JSON invalideErreur de syntaxe dans la sortieTester ./script --list | jq empty
Lent à chaque commandePas de cache implémentéAjouter cache local (5 min suffisent souvent)
Hôtes manquantsErreur silencieuse dans la boucleAjouter try/except + log explicite
Variables non lues_meta.hostvars absent du JSONToujours inclure _meta.hostvars même vide
Crash sur valeur nullPas de default()Validation et default() Python systématiques

Si votre script dépasse 200 lignes, ou si vous avez besoin de :

  • Configuration via fichier YAML (comme les plugins officiels).
  • Cache plugin (cache_plugin: jsonfile).
  • groups: et keyed_groups: Jinja.

Alors écrivez un vrai plugin Python dans une collection. C’est plus de travail initial, mais le résultat est réutilisable par d’autres équipes et publiable sur Galaxy. Voir la doc Ansible : Developing dynamic inventory.

  • Format JSON Ansible = _meta.hostvars + groupes avec hosts, children, vars.
  • --list mandatory, --host optionnel si _meta.hostvars est complet.
  • Bash + jq pour des sources simples (CSV, fichier texte) ; Python pour API.
  • Permissions strictes (0750), secrets via variables d’env, validation des entrées.
  • Cache local mandatory dès qu’il y a un appel API.
  • Code review obligatoire avant de merger un nouveau script.
  • Plugin Python officiel > script si > 200 lignes ou multi-équipe.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn