Utiliser Netbox comme inventaire pour Ansible

22 Mars 2022 • 8 min

Dans la continuité de l’excellent blogpost de Vincent Bernat décrivant comment synchroniser des éléments d’inventaire ou de facts dans Netbox, je vous propose aujourd’hui l’exercice inverse : utiliser Netbox comme source d’inventaire Ansible.

Avant de rentrer dans le détail, je me suis dit qu’un petit rappel des principaux protagonistes de cette aventure ne ferait pas de mal pour se rafraîchir les idées.

Ansible

Ansible est un outil de déploiement qui nécessite peu de dépendances sur les machines cibles à déployer. Elles se limitent souvent à SSH et Python, on le qualifie souvent d'agentless.

La recette de déploiement se décrit ainsi :

  • D’une part en YAML, avec le code étape par étape (tasks) assurant l’installation de paquets et le déploiement de fichiers sur les machines cibles ;
  • D’autre part avec l’inventaire décrivant les spécificités liées à l’état souhaité de chaque machine cible (variables).

Pour les déploiements les plus complexes, la structure de l’inventaire peut devenir un véritable problème. En voici un exemple :

host.ini

server1 ansible_host=10.0.1.2                                                                                                                                                                        
server2 ansible_host=10.0.2.1                                                                                                                                                                        
db   ansible_host=10.0.1.2  

Bien que parfaitement valide du point de vue d’Ansible, server1 et db ont la même adresse IP. C’est une erreur facile à identifier dans un inventaire de 3 lignes, mais ça devient plus difficile quand l’inventaire (ou les inventaires !) fait plusieurs centaines de lignes.

Netbox

Netbox est un logiciel qui a fait de l’inventaire son cœur de métier. Il assure l’intégrité des données tout en fournissant une interface claire de visualisation et d’édition. En l’utilisant, des erreurs d’inventaire comme ci-dessus peuvent être aisément identifiées et évitées.

L’autre aspect intéressant de netbox est son API permettant l’exploitation de cette source riche de données par d’autres logiciels.

Si vous voulez mettre vos mains dans le cambouis, vous pouvez suivre ce tutoriel grâce à Docker Compose et au dépôt Git ci-dessous :

$ git clone -b release https://github.com/netbox-community/netbox-docker.git                                                                                                                          
$ cd netbox-docker
$ tee docker-compose.override.yml <<EOF
version: '3.4'
services:
  netbox:
    ports:
      - 8000:8080
EOF                                                          
$ docker-compose up -d                                                                                                                                                                                
$ export NETBOX_API="http://127.0.0.1:8000" # NETBOX_API sera l'url de netbox dans la suite du tuto  

Netbox, en inventaire pour Ansible

Quand on travaille avec des informations de configuration stockées en double dans plusieurs systèmes différents, il est généralement pertinent de décréter qu’un seul de ces systèmes sera la « source de vérité » (SSOT ou Single Source Of Truth pour les anglophones). Autrement dit, ce sera lui qui fera foi et les autres se cascaderont ou se synchroniseront dessus. Ici on propose que ce soit Netbox qui fasse foi, et que Ansible se synchronise dessus grâce à l’API de Netbox.

Token entre Netbox et Ansible

Pour Netbox, il s’agit de reconnaître Ansible comme un utilisateur valide capable de lire les données. Si Ansible était un humain capable d’utiliser un navigateur, on lui donnerait un identifiant et un mot de passe. Comme Ansible n’est pas un humain, on va plutôt utiliser un token. Si vous avez installé Netbox avec le dépôt ci-dessus, un token a été créé automatiquement au passage avec une valeur codée en dur, et on peut l’utiliser tel quel pour la suite :

$ export NETBOX_TOKEN=0123456789abcdef0123456789abcdef01234567                                                                                                                                        
$ curl -H "Authorization: Token $NETBOX_TOKEN" "${NETBOX_API}/api/dcim/sites/"   

Sinon, pour tester sur un Netbox existant (dans lequel, espérons-le, ce token n’existe pas encore), il faut se logguer sur ce Netbox (bouton en haut à droite) avec un compte administrateur. Puis il faut aller dans la partie “admin” de Netbox (via le même menu en haut à droite, qui doit maintenant montrer une entrée “Admin”, justement). La section “USERS” du back-office comprend une entrée “Token”, qui vous permet de lister les tokens existants ; ainsi que d’ajouter un token (“Add token”). Un simple token avec des permissions read-only est nécessaire pour ce tutoriel à base d’Ansible.

Une fois le token obtenu, on peut le tester avec la commande suivante :

$ export NETBOX_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX                                                                                                                                        
$ curl -H "Authorization: Token $NETBOX_TOKEN" "${NETBOX_API}/api/dcim/sites/"   

⚠️ Si votre installation de Netbox est en HTTPS mais que vous obtenez dans vos réponses des URLs commençant par “http://<host>”, vous avez sûrement un problème fréquent de reverse proxy.

Ça devrait marcher quand même, mais vous ferez 2 fois plus de requêtes (la première étant en HTTP, puis redirigée sur HTTPS). Plus grave: vous envoyez votre token en clair une requête sur 2 ! Vous pouvez faire un simple curl https://netbox.example.com/api/, pour vérifier.

Création de l’inventaire Netbox

Une fois en possession de notre token, on va créer un fichier d’inventaire (inventory) en utilisant la syntaxe plugin :

netbox.yml

---                                                                                                                                                                                                  
plugin: netbox.netbox.nb_inventory                                                                                                                                                                  
query_filters:                                                                                                                                                                                      
  - status: active  

Puis installer la collection Netbox (nécessitant Ansible > 2.9) :

$ ansible-galaxy collection install netbox.netbox 

Et enfin tester !

$ ansible-inventory -i netbox.yml --list  

À ce stade, si vous testez avec un Netbox qui contient déjà vos serveurs ou autres équipements, vous devez les voir apparaître dans votre inventaire Ansible.

En revanche, avec un Netbox fraîchement installé (comme à partir du dépôt git donné en exemple plus haut), l’inventaire Ansible est vide car il n’y a encore rien de déclaré dans ce Netbox. Le cas échéant, on peut remplir un peu notre Netbox pour avoir quelques informations :

  • Créez un site (avec un nom)
  • Créez un device-role (avec un nom)
  • Créez un manufacturer (avec un nom)
  • Créez un device-type (avec un nom et un manufacturer)
  • Créez un device (avec un nom, un device-role, un device-type et un site)
  • Cliquez sur votre device, en haut à droite Add Components > Interface, ajoutez une interface (avec un nom)
  • Cliquez sur votre interface, ajoutez un adresse IP (avec une adresse IP suivie d’un slash “/” et d’un netmask ! Par exemple: “127.0.0.1/8”)

Pfieww ! Oui, Netbox demande un minimum de classification et ne nous laisse pas créer des éléments sans un minimum d’informations, mais c’est cette exigence qui fait toute la puissance de ce logiciel.

Vérifions si nous retrouvons désormais bien le fruit de notre dur labeur de click dans Netbox via la commande ansible-inventory.

$ ansible-inventory -i netbox.yml --list                                                                                                                                                            
{                                                                                                                                                                                                    
    "_meta": {                                                                                                                                                                                      
        "hostvars": {                                                                                                                                                                                
            "laptop": {                                                                                                                                                                              
                "ansible_host": "127.0.0.1"                                                                                                                                                          
                "custom_fields": {},                                                                                                                                                                
                "device_roles": [                                                                                                                                                                    
                    "test"                                                                                                                                                                          
                ],      
                "device_types": [                                                                                                                                                                    
                    "xps-13"                                                                                                                                                                        
                ],                                                                                                                                                                                  
                "is_virtual": false,                                                                                                                                                                
                "local_context_data": [                                                                                                                                                              
                    null                                                                                                                                                                            
                ],                                                                                                                                                                                  
                "manufacturers": [                                                                                                                                                                  
                    "dell"                                                                                                                                                                          
                ],                                                                                                                                                                                  
                "primary_ip4": "127.0.0.1",                                                                                                                                                          
                "regions": [],                                                                                                                                                                      
                "services": [],                                                                                                                                                                      
                "sites": [                                                                                                                                                                          
                    "local"                                                                                                                                                                          
                ],                                                                                                                                                                                  
                "tags": []                                                                       
            }                                               
        }
    },
    "all": {                                                                                                                                                                                        
        "children": [                                                                                                                                                                                
            "ungrouped"                                                                                                                                                                              
        ]                                                                                                                                                                                            
    },                                                                                                                                                                                              
    "ungrouped": {                                                                                                                                                                                  
        "hosts": [                                                                                                                                                                                  
            "laptop"                                                                                                                                                                                
        ]                                                                                                                                                                                            
    }                                                                                                                                                                                                
} 

Ahhhh du monde, enfin !

Un peu d’ordre dans ce monde de groupe

En revanche, tous nos équipements se retrouvent dans un groupe nommé “ungrouped”, lui-même dans le groupe “all”, autrement dit … il n’y a pas de groupe.

Pas très grave pour une démo dans laquelle on n’aurait qu’un seul équipement, mais si vous avez un Netbox bien peuplé, vous allez sûrement vouloir organiser un peu mieux tout ça.

Remédions à ce souci grâce à l’option group_by :

netbox.yaml

---                                                                                                                                                                                                  
plugin: netbox.netbox.nb_inventory                                                                                                                                                                  
query_filters:                                                                                                                                                                                      
  - status: active                                                                                                                                                                                  
group_by:                                                                                                                                                                                            
  - device_roles                                                                                                                                                                                    
  - sites   

La liste des groupes devient :

$ ansible-inventory -i netbox.yml --graph                                                                                                                                                              
@all:                                                                                                                                                                                                
  |--@sites_local:                                                                                                                                                                                  
  |  |--laptop                                                                                                                                                                                      
  |--@device_roles_test:                                                                                                                                                                            
  |  |--laptop                                                                                                                                                                                      
  |--@ungrouped:  

On n’est pas bien là ? Non pas tout à fait ; le groupe sites_local passe encore, mais vais-je devoir me coltiner device_roles_ devant chaque groupe de devices_roles ? Pas nécessairement :

netbox.yml

---
plugin: netbox.netbox.nb_inventory                                                                                                                                                                  
query_filters:                                                                                                                                                                                      
  - status: active                                                                                                                                                                                  
group_by:                                                                                                                                                                                            
  - sites                                                                                                                                                                                            
keyed_groups:                                                                                                                                                                                        
  - prefix: ""                                                                                                                                                                                      
    separator: ""                                                                                                                                                                                    
    key: device_roles[0]  

La configuration peut paraître bizarre, mais elle est toutefois parfaitement fonctionnelle.

$ ansible-inventory -i netbox.yml --graph
@all: 
  |--@site_local:
  |  |--laptop
  |--@test:
  |  |--laptop
  |--@ungrouped:

Stockage du token avec Ansible-vault

Le plugin netbox.netbox.nb_inventory s’appuie sur les deux variables d’environnement que nous avons définies plus haut, NETBOX_URL et NETBOX_TOKEN.

Si on utilise un système comme Kubernetes, ou une plateforme de CI/CD, il y a généralement des moyens relativement simples pour définir des variables d’environnements ; mais dans le cas particulier d’Ansible, comment peut-on stocker ces variables (en particulier le token) de manière sécurisée ?

Ansible possède un système de stockage d’information sensible : “ansible-vault”. Nous n’allons pas rentrer dans le détail du setup d’ansible-vault , cela n’a que peu d’intérêt pour ce blog post, mais pour vous donner une petite idée, vous pouvez chiffrer votre token de la sorte :

$ ansible-vault encrypt_string "${NETBOX_TOKEN}" --name 'token''
token: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  XXXXXXXXXXXXXXXXXXX
  XXXXXXXXXXXXXXXXXXX

Puis l’intégrer au fichier d’inventaire:

netbox.yml

---
plugin: netbox.netbox.nb_inventory
api_endpoint: https://demo.netbox.dev/
token: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  XXXXXXXXXXXXXXXXXXX
  XXXXXXXXXXXXXXXXXXX                                                                                                                                                               
query_filters:                                                                                                                                                                                      
  - status: active                                                                                                                                                                                  
group_by:                                                                                                                                                                                            
  - sites                                                                                                                                                                                            
keyed_groups:                                                                                                                                                                                        
  - prefix: ""                                                                                                                                                                                      
    separator: ""                                                                                                                                                                                    
    key: device_roles[0]  

Pour aller plus loin

L’inventory permet d’avoir les informations génériques (nom, site, adresse IP principale, etc.) d’un device. En revanche, si on souhaite obtenir toutes les informations d’un device (par exemple, avoir accès à toutes les interfaces d’un serveur et non uniquement la principale), il va falloir faire des requêtes Netbox supplémentaires. Vous pouvez utiliser pour cela le lookup nb_lookup (livré avec la collection netbox.netbox) dans des rôles au moment où vous avez besoin de travailler avec.

Enfin, dans des infrastructures hybrides où du Cloud se mélange avec du “On-premises”, on peut faire en sorte de répliquer les informations du Cloud dans Netbox avec Terraform et son provider (il en existe d’autres mais c’est celui-là qui nous a donné le plus de satisfaction chez Enix).

Voilà, normalement vous avez toutes les billes pour mettre en place du Ansible avec un inventory Netbox ! Et potentiellement ensuite aussi les autres systèmes ?


Ne ratez pas nos prochains articles DevOps et Cloud Native! Suivez Enix sur Twitter!