Un inventaire tenu à la main finit par mentir. Généré par l’API v4 de Prism Central, il se refait en une commande, à condition de joindre sur les bonnes clés et de ne pas laisser Excel écraser le fichier.
Pourquoi générer
Dans un classeur d’inventaire saisi à la main, la colonne vitesse réseau (10G ou 25G) s’est révélée fausse, parfois inversée. L’API rend le débit négocié de chaque carte, linkSpeedInKbps, sur /api/clustermgmt/v4.0/config/clusters/{clusterExtId}/hosts/{hostExtId}/host-nics : observé 25000000 pour 25 Gb/s. Sur deux clusters, le champ était nul alors que les ports étaient up ; la vitesse se contrôle alors depuis une CVM avec manage_ovs show_interfaces, qui affiche l’état du lien et la vitesse de chaque carte (show_uplinks, lui, montre les bonds).
Quatre listes, une feuille chacune
Chaque objet vient d’une liste paginée de Prism Central v4, dont le SDK documente le chemin :
- clusters :
/api/clustermgmt/v4.0/config/clusters; - hôtes :
/api/clustermgmt/v4.0/config/hosts; - VM :
/api/vmm/v4.0/ahv/config/vms; - catégories :
/api/prism/v4.0/config/categories.
Les pages plafonnent à 100 éléments : on boucle sur $page jusqu’à totalAvailableResults (détail dans ce billet).
Le piège des jointures
Tout se joint par UUID, jamais par nom : deux VM peuvent porter le même (voir ici). Mais le champ change de nom selon l’API. Une VM désigne son cluster par cluster.extId ; un hôte, par cluster.uuid, accompagné de cluster.name. Même valeur, deux chemins. Les catégories d’une VM ne sont que des extId : la liste des catégories les traduit en clé:valeur.
Le script écrit les feuilles VM et Hôtes ; clusters et catégories suivent le même modèle.
import os
import requests
from openpyxl import Workbook
PC = os.environ["PC_HOST"]
s = requests.Session()
s.auth = (os.environ["PC_USER"], os.environ["PC_PASSWORD"])
s.verify = os.environ.get("PC_CA_BUNDLE") or True # bundle dédié, sinon magasin par défaut ; jamais False
def lister(chemin):
items, page = [], 0
while True:
r = s.get(f"https://{PC}:9440/api/{chemin}",
params={"$page": page, "$limit": 100}, timeout=60)
r.raise_for_status()
corps = r.json()
data = corps.get("data") or []
items += data
total = (corps.get("metadata") or {}).get("totalAvailableResults")
if not data or len(data) < 100 or (total is not None and len(items) >= total):
return items
page += 1
def sur(v):
if isinstance(v, str) and v.startswith("="):
raise ValueError(f"chaîne prise pour une formule : {v[:30]!r}")
return v
clusters = {c["extId"]: c.get("name") for c in lister("clustermgmt/v4.0/config/clusters")}
hotes = {h["extId"]: h for h in lister("clustermgmt/v4.0/config/hosts")}
cats = {c["extId"]: f'{c.get("key")}:{c.get("value")}' for c in lister("prism/v4.0/config/categories")}
vms = lister("vmm/v4.0/ahv/config/vms")
wb = Workbook()
ws = wb.active
ws.title = "VM"
ws.append(["UUID", "Nom", "Cluster", "Hôte", "Catégories"])
for vm in vms:
hote = hotes.get((vm.get("host") or {}).get("extId"), {})
ws.append([sur(x) for x in (
vm["extId"],
vm.get("name"),
clusters.get((vm.get("cluster") or {}).get("extId"), "?"), # VM : cluster.extId
hote.get("hostName", ""),
", ".join(cats.get(c.get("extId"), "?") for c in vm.get("categories") or []),
)])
ws.auto_filter.ref = f"A1:E{ws.max_row}"
wh = wb.create_sheet("Hôtes")
wh.append(["UUID", "Nom", "Cluster", "Modèle", "CPU", "Cœurs", "RAM (Gio)"])
for h in hotes.values():
wh.append([sur(x) for x in (
h["extId"],
h.get("hostName"),
(h.get("cluster") or {}).get("name"), # hôte : cluster.uuid et name
h.get("blockModel"),
h.get("cpuModel"),
h.get("numberOfCpuCores"),
round((h.get("memorySizeBytes") or 0) / 2**30),
)])
wb.save(os.environ["XLSX_TMP"]) # fichier temporaire, comparé puis renommé
Ce qu’openpyxl ne vous dira pas
La documentation d’openpyxl écrit une formule en affectant une chaîne qui commence par =, et précise que la bibliothèque n’évalue jamais les formules. Conséquence vécue : une note de bas de tableau commençant par => est partie comme formule ; Excel l’a jugée invalide, a affiché à l’ouverture « Enregistrements supprimés : Formule dans la partie /xl/worksheets/sheetN.xml » et effacé la cellule, sans que le script ait rien signalé. Le correctif a été d’écrire -> et de relire le classeur pour lister les cellules commençant par =. La fonction sur() du script va plus loin : elle refuse l’écriture plutôt que de laisser passer. De même, auto_filter ajoute le filtre au fichier sans rien filtrer : Excel l’applique.
Planifier sans se faire écraser
Deux observations à garder en tête si le script passe en tâche planifiée :
- un classeur ouvert dans Excel bloque même la lecture par openpyxl (
PermissionError: [Errno 13]), et le verrou~$n’est pas toujours visible ; - un Excel resté ouvert sur une ancienne version a, en sauvegardant, réécrit le fichier régénéré dans la matinée : plusieurs centaines de cellules revenues en arrière, sans message. Sa date de modification, plus récente que le script, ne prouvait rien.
La méthode qui en est sortie : générer dans un fichier temporaire (save écrase sans avertir, dit la doc d’openpyxl), comparer cellule par cellule avec la version en place, remplacer, puis garder l’empreinte de la version validée. Identifiants dans l’environnement ou un coffre, tâche planifiée nommée ou cron : rien de caché.
Ce qu’il faut retenir
- Quatre listes v4, paginées par 100 : clusters, hôtes, VM, catégories.
- Jointures par UUID : la VM porte
cluster.extId, l’hôtecluster.uuid. - Une chaîne qui commence par
=devient une formule : refusez-la avant écriture. - Générez dans un fichier temporaire, comparez, puis remplacez.
- La vitesse réseau se lit dans
linkSpeedInKbps, pas dans une colonne saisie à la main.
In English
Clusters, hosts, VMs and categories in four paginated lists, joined by UUID and written with openpyxl. And the pitfalls: join keys that change from one API to another, and a workbook overwritten by Excel. A VM refers to its cluster through cluster.extId but a host through cluster.uuid, and openpyxl writes any string starting with = as a formula.
Sources & vérification
Testé sur : Prism Central de production, API v4.0, 2026
- Nutanix v4 Clustermgmt SDK, ListClustersApiResponse developers.nutanix.com
- Nutanix v4 Clustermgmt SDK, ListHostsApiResponse developers.nutanix.com
- Nutanix v4 Clustermgmt SDK, ClustersApi developers.nutanix.com
- Nutanix v4 Clustermgmt SDK, modèle Host developers.nutanix.com
- Nutanix v4 Clustermgmt SDK, ClusterReference developers.nutanix.com
- Nutanix v4 Clustermgmt SDK, modèle HostNic developers.nutanix.com
- Nutanix v4 VMM SDK, ListVmsApiResponse developers.nutanix.com
- Nutanix v4 VMM SDK, modèle Vm developers.nutanix.com
- Nutanix v4 VMM SDK, ClusterReference developers.nutanix.com
- Nutanix v4 VMM SDK, CategoryReference developers.nutanix.com
- Nutanix v4 Prism SDK, CategoriesApi developers.nutanix.com
- Nutanix v4 Prism SDK, ListCategoriesApiResponse developers.nutanix.com
- Nutanix API User Guide (nutanix.dev) nutanix.dev
- openpyxl 3.1.3, Simple Formulae openpyxl.readthedocs.io
- openpyxl 3.1.3, Tutorial openpyxl.readthedocs.io
- openpyxl 3.1.3, Filters openpyxl.readthedocs.io
- Nutanix Community, Setting NIC speed (2021) next.nutanix.com
- vStellar, AHV Networking Part 1, Basics (2019) vstellar.com
- Expérience directe de l’auteur, en production (versions ci-dessus)
