RVTools est l’outil de référence pour sortir un inventaire complet d’une infrastructure VMware vSphere dans un classeur Excel. Problème : c’est une application Windows .NET, et il n’en existe aucune version native pour macOS. Plutôt que de lancer une VM Windows juste pour ça, on peut obtenir exactement le même résultat — un rapport multi-onglets (VM, CPU, mémoire, disques, réseau, snapshots, hôtes, datastores, santé…) — directement depuis un Mac, avec PowerShell 7 (PowerShell Core) et le module VMware PowerCLI.
Dans cet article : tous les prérequis pas à pas, le script complet (réutilisable sur n’importe quel vCenter, avec saisie interactive ou fichier d’identifiants), et le piège macOS à connaître sur la mise en forme Excel.
Le principe
Le script se connecte à n’importe quel vCenter (ou hôte ESXi), collecte l’inventaire via PowerCLI, puis génère :
- un seul fichier .xlsx multi-onglets si le module ImportExcel est présent ;
- sinon, un dossier de fichiers .csv (un par onglet).
À chaque exécution, le rapport est rangé dans un sous-dossier nommé d’après le vCenter, avec un horodatage (aucun écrasement entre deux exports).
Prérequis
1. Homebrew
Le gestionnaire de paquets macOS, si vous ne l’avez pas déjà :
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
2. PowerShell 7 (PowerShell Core)
Attention : le cask powershell stable a été retiré de Homebrew. On passe désormais par la formule :
brew install powershell # Variantes possibles : brew install --cask powershell@preview # version preview # ou telecharger le .pkg officiel (Apple Silicon / Intel) sur : # github.com/PowerShell/PowerShell/releases
On vérifie :
pwsh --version # doit afficher PowerShell 7.x
3. VMware PowerCLI
Le module officiel VMware, à installer pour l’utilisateur courant (le script propose aussi de le faire automatiquement s’il manque) :
pwsh Install-Module VMware.PowerCLI -Scope CurrentUser -Force
4. ImportExcel (optionnel mais recommandé)
Pour obtenir un vrai classeur .xlsx multi-onglets. Sans lui, le script bascule automatiquement sur un export CSV :
Install-Module ImportExcel -Scope CurrentUser -Force
5. Un compte vCenter
Un compte ayant au minimum un accès en lecture seule sur le vCenter ou l’hôte ESXi suffit (rôle Read-only). Inutile d’utiliser un compte administrateur.
6. Bonus : couper le message CEIP
PowerCLI affiche un long avertissement au démarrage. Pour le supprimer une fois pour toutes, dans pwsh :
Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false
Le script
Enregistrez ce qui suit dans un fichier Export-vSphereInventory.ps1. Par défaut, les rapports sont écrits dans ~/VSphere-Reports/ (modifiable via le paramètre -OutputPath).
[CmdletBinding()]
param(
[string]$Server,
[string]$CredentialFile,
[string]$OutputPath = (Join-Path $HOME 'VSphere-Reports'),
[ValidateSet('auto','xlsx','csv')]
[string]$Format = 'auto',
[int]$SnapshotAgeDays = 7,
[int]$DatastoreFullPct = 90
)
$ErrorActionPreference = 'Stop'
function Write-Step { param([string]$Msg) Write-Host "==> $Msg" -ForegroundColor Cyan }
function Write-Ok { param([string]$Msg) Write-Host " $Msg" -ForegroundColor Green }
function Write-Warn2{ param([string]$Msg) Write-Host " $Msg" -ForegroundColor Yellow }
# --------------------------------------------------------------------------
# 1. Prerequis : module PowerCLI
# --------------------------------------------------------------------------
Write-Step "Verification de VMware PowerCLI"
if (-not (Get-Module -ListAvailable -Name VMware.PowerCLI)) {
Write-Warn2 "Module VMware.PowerCLI introuvable."
$rep = Read-Host " Installer VMware.PowerCLI pour l'utilisateur courant maintenant ? (o/N)"
if ($rep -match '^[oOyY]') {
Install-Module VMware.PowerCLI -Scope CurrentUser -Force -AllowClobber
} else {
throw "VMware.PowerCLI est requis. Installe-le avec : Install-Module VMware.PowerCLI -Scope CurrentUser"
}
}
Import-Module VMware.VimAutomation.Core -ErrorAction Stop
Write-Ok "PowerCLI charge."
Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -ParticipateInCEIP $false -Scope Session -Confirm:$false | Out-Null
# --------------------------------------------------------------------------
# 2. vCenter
# --------------------------------------------------------------------------
if ([string]::IsNullOrWhiteSpace($Server)) {
$Server = Read-Host "Adresse du vCenter / ESXi (FQDN ou IP)"
}
if ([string]::IsNullOrWhiteSpace($Server)) { throw "Aucun serveur fourni." }
# --------------------------------------------------------------------------
# 2bis. Identifiants : fichier (-CredentialFile) ou interactif
# --------------------------------------------------------------------------
function Get-CredentialFromFile {
param([string]$Path)
if (-not (Test-Path -LiteralPath $Path)) {
Write-Warn2 "Fichier d'identifiants introuvable : $Path"
return $null
}
$ext = [System.IO.Path]::GetExtension($Path).ToLower()
try {
if ($ext -in '.xml','.clixml') {
$c = Import-Clixml -LiteralPath $Path
if ($c -is [System.Management.Automation.PSCredential]) { return $c }
throw "Le fichier XML ne contient pas un PSCredential."
}
$raw = Get-Content -LiteralPath $Path -Raw
if ($ext -eq '.json' -or $raw.TrimStart().StartsWith('{')) {
$j = $raw | ConvertFrom-Json
$u = $j.username; if (-not $u) { $u = $j.user }; if (-not $u) { $u = $j.login }
$p = $j.password; if (-not $p) { $p = $j.pass }
if (-not $u -or -not $p) { throw "JSON sans username/password." }
return [pscredential]::new($u, (ConvertTo-SecureString $p -AsPlainText -Force))
}
# Format texte : key=value OU 2 lignes
$lines = Get-Content -LiteralPath $Path | Where-Object { $_ -and -not $_.TrimStart().StartsWith('#') }
$u = $null; $p = $null
foreach ($l in $lines) {
if ($l -match '^\s*(username|user|login)\s*=\s*(.+)$') { $u = $matches[2].Trim() }
elseif ($l -match '^\s*(password|pass)\s*=\s*(.+)$') { $p = $matches[2].Trim() }
}
if (-not $u -and $lines.Count -ge 2) { $u = $lines[0].Trim(); $p = $lines[1].Trim() }
if (-not $u -or -not $p) { throw "Format texte non reconnu (attendu 2 lignes ou username=/password=)." }
return [pscredential]::new($u, (ConvertTo-SecureString $p -AsPlainText -Force))
} catch {
Write-Warn2 "Lecture du fichier d'identifiants impossible : $($_.Exception.Message)"
return $null
}
}
$Credential = $null
if ($CredentialFile) {
$Credential = Get-CredentialFromFile -Path $CredentialFile
if ($Credential) { Write-Ok "Identifiants charges depuis $CredentialFile (user: $($Credential.UserName))." }
}
if (-not $Credential) {
Write-Step "Identifiants pour $Server"
$Credential = Get-Credential -Message "Login vSphere pour $Server (ex: administrator@vsphere.local)"
}
# --------------------------------------------------------------------------
# 3. Connexion
# --------------------------------------------------------------------------
Write-Step "Connexion a $Server"
$vc = Connect-VIServer -Server $Server -Credential $Credential
Write-Ok "Connecte a $($vc.Name) (vSphere $($vc.Version) build $($vc.Build))"
function To-GB { param($bytes) if ($null -eq $bytes) { $null } else { [math]::Round($bytes/1GB,2) } }
$report = [ordered]@{}
try {
Write-Step "Collecte des donnees (cela peut prendre un moment)"
$allVMs = Get-VM
$allHosts = Get-VMHost
Write-Ok "$($allVMs.Count) VM(s), $($allHosts.Count) hote(s)."
Write-Step "Onglet vInfo"
$report['vInfo'] = foreach ($vm in $allVMs) {
$v = $vm.ExtensionData
[pscustomobject][ordered]@{
VM='?'; Powerstate=$vm.PowerState; Template=$v.Config.Template
CPUs=$vm.NumCpu; 'Memory (MB)'=$vm.MemoryMB
NICs=($v.Network).Count; Disks=(Get-HardDisk -VM $vm).Count
'Provisioned (GB)'=[math]::Round($vm.ProvisionedSpaceGB,2)
'Used (GB)'=[math]::Round($vm.UsedSpaceGB,2)
'HW version'=$vm.HardwareVersion; 'Guest OS'=$v.Guest.GuestFullName
'OS (config)'=$v.Config.GuestFullName; 'IP Address'=$v.Guest.IpAddress
DNSName=$v.Guest.HostName; VMToolsStatus=$v.Guest.ToolsStatus
Folder=$vm.Folder.Name; ResourcePool=$vm.ResourcePool.Name
Host=$vm.VMHost.Name; Cluster=(Get-Cluster -VM $vm -ErrorAction SilentlyContinue).Name
Annotation=$v.Config.Annotation; UUID=$v.Config.Uuid; VMID=$vm.Id
} | ForEach-Object { $_.VM = $vm.Name; $_ }
}
Write-Step "Onglet vCPU"
$report['vCPU'] = foreach ($vm in $allVMs) {
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; CPUs=$vm.NumCpu; CoresPerSocket=$vm.CoresPerSocket
Sockets=$(if ($vm.CoresPerSocket){[math]::Ceiling($vm.NumCpu/$vm.CoresPerSocket)}else{$null})
'Reservation (MHz)'=$vm.ExtensionData.Config.CpuAllocation.Reservation
'Limit (MHz)'=$vm.ExtensionData.Config.CpuAllocation.Limit
Shares=$vm.ExtensionData.Config.CpuAllocation.Shares.Shares
HotAdd=$vm.ExtensionData.Config.CpuHotAddEnabled; Host=$vm.VMHost.Name
}
}
Write-Step "Onglet vMemory"
$report['vMemory'] = foreach ($vm in $allVMs) {
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; 'Size (MB)'=$vm.MemoryMB
'Reservation (MB)'=$vm.ExtensionData.Config.MemoryAllocation.Reservation
'Limit (MB)'=$vm.ExtensionData.Config.MemoryAllocation.Limit
Shares=$vm.ExtensionData.Config.MemoryAllocation.Shares.Shares
HotAdd=$vm.ExtensionData.Config.MemoryHotAddEnabled; Host=$vm.VMHost.Name
}
}
Write-Step "Onglet vDisk"
$report['vDisk'] = foreach ($vm in $allVMs) {
foreach ($hd in (Get-HardDisk -VM $vm)) {
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; Disk=$hd.Name
'Capacity (GB)'=[math]::Round($hd.CapacityGB,2); DiskType=$hd.StorageFormat
Persistence=$hd.Persistence; Datastore=($hd.Filename -split '\]')[0].TrimStart('['); Path=$hd.Filename
}
}
}
Write-Step "Onglet vPartition"
$report['vPartition'] = foreach ($vm in $allVMs) {
foreach ($d in $vm.ExtensionData.Guest.Disk) {
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; DiskPath=$d.DiskPath
'Capacity (GB)'=(To-GB $d.Capacity); 'Free (GB)'=(To-GB $d.FreeSpace)
'Free %'=$(if ($d.Capacity -gt 0){[math]::Round(($d.FreeSpace/$d.Capacity)*100,1)}else{$null})
}
}
}
Write-Step "Onglet vNetwork"
$report['vNetwork'] = foreach ($vm in $allVMs) {
foreach ($nic in (Get-NetworkAdapter -VM $vm)) {
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; Adapter=$nic.Name; Type=$nic.Type
MacAddress=$nic.MacAddress; Network=$nic.NetworkName
Connected=$nic.ConnectionState.Connected; StartConnected=$nic.ConnectionState.StartConnected; Host=$vm.VMHost.Name
}
}
}
Write-Step "Onglet vSnapshot"
$report['vSnapshot'] = foreach ($vm in $allVMs) {
foreach ($snap in (Get-Snapshot -VM $vm -ErrorAction SilentlyContinue)) {
[pscustomobject][ordered]@{
VM=$vm.Name; Snapshot=$snap.Name; Description=$snap.Description; Created=$snap.Created
'Age (days)'=[math]::Round(((Get-Date)-$snap.Created).TotalDays,1)
'Size (GB)'=[math]::Round($snap.SizeGB,2); PowerState=$snap.PowerState; IsCurrent=$snap.IsCurrent
}
}
}
Write-Step "Onglet vTools"
$report['vTools'] = foreach ($vm in $allVMs) {
$g = $vm.ExtensionData.Guest
[pscustomobject][ordered]@{
VM=$vm.Name; Powerstate=$vm.PowerState; ToolsStatus=$g.ToolsStatus; ToolsVersion=$g.ToolsVersion
ToolsVersionStatus=$g.ToolsVersionStatus2; ToolsRunningStatus=$g.ToolsRunningStatus
UpgradePolicy=$vm.ExtensionData.Config.Tools.ToolsUpgradePolicy; Host=$vm.VMHost.Name
}
}
Write-Step "Onglet vHost"
$report['vHost'] = foreach ($h in $allHosts) {
$hv = $h.ExtensionData
[pscustomobject][ordered]@{
Host=$h.Name; Cluster=(Get-Cluster -VMHost $h -ErrorAction SilentlyContinue).Name
ConnectionState=$h.ConnectionState; PowerState=$h.PowerState
Vendor=$hv.Hardware.SystemInfo.Vendor; Model=$hv.Hardware.SystemInfo.Model
'CPU Model'=$hv.Summary.Hardware.CpuModel; CPUSockets=$hv.Hardware.CpuInfo.NumCpuPackages
CPUCores=$hv.Hardware.CpuInfo.NumCpuCores; CPUThreads=$hv.Hardware.CpuInfo.NumCpuThreads
'CPU MHz'=$h.CpuTotalMhz; 'CPU Used MHz'=$h.CpuUsageMhz
'Memory (GB)'=[math]::Round($h.MemoryTotalGB,1); 'Mem Used (GB)'=[math]::Round($h.MemoryUsageGB,1)
ESXiVersion=$h.Version; Build=$h.Build; 'BIOS Version'=$hv.Hardware.BiosInfo.BiosVersion
'Service Tag'=($hv.Hardware.SystemInfo.OtherIdentifyingInfo | Where-Object { $_.IdentifierType.Key -eq 'ServiceTag' }).IdentifierValue
NumVMs=(Get-VM -Location $h -ErrorAction SilentlyContinue).Count
}
}
Write-Step "Onglet vCluster"
$report['vCluster'] = foreach ($c in (Get-Cluster)) {
[pscustomobject][ordered]@{
Cluster=$c.Name; HAEnabled=$c.HAEnabled; DrsEnabled=$c.DrsEnabled; DrsAutomation=$c.DrsAutomationLevel
Hosts=($c | Get-VMHost).Count; VMs=($c | Get-VM).Count
'Total CPU (GHz)'=[math]::Round((($c | Get-VMHost | Measure-Object CpuTotalMhz -Sum).Sum)/1000,1)
'Total Mem (GB)'=[math]::Round((($c | Get-VMHost | Measure-Object MemoryTotalGB -Sum).Sum),1)
}
}
Write-Step "Onglet vDatastore"
$report['vDatastore'] = foreach ($ds in (Get-Datastore)) {
[pscustomobject][ordered]@{
Datastore=$ds.Name; Type=$ds.Type; 'Capacity (GB)'=[math]::Round($ds.CapacityGB,1)
'Free (GB)'=[math]::Round($ds.FreeSpaceGB,1)
'Free %'=$(if ($ds.CapacityGB -gt 0){[math]::Round(($ds.FreeSpaceGB/$ds.CapacityGB)*100,1)}else{$null})
'Used (GB)'=[math]::Round($ds.CapacityGB-$ds.FreeSpaceGB,1)
NumVMs=($ds | Get-VM -ErrorAction SilentlyContinue).Count; Accessible=$ds.State
}
}
Write-Step "Onglet vRP (Resource Pools)"
$report['vRP'] = foreach ($rp in (Get-ResourcePool)) {
[pscustomobject][ordered]@{
ResourcePool=$rp.Name; 'CPU Reservation (MHz)'=$rp.CpuReservationMHz; 'CPU Limit (MHz)'=$rp.CpuLimitMHz
CPUSharesLevel=$rp.CpuSharesLevel; 'Mem Reservation (MB)'=$rp.MemReservationMB
'Mem Limit (MB)'=$rp.MemLimitMB; MemSharesLevel=$rp.MemSharesLevel
NumVMs=($rp | Get-VM -ErrorAction SilentlyContinue).Count
}
}
# ----------------------------------------------------------------------
# vHealth : detection de problemes (facon onglet vHealth de RVTools)
# ----------------------------------------------------------------------
Write-Step "Onglet vHealth"
$health = New-Object System.Collections.Generic.List[object]
function Add-Health {
param([string]$Severity,[string]$Category,[string]$Object,[string]$Issue,$Detail)
$health.Add([pscustomobject][ordered]@{
Severity = $Severity; Category = $Category; Object = $Object; Issue = $Issue; Detail = $Detail
})
}
# -- Snapshots trop anciens
foreach ($vm in $allVMs) {
foreach ($snap in (Get-Snapshot -VM $vm -ErrorAction SilentlyContinue)) {
$age = [math]::Round(((Get-Date) - $snap.Created).TotalDays, 1)
if ($age -ge $SnapshotAgeDays) {
Add-Health 'Warning' 'Snapshot' $vm.Name "Snapshot ancien (>= $SnapshotAgeDays j)" "'$($snap.Name)' - $age j - $([math]::Round($snap.SizeGB,2)) GB"
}
}
}
# -- VMware Tools absents / obsoletes (VM allumees)
foreach ($vm in ($allVMs | Where-Object { $_.PowerState -eq 'PoweredOn' })) {
$ts = $vm.ExtensionData.Guest.ToolsStatus
if ($ts -in 'toolsNotInstalled','toolsNotRunning') {
Add-Health 'Warning' 'VMware Tools' $vm.Name 'Tools absents ou arretes' "$ts"
} elseif ($ts -eq 'toolsOld') {
Add-Health 'Info' 'VMware Tools' $vm.Name 'Tools obsoletes' "$ts (v$($vm.ExtensionData.Guest.ToolsVersion))"
}
}
# -- Datastores trop pleins
foreach ($ds in (Get-Datastore)) {
if ($ds.CapacityGB -gt 0) {
$usedPct = [math]::Round((($ds.CapacityGB - $ds.FreeSpaceGB) / $ds.CapacityGB) * 100, 1)
if ($usedPct -ge $DatastoreFullPct) {
$sev = if ($usedPct -ge 95) { 'Critical' } else { 'Warning' }
Add-Health $sev 'Datastore' $ds.Name "Datastore plein (>= $DatastoreFullPct%)" "$usedPct% utilise - $([math]::Round($ds.FreeSpaceGB,1)) GB libres"
}
}
}
# -- VMs orphelines / inaccessibles
foreach ($vm in $allVMs) {
$cs = $vm.ExtensionData.Runtime.ConnectionState
if ($cs -in 'orphaned','inaccessible','invalid') {
Add-Health 'Critical' 'VM' $vm.Name 'VM orpheline / inaccessible' "$cs"
}
}
# -- Hotes deconnectes ou en maintenance
foreach ($h in $allHosts) {
if ($h.ConnectionState -ne 'Connected') {
Add-Health 'Critical' 'Host' $h.Name 'Hote non connecte' "$($h.ConnectionState)"
}
if ($h.ExtensionData.Runtime.InMaintenanceMode) {
Add-Health 'Info' 'Host' $h.Name 'Hote en mode maintenance' ''
}
}
# -- CD/ISO ou floppy montes (empechent vMotion)
foreach ($vm in ($allVMs | Where-Object { $_.PowerState -eq 'PoweredOn' })) {
foreach ($cd in (Get-CDDrive -VM $vm -ErrorAction SilentlyContinue)) {
if ($cd.IsoPath -or $cd.HostDevice -or $cd.RemoteDevice) {
Add-Health 'Info' 'VM' $vm.Name 'Lecteur CD/ISO connecte' "$($cd.IsoPath)$($cd.HostDevice)$($cd.RemoteDevice)"
}
}
}
# Tri par severite (Critical > Warning > Info)
$sevOrder = @{ 'Critical'=0; 'Warning'=1; 'Info'=2 }
$report['vHealth'] = $health | Sort-Object @{ Expression = { $sevOrder[$_.Severity] } }, Category, Object
$nCrit = ($health | Where-Object Severity -eq 'Critical').Count
$nWarn = ($health | Where-Object Severity -eq 'Warning').Count
Write-Ok "$($health.Count) point(s) detecte(s) : $nCrit critique(s), $nWarn avertissement(s)."
# ----------------------------------------------------------------------
# Export : sous-dossier nomme d'apres le vCenter
# ----------------------------------------------------------------------
$safeName = ($Server -replace '[^\w\.\-]','_')
$targetDir = Join-Path $OutputPath $safeName
if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null }
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$baseName = "RVTools_${safeName}_${stamp}"
$hasImportExcel = [bool](Get-Module -ListAvailable -Name ImportExcel)
$useXlsx = switch ($Format) { 'xlsx' {$true} 'csv' {$false} default {$hasImportExcel} }
if ($useXlsx -and -not $hasImportExcel) {
Write-Warn2 "Format xlsx demande mais module ImportExcel absent."
$rep = Read-Host " Installer ImportExcel maintenant ? (o/N)"
if ($rep -match '^[oOyY]') { Install-Module ImportExcel -Scope CurrentUser -Force; $hasImportExcel = $true }
else { Write-Warn2 "Repli sur export CSV."; $useXlsx = $false }
}
if ($useXlsx) {
Import-Module ImportExcel
$xlsx = Join-Path $targetDir "$baseName.xlsx"
Write-Step "Ecriture du classeur Excel : $xlsx"
# NB : pas de -AutoSize (non supporte sous macOS/.NET 6+, meme avec mono-libgdiplus).
# On accumule tous les onglets dans un seul package puis on regle les largeurs
# manuellement (longueur max du contenu) -> aucun warning, aucune dependance.
if (Test-Path $xlsx) { Remove-Item $xlsx -Force }
$excel = $null
foreach ($sheet in $report.Keys) {
$data = $report[$sheet]; if (-not $data) { $data = [pscustomobject]@{ Info='Aucune donnee' } }
if ($excel) {
$excel = $data | Export-Excel -ExcelPackage $excel -WorksheetName $sheet -FreezeTopRow -BoldTopRow -TableName $sheet -PassThru -WarningAction SilentlyContinue
} else {
$excel = $data | Export-Excel -Path $xlsx -WorksheetName $sheet -FreezeTopRow -BoldTopRow -TableName $sheet -PassThru -WarningAction SilentlyContinue
}
}
# Largeur des colonnes calculee a la main (sans System.Drawing)
foreach ($ws in $excel.Workbook.Worksheets) {
if ($null -eq $ws.Dimension) { continue }
for ($col = 1; $col -le $ws.Dimension.End.Column; $col++) {
$maxLen = 0
for ($row = 1; $row -le $ws.Dimension.End.Row; $row++) {
$t = [string]$ws.Cells[$row, $col].Text
if ($t.Length -gt $maxLen) { $maxLen = $t.Length }
}
$ws.Column($col).Width = [math]::Min([math]::Max($maxLen + 2, 8), 60)
}
}
Close-ExcelPackage $excel
Write-Ok "Termine : $xlsx"; $result = $xlsx
} else {
$dir = Join-Path $targetDir $baseName
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Step "Ecriture des CSV dans : $dir"
foreach ($sheet in $report.Keys) {
$csv = Join-Path $dir "$sheet.csv"; $data = $report[$sheet]
if ($data) { $data | Export-Csv -Path $csv -NoTypeInformation -Encoding UTF8 } else { 'Aucune donnee' | Out-File -FilePath $csv -Encoding UTF8 }
Write-Ok " - $sheet.csv"
}
Write-Ok "Termine : $dir"; $result = $dir
}
Write-Host ""
Write-Host "RAPPORT GENERE : $result" -ForegroundColor Green
}
finally {
if ($vc) { Disconnect-VIServer -Server $Server -Confirm:$false -ErrorAction SilentlyContinue; Write-Ok "Deconnecte de $Server." }
}
Utilisation
Le script ne contient rien en dur : on lui passe le vCenter en paramètre (ou il le demande), et les identifiants sont soit demandés interactivement (mot de passe masqué), soit lus dans un fichier.
# 100% interactif : demande le vCenter puis le login / mot de passe pwsh ./Export-vSphereInventory.ps1 # En precisant le vCenter (login / pass demandes) pwsh ./Export-vSphereInventory.ps1 -Server VCENTER.MONDOMAINE.LAN # Avec fichier d'identifiants, format force et seuils de sante personnalises pwsh ./Export-vSphereInventory.ps1 -Server 10.0.0.10 -CredentialFile ~/.vc-creds.json -Format xlsx -SnapshotAgeDays 3 -DatastoreFullPct 85
Le fichier d’identifiants
Le paramètre -CredentialFile accepte plusieurs formats. Le plus simple, un JSON :
# ~/.vc-creds.json
{ "username": "administrator@vsphere.local", "password": "MotDePasse" }
# A proteger imperativement :
chmod 600 ~/.vc-creds.json
Sont aussi acceptés : un fichier texte de 2 lignes (login puis mot de passe), des lignes username= / password=, ou — le plus sûr — un credential PowerShell chiffré (lié au compte et à la machine) :
# Variante chiffree (recommandee) pwsh -c "Get-Credential | Export-Clixml ~/.vc-creds.xml" pwsh ./Export-vSphereInventory.ps1 -Server 10.0.0.10 -CredentialFile ~/.vc-creds.xml
Le piège macOS : la mise en forme Excel
C’est LE point qui fait perdre du temps. Le module ImportExcel propose une option -AutoSize pour ajuster la largeur des colonnes… mais elle s’appuie sur System.Drawing, qui n’est plus supporté hors Windows depuis .NET 6+ (base de PowerShell 7). Résultat : même après brew install mono-libgdiplus, on obtient en boucle :
WARNING: ImportExcel Module Cannot Autosize. Please run the following command to install dependencies: brew install mono-libgdiplus
La solution propre, intégrée dans le script ci-dessus : ne pas utiliser -AutoSize et calculer la largeur des colonnes soi-même (longueur maximale du contenu), puis museler l’avertissement résiduel avec -WarningAction SilentlyContinue. Aucune dépendance graphique, aucun warning, et des colonnes correctement dimensionnées.
Ce que contient le rapport
Le classeur reprend la logique de RVTools, avec un onglet par thème :
- vInfo, vCPU, vMemory, vDisk, vPartition, vNetwork : les machines virtuelles sous tous les angles ;
- vSnapshot, vTools : snapshots et état des VMware Tools ;
- vHost, vCluster, vDatastore, vRP : hôtes, clusters, datastores et resource pools ;
- vHealth : un onglet de détection de problèmes (snapshots trop anciens, VMware Tools absents ou obsolètes, datastores trop pleins, VM orphelines, hôtes déconnectés ou en maintenance, lecteurs CD/ISO montés), trié par sévérité. Les seuils sont réglables via -SnapshotAgeDays et -DatastoreFullPct.
Récapitulatif
# Installation (une seule fois) brew install powershell pwsh Install-Module VMware.PowerCLI -Scope CurrentUser -Force Install-Module ImportExcel -Scope CurrentUser -Force Set-PowerCLIConfiguration -Scope User -ParticipateInCEIP $false -Confirm:$false exit # Generation d'un rapport pwsh ./Export-vSphereInventory.ps1 -Server VCENTER.MONDOMAINE.LAN
Conclusion
Pas besoin de Windows ni de machine virtuelle : avec PowerShell Core et PowerCLI, on obtient sur macOS un inventaire vSphere complet, réutilisable sur n’importe quel vCenter, exporté dans un classeur Excel propre — le tout dans un script que l’on peut aussi planifier ou intégrer à ses propres workflows. Un équivalent RVTools tout à fait honorable, et 100% scriptable.
