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
| Colonna | Significato |
|---|---|
Nome | Nome della VM. Non può contenere \ / : * ? " < > | |
VMPath | Cartella della configurazione della VM. Deve esistere |
VHDPath | Cartella in cui creare i dischi. Deve esistere |
CPU | Numero di vCPU, da 1 al numero di processori logici dell’host |
RAM | Memoria di avvio. Minimo 32 MB, multiplo di 2 MB |
dischiDaCreare | Numero di dischi da creare (0 o più) |
Colonne opzionali
| Colonna | Significato |
|---|---|
discoNomeN | Nome logico del disco N (a partire da 0). Obbligatorio per ogni disco da creare |
discoDimensioneN | Dimensione del disco N. Ignorata per il disco 0 se è presente un template |
templatePath0 | File .vhdx da usare come disco 0 |
dvdPath | ISO da montare nel lettore DVD |
NomeNic1 | Suffisso del nome della scheda di rete |
SwitchNic1 | Virtual switch a cui collegare la scheda. Deve esistere |
VlanNic1 | VLAN 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>-NIC1seNomeNic1è 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:
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
| Parametro | Default | Descrizione |
|---|---|---|
-CsvPath | obbligatorio | Percorso del CSV. Deve essere un file esistente |
-Delimiter | , | Separatore del CSV |
-LogDirectory | .\Logs accanto allo script | Cartella per log e riepilogo |
-UseDifferencing | disattivo | Crea 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:
# 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:
- crea una VM di generazione 2 senza dischi, con la RAM di avvio indicata e la configurazione nella cartella
VMPath; - crea e collega i dischi richiesti;
- se indicato, aggiunge un lettore DVD con l’ISO montata;
- imposta il numero di vCPU;
- rinomina la scheda di rete creata di default;
- se indicato, collega la scheda a un virtual switch;
- se richiesto, imposta la VLAN in modalità access;
- imposta il boot order;
- 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.
- 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.
- 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”.
- 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.
- Validazione di tutte le righe. Ogni riga passa da
Test-VmDefinitionprima 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. - Creazione. Per ogni VM, in ordine: se esiste già una VM con quel nome viene saltata (esito Saltata); se è attivo
-WhatIfo l’utente risponde “No” a-Confirmviene 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. - 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
VMPatheVHDPathsiano 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
dischiDaCrearesia almeno 1; - per ogni disco, verifica la presenza di
discoNomeN, convertediscoDimensioneN(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:
| Condizione | Cosa fa |
|---|---|
Template e -UseDifferencing | New-VHD -Differencing con il template come padre |
Template senza -UseDifferencing | Copy-Item del template nella destinazione |
| Nessun template | New-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:
- rimuove la VM con
Remove-VM, che scollega anche i dischi; - 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-Confirmla 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
4GBo512 MBin 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
$nullse la colonna manca o se la cella è vuota o contiene solo spazi; applicaTrim()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 formatodata 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:
| Codice | Significato |
|---|---|
| 0 | Tutte le VM create, saltate perché già esistenti o simulate |
| 1 | Almeno una VM non è stata creata; le altre sì |
| 2 | Errore 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-VmDefinitionma 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:
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
Errore su #Requires per l’amministratore | Sessione non elevata | Aprire PowerShell come amministratore |
| Modulo Hyper-V mancante | Strumenti di gestione non installati | Install-WindowsFeature Hyper-V-PowerShell (server) o abilitare la funzionalità (client) |
| “Colonne obbligatorie mancanti” con colonne presenti | CSV con separatore punto e virgola | Usare -Delimiter ';' |
| “Il disco … esiste già” | File residuo di una prova precedente | Eliminarlo o cambiare discoNomeN |
| La VM da template Linux non si avvia | Secure Boot con il template Microsoft Windows | Set-VMFirmware -VMName <nome> -SecureBootTemplate MicrosoftUEFICertificateAuthority |
| Le VM differenziali non partono più | Template modificato o spostato | Ripristinare il template; in alternativa usare la copia |
| Messaggi “impossibile rimuovere …” nel rollback | File bloccato o permessi insufficienti | Rimuovere 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.

