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
ClusterUUIDdoit ê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
- Nutanix Move REST APIs nutanix.dev
- Expérience directe de l’auteur, en production (versions ci-dessus)
