
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