
Quando si amministrano ambienti virtualizzati su VMware vSphere, capita spesso di dover esportare una VM come pacchetto OVF/OVA — ad esempio per un backup “a freddo”, per spostarla su un altro sito, o per consegnarla a un fornitore/partner come immagine autonoma. Dal client vSphere l’operazione è semplice: tasto destro sulla VM, “Export System Image”, e — se serve un pacchetto davvero fedele all’originale si spuntano le opzioni avanzate “Include BIOS UUID”, “Include MAC addresses” e “Include extra configuration”.
Il problema nasce quando questa esportazione va ripetuta più volte, magari su VM diverse o su vCenter diversi, e si vuole evitare di rifare manualmente tutti i click ogni volta — con il rischio di dimenticare proprio una di quelle tre checkbox avanzate, che sono facili da tralasciare ma spesso indispensabili (ad esempio per VM di sincronizzazione/DR che devono mantenere lo stesso BIOS UUID e gli stessi MAC address quando vengono re-importate altrove). Lo script che segue automatizza l’intero processo da PowerShell, usando PowerCLI per il lookup della VM e la gestione dello stato di accensione, e VMware OVF Tool per l’export vero e proprio — perché è l’unico strumento che espone il flag –exportFlags necessario per includere quelle opzioni avanzate. Il cmdlet PowerCLI Export-VApp, da solo, non le supporta.
PREREQUISITI
- PowerShell 5.1 o superiore, su Windows.
- Modulo VMware.PowerCLI (lo script lo installa automaticamente per l’utente corrente se non è già presente).
- VMware OVF Tool installato sulla macchina che esegue lo script. Non è incluso in PowerCLI: va scaricato a parte dal portale supporto Broadcom/VMware. Percorso tipico di installazione: C:\Program Files\VMware\VMware OVF Tool\ovftool.exe.
- Accesso in scrittura al percorso di destinazione (locale o condivisione UNC).
- Permessi su vCenter sufficienti per l’export: ruolo Administrator, oppure un ruolo custom con i privilegi Virtual machine > Provisioning > Allow disk access / Allow read-only disk access.
NOTA BENE: per installare PowerCLI manualmente, da una console PowerShell (non necessariamente elevata, basta lo scope utente) eseguire:
|
0 |
Install-Module VMware.PowerCLI -Scope CurrentUser -Force
|
FUNZIONALITÀ DELLO SCRIPT
Lo script fa le seguenti cose:
- Chiede quale vCenter usare tramite un menu (1/2/3), oppure accetta la scelta come parametro per esecuzioni non interattive.
- Chiede il nome esatto della VM da esportare, se non passato come parametro.
- Si autentica sempre con l’utente Windows correntemente loggato sulla macchina: nessuna username da digitare. Prova prima l’autenticazione SSO/SSPI silenziosa (zero prompt); se il vCenter non ha questa funzionalità abilitata lato SSO, ricade automaticamente su un prompt Get-Credential già precompilato con l’utente corrente.
- Riusa la stessa credenziale raccolta per PowerCLI anche per ovftool, evitando di chiederla una seconda volta. Se invece l’SSO/SSPI di PowerCLI ha funzionato (zero prompt), per ovftool resta comunque necessaria una password, perché — a differenza di PowerCLI — non ha alcun equivalente dell’autenticazione integrata di Windows: è un limite del tool, non dello script.
- Verifica che la destinazione sia scrivibile prima di toccare vCenter (fail-fast), creando la cartella se non esiste.
- Individua la VM su vCenter e ne controlla lo stato di accensione.
- Spegne opzionalmente la VM prima dell’export, per garantirne la consistenza: prova prima uno shutdown “gentile” del guest OS (via VMware Tools), e solo se non va a buon fine entro un timeout ricorre a uno spegnimento forzato (Stop-VM).
- Esegue l’export vero e proprio con ovftool, includendo per default tutte e tre le opzioni avanzate (–exportFlags=mac,uuid,extraconfig), replicando esattamente quanto fa il client vSphere con “Enable advanced options” spuntato su tutte e tre le voci.
- Riaccende la VM al termine, se richiesto e se era accesa prima dell’intervento dello script.
- Supporta -WhatIf / -Confirm su tutte le operazioni che modificano lo stato (spegnimento/accensione, scrittura dei file), così è possibile simulare l’esecuzione prima di lanciarla sul serio.
- Tiene un log timestampato di tutta l’operazione su file, oltre che a video.
Per impostazione predefinita lo script non spegne la VM: esporta lo stato “a caldo” (ovftool esegue comunque l’export, ma senza garanzia di consistenza applicativa se non c’è un quiescing via VMware Tools). Per un export garantito consistente si usa il parametro -PowerOffFirst (con eventuale riaccensione al termine tramite -PowerOnAfter).
SCRIPT COMPLETO
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
|
#Requires -Version 5.1
<#
.SYNOPSIS
Esporta una VM da vCenter come pacchetto OVF/OVA su un percorso di rete
(UNC), includendo le stesse opzioni avanzate del client vSphere (Include
BIOS UUID, Include MAC addresses, Include extra configuration).
.PARAMETER VCenterChoice
"1", "2" o "3" per scegliere il vCenter senza il prompt interattivo:
1 = vcenter01.azienda.local
2 = vcenter02.azienda.local
3 = vcenter03.azienda.local
Se omesso, lo script mostra il menu e chiede la scelta.
.PARAMETER VMName
Nome esatto della VM da esportare. Se omesso, lo script lo chiede
interattivamente.
.PARAMETER DestinationPath
Cartella di destinazione (UNC o locale). Il file descrittore
(<VMName>.ovf o <VMName>.ova) verra' creato al suo interno; per il
formato Ovf, i file .mf e .vmdk vengono scritti nella stessa cartella.
Puoi anche indicare direttamente il percorso file completo.
.PARAMETER Format
"Ovf" (default, produce .ovf + .mf + uno o piu' .vmdk in una cartella) o
"Ova" (un unico file .ova).
.PARAMETER ExportFlags
Opzioni avanzate da includere nel pacchetto OVF, equivalenti alle
checkbox del client vSphere in "Enable advanced options". Default: tutte
e tre — @('mac','uuid','extraconfig') — cioe' Include MAC addresses,
Include BIOS UUID, Include extra configuration.
.PARAMETER OvftoolPath
Percorso di ovftool.exe. Se omesso, lo script cerca nelle posizioni di
installazione standard e nel PATH di sistema.
.PARAMETER ShareCredential
Credenziali opzionali da usare per autenticarsi sulla share di rete di
destinazione, se l'utente corrente non ha già accesso al percorso.
.PARAMETER PowerOffFirst
Se presente, spegne la VM (prima gentilmente via Guest OS, poi Stop-VM se
necessario) prima di esportarla, per garantire un export consistente.
.PARAMETER PowerOnAfter
Se presente insieme a -PowerOffFirst, riaccende la VM al termine
dell'export (solo se era accesa prima dello spegnimento fatto dallo script).
.PARAMETER Force
Sovrascrive eventuali file di export già presenti nella destinazione
(--overwrite di ovftool).
.EXAMPLE
.\Export-VM-OVF.ps1
Modalità completamente interattiva: chiede il vCenter (menu 1/2/3) e il
nome della VM, poi esporta in "\\fileserver01.azienda.local\export\ProgettoX".
.EXAMPLE
.\Export-VM-OVF.ps1 -VCenterChoice 2 -VMName "SRV-PROD-SYNC-01" -PowerOffFirst -PowerOnAfter -Force
Nessun prompt per vCenter/VM name (passati come parametro); chiede solo
le credenziali se necessario.
.NOTES
Prerequisiti:
- Modulo VMware.PowerCLI (per il lookup della VM e la gestione power state).
- VMware OVF Tool installato sulla macchina che esegue lo script
(download dal portale supporto Broadcom/VMware — non e' incluso in
PowerCLI). Percorso tipico: "C:\Program Files\VMware\VMware OVF Tool\ovftool.exe".
- Accesso in scrittura al percorso di destinazione.
- Permessi vCenter sufficienti per l'export (ruolo Administrator o
ruolo custom con i privilegi "Virtual machine > Provisioning >
Allow disk access / Allow read-only disk access").
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory = $false)]
[ValidateSet('1', '2', '3')]
[string]$VCenterChoice = "",
[Parameter(Mandatory = $false)]
[string]$VMName = "",
[Parameter(Mandatory = $false)]
[string]$DestinationPath = "\\fileserver01.azienda.local\export\ProgettoX",
[Parameter(Mandatory = $false)]
[ValidateSet("Ovf", "Ova")]
[string]$Format = "Ovf",
[Parameter(Mandatory = $false)]
[string[]]$ExportFlags = @('mac', 'uuid', 'extraconfig'),
[Parameter(Mandatory = $false)]
[string]$OvftoolPath = "",
[Parameter(Mandatory = $false)]
[System.Management.Automation.PSCredential]$ShareCredential,
[Parameter(Mandatory = $false)]
[switch]$PowerOffFirst,
[Parameter(Mandatory = $false)]
[switch]$PowerOnAfter,
[Parameter(Mandatory = $false)]
[switch]$Force,
[Parameter(Mandatory = $false)]
[int]$GuestShutdownTimeoutSec = 180
)
$ErrorActionPreference = 'Stop'
# Tutte le operazioni usano sempre l'utente Windows correntemente loggato.
$CurrentUserForAuth = "$env:USERDOMAIN\$env:USERNAME"
# ==============================================================================
# Selezione vCenter (menu 1/2/3) — puoi anche passare -VCenterChoice 1|2|3
# per saltare il prompt in esecuzioni non interattive.
# ==============================================================================
$VCenterOptions = [ordered]@{
'1' = 'vcenter01.azienda.local'
'2' = 'vcenter02.azienda.local'
'3' = 'vcenter03.azienda.local'
}
if (-not $VCenterOptions.Contains($VCenterChoice)) {
Write-Host ""
Write-Host "Seleziona il vCenter:" -ForegroundColor Cyan
foreach ($key in $VCenterOptions.Keys) {
Write-Host (" {0}) {1}" -f $key, $VCenterOptions[$key])
}
do {
$VCenterChoice = Read-Host "Scelta (1-3)"
} while (-not $VCenterOptions.Contains($VCenterChoice))
}
$vCenterServer = $VCenterOptions[$VCenterChoice]
# ==============================================================================
# Nome della VM da esportare, se non passato come parametro
# ==============================================================================
if ([string]::IsNullOrWhiteSpace($VMName)) {
do {
$VMName = Read-Host "Nome esatto della VM da esportare"
} while ([string]::IsNullOrWhiteSpace($VMName))
}
$LogFile = Join-Path (Get-Location).Path "Export-VM-OVF_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$MappedDriveName = $null
$vmWasPoweredOn = $false
$global:VIConnection = $null
$SharedCredential = $null # riusata per ovftool se raccolta durante il fallback PowerCLI, per non chiedere due volte
# ==============================================================================
# Funzioni helper
# ==============================================================================
function Write-Log {
param(
[Parameter(Mandatory = $true)][string]$Message,
[Parameter(Mandatory = $false)][ValidateSet('INFO', 'WARN', 'ERROR', 'OK')][string]$Level = 'INFO'
)
$line = "[{0}] [{1}] {2}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $Message
switch ($Level) {
'WARN' { Write-Host $line -ForegroundColor Yellow }
'ERROR' { Write-Host $line -ForegroundColor Red }
'OK' { Write-Host $line -ForegroundColor Green }
default { Write-Host $line }
}
try { Add-Content -Path $LogFile -Value $line -Encoding UTF8 } catch { }
}
function Test-DestinationWritable {
<#
Verifica che $Path esista (creandola se possibile) e sia scrivibile,
scrivendo e rimuovendo un piccolo file di prova. Funziona sia su
percorsi locali sia su UNC.
#>
param([Parameter(Mandatory = $true)][string]$Path)
if (-not (Test-Path -Path $Path)) {
Write-Log "La destinazione '$Path' non esiste: provo a crearla..." -Level WARN
New-Item -Path $Path -ItemType Directory -Force -ErrorAction Stop | Out-Null
}
$probeFile = Join-Path $Path (".write_test_$([guid]::NewGuid().ToString('N')).tmp")
try {
[IO.File]::WriteAllText($probeFile, "test")
Remove-Item -Path $probeFile -Force -ErrorAction SilentlyContinue
}
catch {
throw "Impossibile scrivere in '$Path': $($_.Exception.Message). Verifica i permessi sulla share o usa -ShareCredential."
}
}
function Wait-ForGuestShutdown {
param(
[Parameter(Mandatory = $true)]$VM,
[Parameter(Mandatory = $true)][int]$TimeoutSec
)
$elapsed = 0
while ($elapsed -lt $TimeoutSec) {
Start-Sleep -Seconds 5
$elapsed += 5
$current = Get-VM -Id $VM.Id
if ($current.PowerState -eq 'PoweredOff') { return $true }
}
return $false
}
function Get-InventoryFolderPath {
<#
Ricostruisce il percorso di inventory della VM (Datacenter e catena
di cartelle) necessario per il locator vi:// di ovftool, risalendo i
MoRef dei folder padre fino alla cartella "vm" radice del datacenter.
#>
param([Parameter(Mandatory = $true)]$VM)
$dc = Get-Datacenter -VM $VM -ErrorAction Stop
$vmFolderMoRefValue = $dc.ExtensionData.VmFolder.Value
$pathParts = [System.Collections.Generic.List[string]]::new()
$currentMoRef = $VM.ExtensionData.Parent
$guard = 0
while ($currentMoRef -and $currentMoRef.Value -ne $vmFolderMoRefValue -and $guard -lt 50) {
$folderView = Get-View -Id $currentMoRef -Property Name, Parent -ErrorAction Stop
$pathParts.Insert(0, $folderView.Name)
$currentMoRef = $folderView.Parent
$guard++
}
return [PSCustomObject]@{
Datacenter = $dc.Name
FolderPath = ($pathParts -join '/')
}
}
function Resolve-OvfToolPath {
<#
Individua ovftool.exe: usa il percorso fornito se valido, altrimenti
prova le posizioni di installazione standard, altrimenti cerca nel PATH.
#>
param([Parameter(Mandatory = $false)][string]$PreferredPath)
if ($PreferredPath -and (Test-Path -Path $PreferredPath -PathType Leaf)) {
return $PreferredPath
}
$candidates = @()
if ($env:ProgramFiles) { $candidates += Join-Path $env:ProgramFiles 'VMware\VMware OVF Tool\ovftool.exe' }
if (${env:ProgramFiles(x86)}) { $candidates += Join-Path ${env:ProgramFiles(x86)} 'VMware\VMware OVF Tool\ovftool.exe' }
foreach ($candidate in $candidates) {
if (Test-Path -Path $candidate -PathType Leaf) { return $candidate }
}
$cmd = Get-Command -Name ovftool.exe -ErrorAction SilentlyContinue
if (-not $cmd) { $cmd = Get-Command -Name ovftool -ErrorAction SilentlyContinue }
if ($cmd) { return $cmd.Source }
throw "ovftool non trovato. Installa 'VMware OVF Tool' (Broadcom/VMware support portal) oppure specifica il percorso con -OvftoolPath."
}
function Ensure-PowerCLIModule {
<#
Verifica che il modulo VMware.PowerCLI sia importato nella sessione
corrente; se manca del tutto sulla macchina, prova ad installarlo per
l'utente corrente da PowerShell Gallery. Senza questo controllo,
cmdlet come Set-PowerCLIConfiguration/Connect-VIServer falliscono con
"term ... is not recognized" su qualunque macchina dove PowerCLI non
sia già stato installato/importato in precedenza.
#>
if (Get-Command -Name Connect-VIServer -ErrorAction SilentlyContinue) {
return # già importato in questa sessione
}
Write-Log "Modulo VMware.PowerCLI non ancora caricato in questa sessione: verifico se è installato..."
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Log "VMware.PowerCLI non risulta installato: provo l'installazione (scope CurrentUser)..." -Level WARN
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null
}
Install-Module -Name VMware.PowerCLI -Scope CurrentUser -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-Log "VMware.PowerCLI installato." -Level OK
}
catch {
throw "Impossibile installare VMware.PowerCLI automaticamente: $($_.Exception.Message). Installalo manualmente con: Install-Module VMware.PowerCLI -Scope CurrentUser"
}
}
Import-Module VMware.PowerCLI -ErrorAction Stop
Write-Log "Modulo VMware.PowerCLI importato." -Level OK
}
# ==============================================================================
# 1. Validazione destinazione (fail-fast prima di toccare vCenter)
# ==============================================================================
try {
Write-Log "==================== EXPORT VM: $VMName ===================="
Write-Log "Destinazione : $DestinationPath (formato $Format, exportFlags: $($ExportFlags -join ','))"
Write-Log "Utente corrente: $env:USERDOMAIN\$env:USERNAME su $env:COMPUTERNAME"
if ($ShareCredential) {
# Autentica una volta sul server della share; Windows mantiene la
# sessione autenticata per le successive operazioni dirette su UNC.
$uncRoot = ($DestinationPath -split '\\')[0..3] -join '\'
$MappedDriveName = "OvfExport_$([guid]::NewGuid().ToString('N').Substring(0,6))"
Write-Log "Autenticazione sulla share '$uncRoot' con le credenziali fornite..."
New-PSDrive -Name $MappedDriveName -PSProvider FileSystem -Root $uncRoot -Credential $ShareCredential -Scope Script -ErrorAction Stop | Out-Null
}
$extension = if ($Format -eq 'Ova') { '.ova' } else { '.ovf' }
if ($DestinationPath -match ([regex]::Escape($extension) + '$')) {
# E' stato passato direttamente un percorso file completo: lo si
# rispetta cosi' com'e', senza aggiungere una sottocartella.
$TargetFile = $DestinationPath
$TargetFolder = Split-Path -Parent $TargetFile
}
else {
# $DestinationPath e' una cartella "contenitore" (es. la share/cartella
# di progetto): dentro viene creata una sottocartella con il nome della
# VM, e dentro quella i file dell'export — stessa convenzione del
# client vSphere quando esporti in "Folder of files (OVF)".
$TargetFolder = Join-Path $DestinationPath $VMName
$TargetFile = Join-Path $TargetFolder "$VMName$extension"
}
Test-DestinationWritable -Path $TargetFolder
Write-Log "File di destinazione: $TargetFile" -Level OK
}
catch {
Write-Log "ERRORE sulla destinazione: $($_.Exception.Message)" -Level ERROR
if ($MappedDriveName) { Remove-PSDrive -Name $MappedDriveName -ErrorAction SilentlyContinue }
throw
}
# ==============================================================================
# 2. Connessione a vCenter (PowerCLI: lookup VM, power state, inventory path)
# ==============================================================================
try {
Ensure-PowerCLIModule
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -ParticipateInCeip $false -Confirm:$false | Out-Null
}
catch {
Write-Log "ERRORE preparazione PowerCLI: $($_.Exception.Message)" -Level ERROR
if ($MappedDriveName) { Remove-PSDrive -Name $MappedDriveName -ErrorAction SilentlyContinue }
exit 1
}
try {
Write-Log "Tentativo di connessione a '$vCenterServer' con l'utente Windows corrente ($CurrentUserForAuth) via SSO/SSPI..."
try {
$global:VIConnection = Connect-VIServer -Server $vCenterServer -SspiAuthentication $true -ErrorAction Stop
Write-Log "Connesso a $vCenterServer come $($global:VIConnection.User) (SSO)" -Level OK
}
catch {
Write-Log "Autenticazione SSO/SSPI non disponibile ($($_.Exception.Message)): il vCenter probabilmente non ha 'Windows Session Authentication' abilitata." -Level WARN
Write-Log "Fallback: prompt credenziali (precompilato con l'utente corrente $CurrentUserForAuth) — verranno riusate anche per ovftool più avanti, senza richiederle una seconda volta." -Level WARN
$SharedCredential = Get-Credential -UserName $CurrentUserForAuth -Message "Credenziali per $vCenterServer"
$global:VIConnection = Connect-VIServer -Server $vCenterServer -Credential $SharedCredential -ErrorAction Stop
Write-Log "Connesso a $vCenterServer come $($global:VIConnection.User)" -Level OK
}
}
catch {
Write-Log "ERRORE connessione a $vCenterServer : $($_.Exception.Message)" -Level ERROR
if ($MappedDriveName) { Remove-PSDrive -Name $MappedDriveName -ErrorAction SilentlyContinue }
exit 1
}
try {
# ==========================================================================
# 3. Individuazione VM
# ==========================================================================
$matchingVMs = @(Get-VM -Name $VMName -ErrorAction Stop)
if ($matchingVMs.Count -eq 0) {
throw "Nessuna VM trovata con nome '$VMName'."
}
if ($matchingVMs.Count -gt 1) {
throw "Trovate $($matchingVMs.Count) VM che corrispondono a '$VMName': specifica un nome univoco. ($(($matchingVMs.Name) -join ', '))"
}
$vm = $matchingVMs[0]
Write-Log "VM trovata: $($vm.Name) — Stato: $($vm.PowerState) — Host: $($vm.VMHost.Name)" -Level OK
$vmWasPoweredOn = ($vm.PowerState -eq 'PoweredOn')
# ==========================================================================
# 4. Spegnimento opzionale per consistenza dell'export
# ==========================================================================
if ($vmWasPoweredOn) {
if ($PowerOffFirst) {
if ($PSCmdlet.ShouldProcess($vm.Name, "Spegnimento (guest shutdown, fallback Stop-VM) prima dell'export")) {
Write-Log "Spegnimento della VM in corso (shutdown del guest OS)..." -Level WARN
$shutdownOk = $false
try {
Shutdown-VMGuest -VM $vm -Confirm:$false -ErrorAction Stop | Out-Null
$shutdownOk = Wait-ForGuestShutdown -VM $vm -TimeoutSec $GuestShutdownTimeoutSec
}
catch {
Write-Log "Shutdown del guest OS non disponibile (VMware Tools assenti/non attivi): $($_.Exception.Message)" -Level WARN
}
if (-not $shutdownOk) {
Write-Log "Guest shutdown non completato entro $GuestShutdownTimeoutSec secondi: eseguo Stop-VM (power off forzato)." -Level WARN
Stop-VM -VM $vm -Confirm:$false -ErrorAction Stop | Out-Null
}
$vm = Get-VM -Id $vm.Id
Write-Log "VM spenta. Stato attuale: $($vm.PowerState)" -Level OK
}
}
else {
Write-Log "La VM è ACCESA e verrà esportata 'a caldo'." -Level WARN
Write-Log "Per un export garantito consistente usa -PowerOffFirst." -Level WARN
}
}
else {
Write-Log "La VM è già spenta: export consistente senza bisogno di ulteriori azioni." -Level OK
}
# ==========================================================================
# 5. Export OVF/OVA tramite ovftool (unico modo per includere BIOS UUID,
# MAC addresses ed extra configuration — Export-VApp non lo supporta)
# ==========================================================================
if ($PSCmdlet.ShouldProcess("$($vm.Name) -> $TargetFile", "Export via ovftool (exportFlags: $($ExportFlags -join ','))")) {
$ovftoolExe = Resolve-OvfToolPath -PreferredPath $OvftoolPath
Write-Log "ovftool: $ovftoolExe"
# ovftool, a differenza di PowerCLI, non ha un equivalente di
# -SspiAuthentication: non esiste modo di fargli riusare in automatico
# e senza password la sessione Windows corrente. Se pero' e' già
# stata raccolta una credenziale durante il fallback della
# connessione PowerCLI (SSPI non disponibile su questo vCenter), la
# riusiamo qui invece di chiederla una seconda volta.
if ($SharedCredential) {
Write-Log "ovftool riusa la credenziale già inserita per la connessione PowerCLI (nessun secondo prompt)."
$ovfUser = $SharedCredential.UserName
$plainPassword = $SharedCredential.GetNetworkCredential().Password
}
else {
Write-Log "ovftool userà l'utente corrente ($CurrentUserForAuth); serve solo la password (ovftool non supporta il login integrato di Windows)."
$ovfUser = $CurrentUserForAuth
$securePassword = Read-Host -Prompt "Password vCenter per $CurrentUserForAuth (ovftool)" -AsSecureString
$plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword))
}
$inv = Get-InventoryFolderPath -VM $vm
$locatorPath = if ($inv.FolderPath) { "$($inv.Datacenter)/vm/$($inv.FolderPath)/$($vm.Name)" } else { "$($inv.Datacenter)/vm/$($vm.Name)" }
Write-Log "Percorso inventory VM: $locatorPath"
# Percent-encoding di ogni segmento del path e delle credenziali
# (necessario per caratteri come spazi, backslash nel dominio, @ nella
# UPN o nella password, dentro il locator vi://user:pass@host/path).
$encodedLocatorPath = (($locatorPath -split '/') | ForEach-Object { [uri]::EscapeDataString($_) }) -join '/'
$encodedUser = [uri]::EscapeDataString($ovfUser)
$encodedPass = [uri]::EscapeDataString($plainPassword)
$sourceUri = "vi://{0}:{1}@{2}/{3}" -f $encodedUser, $encodedPass, $vCenterServer, $encodedLocatorPath
$plainPassword = $null
$ovftoolArgs = [System.Collections.Generic.List[string]]::new()
$ovftoolArgs.Add("--noSSLVerify")
$ovftoolArgs.Add("--exportFlags=$($ExportFlags -join ',')")
if ($Force) { $ovftoolArgs.Add("--overwrite") }
$ovftoolArgs.Add($sourceUri)
$ovftoolArgs.Add($TargetFile)
Write-Log "Avvio ovftool (exportFlags: $($ExportFlags -join ', '))..."
$ovftoolOutput = & $ovftoolExe @ovftoolArgs 2>&1
$ovftoolExitCode = $LASTEXITCODE
$ovftoolOutput | ForEach-Object { Write-Log " [ovftool] $_" }
if ($ovftoolExitCode -ne 0) {
throw "ovftool ha restituito il codice di uscita $ovftoolExitCode. Vedi l'output sopra e il file di log '$LogFile' per il dettaglio."
}
Write-Log "Export completato con successo (BIOS UUID, MAC addresses ed extra configuration inclusi)." -Level OK
if (Test-Path -Path $TargetFile -PathType Leaf) {
$baseName = [IO.Path]::GetFileNameWithoutExtension($TargetFile)
$siblingFiles = @(Get-ChildItem -Path $TargetFolder -ErrorAction SilentlyContinue | Where-Object { $_.BaseName -eq $baseName })
Write-Log "File presenti a destinazione: $($siblingFiles.Count)" -Level OK
$siblingFiles | ForEach-Object { Write-Log " - $($_.Name) ($([math]::Round($_.Length/1GB,2)) GB)" }
}
else {
Write-Log "Impossibile confermare la presenza del file '$TargetFile' — verifica manualmente." -Level WARN
}
}
else {
Write-Log "Export saltato (-WhatIf): nessuna modifica effettuata." -Level WARN
}
# ==========================================================================
# 6. Riaccensione opzionale
# ==========================================================================
if ($PowerOffFirst -and $PowerOnAfter -and $vmWasPoweredOn) {
if ($PSCmdlet.ShouldProcess($vm.Name, "Riaccensione dopo l'export")) {
Write-Log "Riaccensione della VM..."
Start-VM -VM $vm -Confirm:$false -ErrorAction Stop | Out-Null
Write-Log "VM riaccesa." -Level OK
}
}
Write-Log "==================== EXPORT COMPLETATO ====================" -Level OK
}
catch {
Write-Log "ERRORE FATALE: $($_.Exception.Message)" -Level ERROR
Write-Log ($_.ScriptStackTrace) -Level ERROR
throw
}
finally {
if ($global:VIConnection) {
Disconnect-VIServer -Server $global:VIConnection -Confirm:$false -ErrorAction SilentlyContinue
Write-Log "Disconnesso da $vCenterServer" -Level OK
}
if ($MappedDriveName) {
Remove-PSDrive -Name $MappedDriveName -ErrorAction SilentlyContinue
}
}
|
COME SI USA
Modalità completamente interattiva lo script chiede tutto:
|
0 |
.\Export-VM-OVF.ps1
|
Mostra il menu di scelta del vCenter, chiede il nome della VM, e poi procede con l’export “a caldo” nella cartella di destinazione di default.
Esecuzione non interattiva, con spegnimento pre-export, riaccensione automatica al termine e sovrascrittura di eventuali file già presenti:
|
0 |
.\Export-VM-OVF.ps1 -VCenterChoice 2 -VMName "SRV-PROD-SYNC-01" -PowerOffFirst -PowerOnAfter -Force
|
In questo caso l’unico prompt possibile è quello per le credenziali, e solo se necessario (SSO/SSPI non disponibile sul vCenter, oppure per ovftool come spiegato sopra).
Simulazione senza toccare nulla, utile prima di lanciare l’export su una VM di produzione:
|
0 |
.\Export-VM-OVF.ps1 -VCenterChoice 1 -VMName "SRV-PROD-SYNC-01" -PowerOffFirst -WhatIf
|
Export in formato OVA (un unico file) invece della cartella OVF+VMDK+MF:
|
0 |
.\Export-VM-OVF.ps1 -VCenterChoice 3 -VMName "SRV-PROD-SYNC-01" -Format Ova -DestinationPath "D:\Export"
|
OUTPUT ATTESO
Durante l’esecuzione, lo script produce un log a video (e su file, con lo stesso nome timestampato Export-VM-OVF_yyyyMMdd_HHmmss.log) simile a questo:
[2026-08-30 10:12:03] [INFO] ==================== EXPORT VM: SRV-PROD-SYNC-01 ====================
[2026-08-30 10:12:03] [INFO] Destinazione : \\fileserver01.azienda.local\export\ProgettoX (formato Ovf, exportFlags: mac,uuid,extraconfig)
[2026-08-30 10:12:03] [OK] File di destinazione: \\fileserver01.azienda.local\export\ProgettoX\SRV-PROD-SYNC-01\SRV-PROD-SYNC-01.ovf
[2026-08-30 10:12:05] [OK] Connesso a vcenter02.azienda.local come AZIENDA\mario.rossi (SSO)
[2026-08-30 10:12:06] [OK] VM trovata: SRV-PROD-SYNC-01 — Stato: PoweredOn — Host: esxi-host-03.azienda.local
[2026-08-30 10:12:06] [WARN] La VM è ACCESA e verrà esportata ‘a caldo’.
[2026-08-30 10:12:07] [INFO] Avvio ovftool (exportFlags: mac, uuid, extraconfig)…
[2026-08-30 10:14:52] [OK] Export completato con successo (BIOS UUID, MAC addresses ed extra configuration inclusi).
[2026-08-30 10:14:52] [OK] File presenti a destinazione: 3
[2026-08-30 10:14:52] [INFO] – SRV-PROD-SYNC-01.ovf (0 GB)
[2026-08-30 10:14:52] [INFO] – SRV-PROD-SYNC-01.mf (0 GB)
[2026-08-30 10:14:52] [INFO] – SRV-PROD-SYNC-01-disk1.vmdk (24.13 GB)
[2026-08-30 10:14:53] [OK] Disconnesso da vcenter02.azienda.local
[2026-08-30 10:14:53] [OK] ==================== EXPORT COMPLETATO ====================
Nella cartella di destinazione si troverà quindi il file descrittore .ovf, il manifest .mf con i checksum, e uno o più .vmdk con i dischi virtuali (oppure un unico .ova se è stato usato -Format Ova) — pronti per essere re-importati altrove mantenendo BIOS UUID, MAC address e configurazione extra della VM originale.
CONSIDERAZIONI FINALI
Il punto centrale dello script è la combinazione di due strumenti che da soli non basterebbero: PowerCLI per parlare con vCenter in modo “nativo” (lookup della VM, stato di accensione, percorso di inventory) e ovftool per l’export vero e proprio, perché è l’unico dei due a esporre le opzioni avanzate del formato OVF.
Vale la pena ricordare che ovftool va installato separatamente (non arriva con PowerCLI) e che, autenticazione SSO a parte, resta comunque legato a un flusso utente/password classico un limite intrinseco dello strumento di cui è bene tenere conto quando si progettano automazioni di questo tipo su più vCenter.

0 Commenti