Aller au contenu
5 minutes

Piloter Nutanix Move par API : ce que la doc ne dit pas

Listes en POST, ClusterUUID obligatoire, seeding lancé dès la création, cutover jamais armé : ce qu’on découvre en pilotant Move 6.1.1 par son API REST v2.

Piloter Nutanix Move par API : ce que la doc ne dit pas

L’API REST v2 de Nutanix Move permet de créer des plans de migration en série sans passer par l’UI, à condition de connaître les comportements que la référence ne décrit pas. Tout ce qui suit a été observé sur Move 6.1.1 (API 2.3.0), pendant une campagne de migrations AHV vers AHV.

Doc et terrain : pas la même version

La référence publiée sur nutanix.dev décrit l’API 2.7.0, celle de Move 6.3.3, avec une authentification JWT « bearer » dans l’en-tête Authorization. Rien ci-dessous n’a été vérifié sur 6.3.3 : refaites chaque test sur votre version avant d’industrialiser.

Premier écart concret : sur 6.1.1, le jeton renvoyé par le login (Status.Token) passe tel quel dans Authorization, sans préfixe Bearer.

curl -s -X POST "https://<move>/move/v2/users/login" \
  -H "Content-Type: application/json" \
  -d "{\"Spec\":{\"UserName\":\"nutanix\",\"Password\":\"$MOVE_PASSWORD\"}}"
# observé sur 6.1.1 :   -H "Authorization: <jeton>"
# doc 2.7.0 (bearer) :  -H "Authorization: Bearer <jeton>"

Les listes sont des POST

Tout ce qui liste passe en POST ; un GET répond 405.

POST /move/v2/providers/list
POST /move/v2/plans/list?IncludeVMDetails=true
POST /move/v2/providers/{uuid}/workloads/list

La doc 2.7.0 documente elle aussi plans/list en POST. Autre surprise : cette liste ne renvoie que la MetaData de chaque plan (Name, State, Actions, UUID), jamais sa Spec.

Créer un plan : le ClusterUUID introuvable

La création passe par POST /move/v2/plans avec un corps {"Spec":{...}}. Le champ SourceInfo.AOSProviderAttrs.ClusterUUID est obligatoire, mais le GET d’un plan existant ne le renvoie pas : impossible de relire un plan pour le recopier, il faut fournir cette valeur soi-même.

Pour de l’AHV vers AHV sans NGT, les réglages retenus sont GuestPrepMode: manual, NicConfigMode: retain et RetainMacAddress: true.

Créer, c’est démarrer

Pour différer une réplication, planifiez l’exécution du script de création (tâche planifiée), pas le plan. Une fois le plan en cours, un PUT est refusé (409), et PAUSE comme ABORT ne sont disponibles que dans l’UI sur cette version.

Le démarrage explicite existe aussi : POST /move/v2/plans/{uuid}/start, corps vide, répond 202, et le plan passe en Validation puis InProgress.

Suivre la réplication

États observés : MigrationPlanStateUninitialized, puis Validation, puis InProgress avec des VMStateCounts (DataSeeding, puis ReadyToCutover). Le début réel de la réplication se lit dans MetaData.Schedule.RWStartTimeAtEpochSec, en nanosecondes, absent tant que rien n’a démarré.

import datetime as dt

# plan : une entité renvoyée par POST /move/v2/plans/list
ns = plan["MetaData"].get("Schedule", {}).get("RWStartTimeAtEpochSec")
debut = dt.datetime.fromtimestamp(ns / 1e9, tz=dt.timezone.utc) if ns else None

Le cutover, lui, n’est pas armé par le planning : chaque VM porte ScheduleCutover.ScheduleCutoverFlag: false. Un plan qui seed ne bascule jamais seul. Si une VM échoue, son RETRY reprend la copie là où elle en était, compteur de données copiées conservé.

Rafraîchir l’inventaire, avec modération

Une VM créée après le dernier inventaire reste invisible du provider source. Le rafraîchissement qui fonctionne est global, sur tous les providers :

curl -s -X POST "https://<move>/move/v2/providers/list" \
  -H "Authorization: $MOVE_TOKEN" -H "Content-Type: application/json" \
  -d '{"RefreshInventory": true}'

L’appel HTTP expire au bout d’environ 3 minutes, mais le rafraîchissement continue côté appliance, à raison d’environ 1 à 2 minutes par provider : suivez LastInventoryRefreshTime. À l’inverse, GET /providers/{uuid}?RefreshInventory=true répond 200 sans rien rafraîchir.

Ce qu’il faut retenir

  • La doc nutanix.dev décrit l’API 2.7.0 (Move 6.3.3) en bearer ; sur 6.1.1 le jeton passe brut : testez sur votre version.
  • Les listes sont des POST, et ClusterUUID doit être fourni à la création car le GET d’un plan ne le renvoie pas.
  • Créer un plan lance le seeding : pour différer, planifiez le script, pas le plan.
  • Le cutover n’est jamais armé par le planning ; RWStartTimeAtEpochSec (nanosecondes) donne le vrai début de réplication.
  • Un seul rafraîchissement d’inventaire global à la fois, suivi par LastInventoryRefreshTime.

In English

Lists are POST calls, ClusterUUID is mandatory, seeding starts as soon as a plan is created, and cutover is never armed: field notes from driving Move 6.1.1 through its REST API v2. A GET on a list returns 405, ScheduleAtEpochSec is not honored, and a plan that is seeding never switches over on its own.

Sources & vérification

Testé sur : Move 6.1.1 (API 2.3.0), migrations AHV vers AHV

Comment les billets sont vérifiés et corrigés