Hyper-V: creare VM in automatico da CSV con PowerShell

Andrea Garattini  ·   ·  14 min di lettura

Quante volte dobbiamo creare un certo numero di VM sui nostri host Hyper-V? E quante volte mettiamo mano a sistemi esistenti e troviamo tutte le schede di rete con lo stesso nome, dischi dai nomi incomprensibili e percorsi delle VM a casaccio? Se vi riconoscete in almeno una di queste situazioni, questo articolo fa per voi.

New-HyperV-Vm.ps1 è uno script PowerShell che legge un file CSV, con una riga per ogni VM, e crea le macchine virtuali corrispondenti su un host Hyper-V. In questo articolo vediamo cosa fa, in che ordine lo fa e come si comporta nei vari casi. Lo scopo non è solo “fare” uno script (per quello, una volta che si sa bene cosa fare, ormai basta l’AI), ma vedere insieme come si costruisce uno script ben strutturato, con validazione, gestione degli errori e rollback.

Lo script è nato anni fa in PowerShell 5.1, quando mi sono trovato a creare un numero elevato di VM in un ambiente Hyper-V e ad accorgermi che, una volta create, regnava il caos: schede tutte chiamate “Network Adapter”, VHD di cui non si capiva il contenuto e così via. Lo stesso succedeva ogni volta che si preparava un lab o una demo, tutti insieme appassionatamente. Così, un passo alla volta, ho scritto uno script che prende in input un CSV (magari preparato in Excel) e crea le VM in modo ordinato, a prova del mio disordine.

Requisiti

Partiamo da cosa serve. Lo script è stato provato da Windows Server 2016 in poi, ma la versione del sistema operativo non è vincolante. È nato in Windows PowerShell 5.1 e funziona anche con PowerShell 7.x (almeno per ora).

In sintesi servono:

  • Windows Server o Windows client con il ruolo Hyper-V;
  • Windows PowerShell 5.1 o PowerShell 7.x;
  • il modulo PowerShell di Hyper-V;
  • una sessione avviata come amministratore;
  • una execution policy che consenta l’esecuzione dello script, ad esempio RemoteSigned.

Con RemoteSigned uno script scaricato da Internet resta bloccato finché non lo sblocchiamo con Unblock-File .\New-HyperV-Vm.ps1.

Modulo, versione e privilegi sono verificati da tre direttive #Requires in testa allo script: se una condizione non è soddisfatta, PowerShell non lo esegue e riporta il motivo.

Il file CSV

Il CSV ha una riga per VM e, com’è logico, la prima riga contiene i nomi delle colonne. Le colonne si dividono in obbligatorie e opzionali.

Colonne obbligatorie

ColonnaSignificato
NomeNome della VM. Non può contenere \ / : * ? " < > |
VMPathCartella della configurazione della VM. Deve esistere
VHDPathCartella in cui creare i dischi. Deve esistere
CPUNumero di vCPU, da 1 al numero di processori logici dell’host
RAMMemoria di avvio. Minimo 32 MB, multiplo di 2 MB
dischiDaCreareNumero di dischi da creare (0 o più)

Colonne opzionali

ColonnaSignificato
discoNomeNNome logico del disco N (a partire da 0). Obbligatorio per ogni disco da creare
discoDimensioneNDimensione del disco N. Ignorata per il disco 0 se è presente un template
templatePath0File .vhdx da usare come disco 0
dvdPathISO da montare nel lettore DVD
NomeNic1Suffisso del nome della scheda di rete
SwitchNic1Virtual switch a cui collegare la scheda. Deve esistere
VlanNic1VLAN ID, da 1 a 4094

Regole sui valori

  • RAM e dimensioni dei dischi si scrivono in byte (4294967296) oppure con le unità KB, MB, GB e TB (4GB, 512 MB, 1,5GB). La virgola è accettata come separatore decimale.
  • Ogni disco viene creato come <VHDPath>\<Nome>-<discoNomeN>.vhdx.
  • La scheda di rete si chiama <Nome>-<NomeNic1>, oppure <Nome>-NIC1 se NomeNic1 è vuoto.
  • Una cella opzionale vuota significa “non configurare”: senza SwitchNic1, ad esempio, la scheda resta scollegata.
  • Il delimitatore predefinito è la virgola. Io tengo sistemi operativi e applicativi in inglese, quindi per pigrizia lo script usa la virgola; un CSV salvato da Excel con impostazioni italiane, però, usa di solito il punto e virgola. A furia di lamentele da chi installa i sistemi in italiano, questa versione ha il parametro opzionale -Delimiter ';'.

Un CSV di esempio

Il CSV di esempio contiene due VM:

CSV
Nome,VMPath,VHDPath,CPU,RAM,dischiDaCreare,discoNome0,discoDimensione0,discoNome1,discoDimensione1,templatePath0,dvdPath,NomeNic1,SwitchNic1,VlanNic1
SRV-DEMO01,D:\Hyper-V\VM,D:\Hyper-V\VHD,2,4GB,2,OS,,DATA,50GB,D:\Hyper-V\Template\WS2022.vhdx,,LAN,vSwitch-LAN,100
SRV-DEMO02,D:\Hyper-V\VM,D:\Hyper-V\VHD,2,2GB,1,OS,60GB,,,,D:\ISO\WS2022.iso,LAN,vSwitch-LAN,

SRV-DEMO01 prende il disco di sistema dal template, ha un secondo disco dati vuoto da 50 GB ed è collegata al virtual switch vSwitch-LAN in VLAN 100. SRV-DEMO02 ha un solo disco vuoto da 60 GB, l’ISO montata e nessuna VLAN.

Parametri ed esempi di utilizzo

ParametroDefaultDescrizione
-CsvPathobbligatorioPercorso del CSV. Deve essere un file esistente
-Delimiter,Separatore del CSV
-LogDirectory.\Logs accanto allo scriptCartella per log e riepilogo
-UseDifferencingdisattivoCrea il disco da template come differenziale invece di copiarlo
-WhatIf—Valida e mostra cosa verrebbe creato, senza creare nulla
-Confirm—Chiede conferma prima di ogni VM
-Verbose—Mostra a video anche i messaggi di livello DEBUG

Con -UseDifferencing il disco 0 è un VHDX differenziale che ha il template come padre. Da quel momento il template non va modificato, spostato né cancellato, altrimenti le VM che ne dipendono non si avviano. È un’opzione da usare con estrema cautela.

Qualche esempio di invocazione:

PowerShell
# Simulazione
.\New-HyperV-Vm.ps1 -CsvPath .\vms.csv -WhatIf

# Creazione
.\New-HyperV-Vm.ps1 -CsvPath .\vms.csv

# CSV da Excel in italiano, log su share, dischi differenziali
.\New-HyperV-Vm.ps1 -CsvPath .\vms.csv -Delimiter ';' -LogDirectory '\\fileserver\logs\hyperv' -UseDifferencing

# Da schedulatore, leggendo il codice di uscita
pwsh -File .\New-HyperV-Vm.ps1 -CsvPath .\vms.csv
if ($LASTEXITCODE -ne 0) { Write-Warning "Creazione VM non completata (codice $LASTEXITCODE)" }

Come lavora lo script

Cosa crea per ogni VM

Per ogni riga del CSV lo script esegue, in quest’ordine:

  1. crea una VM di generazione 2 senza dischi, con la RAM di avvio indicata e la configurazione nella cartella VMPath;
  2. crea e collega i dischi richiesti;
  3. se indicato, aggiunge un lettore DVD con l’ISO montata;
  4. imposta il numero di vCPU;
  5. rinomina la scheda di rete creata di default;
  6. se indicato, collega la scheda a un virtual switch;
  7. se richiesto, imposta la VLAN in modalità access;
  8. imposta il boot order;
  9. scrive nel campo Note della VM chi l’ha creata, quando e con quale script.

La RAM è statica (niente memoria dinamica) e i dischi nuovi sono VHDX dinamici.

Il flusso di esecuzione

L’esecuzione è divisa in due blocchi distinti: una fase di sola lettura e validazione, e una fase di creazione.

  1. Preparazione del log. Lo script crea la cartella dei log e determina i nomi dei file. Se non ci riesce, termina subito con codice 2, senza toccare nulla.
  2. Lettura del CSV. Importa il file con il delimitatore indicato e termina con errore se il file è vuoto o se manca una delle colonne obbligatorie. Il messaggio riporta anche il delimitatore usato, che è la causa più frequente di “colonne mancanti”.
  3. Lettura dello stato dell’host. Una sola volta, all’inizio, legge il numero di processori logici, i nomi dei virtual switch e quelli delle VM esistenti. Questi dati valgono per tutte le righe.
  4. Validazione di tutte le righe. Ogni riga passa da Test-VmDefinition prima che venga creata qualsiasi VM. Gli errori di tutte le righe vengono raccolti e scritti nel log: se almeno una riga ha errori, lo script termina con codice 2 e non crea nulla.
  5. Creazione. Per ogni VM, in ordine: se esiste già una VM con quel nome viene saltata (esito Saltata); se è attivo -WhatIf o l’utente risponde “No” a -Confirm viene registrata come Simulata; altrimenti viene creata. In caso di errore viene eseguito il rollback, la VM è registrata come Fallita e lo script passa alla successiva.
  6. Chiusura. Un blocco finally, eseguito sempre, stampa la tabella dei risultati, scrive il CSV di riepilogo e la riga finale del log, poi termina con il codice di uscita.

La validazione

Test-VmDefinition non modifica nulla. Per ogni riga:

  • se la VM esiste già, la segna da saltare e non esegue gli altri controlli: lo script non aggiorna mai VM esistenti;
  • verifica che il nome sia presente e senza caratteri non validi;
  • verifica che VMPath e VHDPath siano cartelle esistenti;
  • verifica che la CPU sia un intero compreso tra 1 e il numero di processori logici dell’host;
  • converte la RAM in byte e verifica che sia almeno 32 MB e multiplo di 2 MB;
  • se c’è un template, verifica che il file esista e abbia estensione .vhdx, e che dischiDaCreare sia almeno 1;
  • per ogni disco, verifica la presenza di discoNomeN, converte discoDimensioneN (tranne per il disco 0 da template) e controlla che il file di destinazione non esista già;
  • se c’è un’ISO, verifica che il file esista;
  • se c’è SwitchNic1, verifica che il virtual switch esista;
  • se c’è una VLAN, verifica che sia un intero tra 1 e 4094.

Gli errori vengono accumulati: la validazione non si interrompe al primo. L’etichetta di ogni errore riporta il numero di riga del file (la riga 1 è l’intestazione, quindi i dati partono dalla 2) e il nome della VM. Dopo la validazione di tutte le righe, un controllo separato segnala i nomi duplicati nel CSV.

La funzione restituisce un oggetto Definition con i valori già convertiti (byte, percorsi dei dischi, nome della NIC), che la fase di creazione usa senza rielaborarli.

La creazione

New-VmFromDefinition esegue la sequenza descritta sopra. Per il disco 0 il comportamento dipende dalle opzioni:

CondizioneCosa fa
Template e -UseDifferencingNew-VHD -Differencing con il template come padre
Template senza -UseDifferencingCopy-Item del template nella destinazione
Nessun templateNew-VHD -Dynamic con la dimensione indicata

Gli altri dischi sono sempre VHDX dinamici e ognuno viene collegato con Add-VMHardDiskDrive.

La scheda di rete creata di default da New-VM viene individuata come primo adattatore della VM e rinominata, senza dipendere dal nome predefinito. Il boot order è costruito con il primo disco e il DVD, ma solo con i dispositivi effettivamente presenti.

Il rollback

Durante la creazione lo script tiene traccia di due informazioni: se la VM è stata creata e quali file di disco sono stati predisposti. Ogni percorso viene aggiunto all’elenco prima della creazione del file, così un’interruzione a metà lascia comunque un file noto al rollback.

Se un passo fallisce, Undo-PartialVm:

  1. rimuove la VM con Remove-VM, che scollega anche i dischi;
  2. elimina i file dei dischi registrati.

Ogni operazione ha un proprio try/catch e non rilancia errori: se qualcosa non può essere rimosso, il log riporta l’oggetto e l’indicazione di rimuoverlo a mano. Al termine viene rilanciato l’errore originale, così nel riepilogo la VM risulta Fallita con il messaggio corretto.

Gestione degli errori e -WhatIf

Lo script imposta Set-StrictMode -Version Latest e $ErrorActionPreference = 'Stop'. Il primo trasforma in errori l’uso di variabili non inizializzate e di proprietà inesistenti. Il secondo rende terminanti anche gli errori non terminanti dei cmdlet, così try/catch li intercetta e il rollback viene eseguito.

Per -WhatIf e -Confirm lo script usa SupportsShouldProcess e chiama $PSCmdlet.ShouldProcess una volta per VM, con due accorgimenti:

  • le scritture di log e riepilogo (Add-Content, New-Item, Export-Csv) usano -WhatIf:$false, altrimenti in simulazione non verrebbe scritto nulla;
  • le funzioni interne impostano $ConfirmPreference = 'None', così con -Confirm la domanda è una per VM e non una per ogni cmdlet.

Le funzioni di supporto

  • Write-Log: scrive su file e a video con il livello indicato.
  • ConvertTo-Bytes: converte stringhe come 4GB o 512 MB in byte con un’espressione regolare; accetta la virgola come separatore decimale e solleva un errore per i valori non riconosciuti.
  • Get-Field: legge una colonna del CSV e restituisce $null se la colonna manca o se la cella è vuota o contiene solo spazi; applica Trim() ai valori.
  • New-Result: costruisce la riga del riepilogo, in modo che tutte le righe abbiano le stesse colonne.

Output e codici di uscita

Oltre alle VM, lo script produce due output di controllo e scrive le note sulle VM.

  • Log. Un file per esecuzione, Logs\New-HyperV-Vm_<data-ora>.log, con una riga per messaggio nel formato data ora [LIVELLO] messaggio. I livelli sono INFO, SUCCESS, WARN, ERROR e DEBUG. A video sono colorati; DEBUG compare solo con -Verbose, ma nel file c’è sempre. Se la scrittura del log fallisce, lo script mostra un avviso e continua.
  • Riepilogo. Un CSV Logs\New-HyperV-Vm_<data-ora>-risultati.csv, separato da punto e virgola, con una riga per VM e le colonne VM, Esito, DurataSec e Messaggio. I valori possibili dell’esito sono Creata, Fallita, Saltata e Simulata.
  • Note della VM. Nel campo Note di ogni VM creata vengono scritti lo script, la data e l’utente che l’ha creata.

Il codice di uscita permette di usare lo script anche da uno schedulatore o da una pipeline:

CodiceSignificato
0Tutte le VM create, saltate perché già esistenti o simulate
1Almeno una VM non è stata creata; le altre sì
2Errore fatale: CSV non valido, validazione fallita o log non scrivibile. Nessuna VM creata

Limiti e risoluzione dei problemi

Comportamenti da conoscere

Lo script è nato per rispondere a esigenze personali ed è cresciuto un po’ alla volta. Nella versione attuale ci sono quindi aspetti che non gestisce, o che gestisce in modo specifico (cioè secondo le mie esigenze).

  • VM esistenti. Una VM con lo stesso nome viene saltata senza confrontarne la configurazione con il CSV. Il confronto sul nome non distingue maiuscole e minuscole.
  • Disco 0 da template. Il disco viene copiato o collegato così com’è: la colonna discoDimensione0 è ignorata e non viene eseguito alcun ridimensionamento.
  • -UseDifferencing. Il parametro è passato a Test-VmDefinition ma non viene ancora usato: non ci sono controlli specifici sul template, come l’attributo di sola lettura o l’uso da parte di altre VM. In pratica, il supporto ai dischi differenziali è ancora in evoluzione.
  • Finestra tra validazione e creazione. Il controllo “il disco non esiste” avviene in validazione. Se nel frattempo compare un file con lo stesso percorso, il rollback potrebbe rimuoverlo, perché quel percorso è già nell’elenco dei file da pulire.
  • Rollback e cartella della VM. Il rollback elimina la VM e i dischi registrati, ma non la cartella creata sotto VMPath.
  • Controlli assenti. Non vengono verificati lo spazio libero sul volume dei VHD, la memoria totale dell’host e la lunghezza del nome della VM (quest’ultimo, a dire il vero, non dovrebbe essere un problema).
  • Configurazione fissa. Solo generazione 2, RAM statica e una sola scheda di rete; nessuna impostazione del template di Secure Boot (necessaria per alcune VM Linux), del TPM, delle azioni di avvio automatico o dei checkpoint. Sono tutti candidati per le prossime versioni.
  • Ambito. Lo script lavora sull’host locale e non supporta host remoti né cluster. Su questo punto ci sto già lavorando.
  • Numerazione. I dischi sono numerati da 0 (discoNome0), la scheda di rete da 1 (NomeNic1). Lo so, è un’incoerenza, ma se vi dà fastidio bastano pochi secondi per sistemarla.

Risoluzione dei problemi

Ecco i problemi più comuni e come risolverli:

SintomoCausa probabileSoluzione
Errore su #Requires per l’amministratoreSessione non elevataAprire PowerShell come amministratore
Modulo Hyper-V mancanteStrumenti di gestione non installatiInstall-WindowsFeature Hyper-V-PowerShell (server) o abilitare la funzionalità (client)
“Colonne obbligatorie mancanti” con colonne presentiCSV con separatore punto e virgolaUsare -Delimiter ';'
“Il disco … esiste già”File residuo di una prova precedenteEliminarlo o cambiare discoNomeN
La VM da template Linux non si avviaSecure Boot con il template Microsoft WindowsSet-VMFirmware -VMName <nome> -SecureBootTemplate MicrosoftUEFICertificateAuthority
Le VM differenziali non partono piùTemplate modificato o spostatoRipristinare il template; in alternativa usare la copia
Messaggi “impossibile rimuovere …” nel rollbackFile bloccato o permessi insufficientiRimuovere a mano gli oggetti indicati nel log

Conclusioni

New-HyperV-Vm.ps1 esegue una sequenza precisa: legge il CSV, valida tutte le righe contro lo stato reale dell’host, crea le VM una alla volta e riporta l’esito in log, riepilogo e codice di uscita. Le sue regole (validazione preventiva, VM esistenti saltate, rollback per singola VM, un errore che non blocca le altre) determinano cosa succede sull’host quando qualcosa va storto. Conoscerle è il modo più rapido per capire se lo script è adatto al proprio ambiente o se conviene adattarlo alle proprie esigenze.

windows serverpowershellhyper-v
Condividi:LinkedIn ·X

Da leggere anche