пятница, 23 октября 2026 г.

VMware: Скрипт PowerShell для поиска неиспользуемых виртуальных машин

Данный скрипт предназначен для поиска виртуальных машин на VMware vCenter Server, к которым давно не происходило подключений с помощью консоли VMRC или HTML5 от пользователей. Алгоритм использует события "Events" сервера, поэтому важно чтобы политика "Event retention" была настроена на длительный срок (желательно 365 дней).

Выполняется данная настройка в vCenter->Settings->General->Edit->Database. Там необходимо увеличить значение "Event retention (days)" и перегрузить vCenter сервер для применения параметров.

Ниже приведен сам скрипт, который работает следующим образом: по каждой виртуальной машине из указанной папки и подпапок находит последнее событие из двух типов (VmAcquiredTicketEvent, VmAcquiredMksTicketEvent) и пишет одну строку в отчёт.

<#
.SYNOPSIS
    Последнее подключение к консоли по каждой ВМ из папки vCenter (VMRC + HTML5).
.DESCRIPTION
    Рекурсивно собирает ВМ из указанной папки, находит последнее событие
    VmAcquiredTicketEvent или VmAcquiredMksTicketEvent по каждой ВМ
    и сохраняет отчёт в .xls (SpreadsheetML) без внешних модулей.
#>

# ======================== НАСТРОЙКИ ========================
$vCenterServer = "vcenter.mydomain.local"
$FolderName    = "VM_Folder"
$Days          = 360
$OutputPath    = Join-Path -Path $PSScriptRoot -ChildPath "vm_console_last_event.xls"
# ==========================================================

$eventTypes = @("VmAcquiredTicketEvent", "VmAcquiredMksTicketEvent")
$startTime  = (Get-Date).AddDays(-$Days)
$finishTime = Get-Date

function Get-VMsRecursive {
    param($Folder)
    $vms = @(Get-VM -Location $Folder -ErrorAction SilentlyContinue)
    foreach ($sub in @(Get-Folder -Location $Folder -ErrorAction SilentlyContinue)) {
        $vms += Get-VMsRecursive -Folder $sub
    }
    return $vms
}

function ConvertTo-XmlSafe {
    param([string]$s)
    if ($null -eq $s) { return "" }
    return $s.Replace("&","&amp;").Replace("<","&lt;").Replace(">","&gt;").Replace('"',"&quot;").Replace("'","&apos;")
}

function Export-ToSpreadsheetML {
    param([object[]]$Rows, [string]$Path, [string]$Title)

    $columns = @(
        @{ Name = 'ВМ';                          Width = 200 },
        @{ Name = 'Пользователь';                Width = 160 },
        @{ Name = 'Время последнего подключения'; Width = 170 },
        @{ Name = 'Сообщение';                   Width = 500 }
    )

    $sb = New-Object System.Text.StringBuilder
    [void]$sb.AppendLine('<?xml version="1.0" encoding="UTF-8"?>')
    [void]$sb.AppendLine('<?mso-application progid="Excel.Sheet"?>')
    [void]$sb.AppendLine('<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"')
    [void]$sb.AppendLine(' xmlns:o="urn:schemas-microsoft-com:office:office"')
    [void]$sb.AppendLine(' xmlns:x="urn:schemas-microsoft-com:office:excel"')
    [void]$sb.AppendLine(' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">')
    [void]$sb.AppendLine(@'
 <Styles>
  <Style ss:ID="Default" ss:Name="Normal">
   <Alignment ss:Vertical="Top"/>
   <Font ss:FontName="Calibri" ss:Size="11"/>
  </Style>
  <Style ss:ID="Header">
   <Alignment ss:Vertical="Center" ss:Horizontal="Center" ss:WrapText="1"/>
   <Font ss:FontName="Calibri" ss:Size="11" ss:Bold="1" ss:Color="#FFFFFF"/>
   <Interior ss:Color="#4472C4" ss:Pattern="Solid"/>
  </Style>
  <Style ss:ID="CellWrap">
   <Alignment ss:Vertical="Top" ss:WrapText="1"/>
   <Font ss:FontName="Calibri" ss:Size="11"/>
  </Style>
  <Style ss:ID="CellNoEvent">
   <Font ss:FontName="Calibri" ss:Size="11" ss:Color="#C00000" ss:Italic="1"/>
  </Style>
  <Style ss:ID="Title">
   <Font ss:FontName="Calibri" ss:Size="12" ss:Bold="1" ss:Color="#1F3864"/>
  </Style>
 </Styles>
'@)

    [void]$sb.AppendLine(' <Worksheet ss:Name="Console_Connections">')
    [void]$sb.AppendLine('  <Table>')
    foreach ($c in $columns) { [void]$sb.AppendLine("   <Column ss:Width=`"$($c.Width)`"/>") }

    $rowNum = 1
    if ($Title) {
        [void]$sb.AppendLine("   <Row ss:Height=`"22`"><Cell ss:StyleID=`"Title`" ss:MergeAcross=`"$($columns.Count - 1)`"><Data ss:Type=`"String`">$(ConvertTo-XmlSafe $Title)</Data></Cell></Row>")
        [void]$sb.AppendLine('   <Row/>')
        $rowNum += 2
    }

    [void]$sb.AppendLine('   <Row ss:Height="28">')
    foreach ($c in $columns) { [void]$sb.AppendLine("    <Cell ss:StyleID=`"Header`"><Data ss:Type=`"String`">$(ConvertTo-XmlSafe $c.Name)</Data></Cell>") }
    [void]$sb.AppendLine('   </Row>')
    $headerRow = $rowNum++

    foreach ($r in $Rows) {
        $style = if ($r.'Время последнего подключения' -eq '-') { "CellNoEvent" } else { "CellWrap" }
        [void]$sb.AppendLine('   <Row>')
        foreach ($c in $columns) { [void]$sb.AppendLine("    <Cell ss:StyleID=`"$style`"><Data ss:Type=`"String`">$(ConvertTo-XmlSafe ([string]$r.($c.Name)))</Data></Cell>") }
        [void]$sb.AppendLine('   </Row>')
        $rowNum++
    }

    [void]$sb.AppendLine('  </Table>')
    [void]$sb.AppendLine("  <AutoFilter x:Range=`"R${headerRow}C1:R$($rowNum - 1)C$($columns.Count)`" xmlns=`"urn:schemas-microsoft-com:office:excel`"/>")
    [void]$sb.AppendLine('  <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">')
    [void]$sb.AppendLine('   <FreezePanes/><FrozenNoSplit/>')
    [void]$sb.AppendLine("   <SplitHorizontal>$headerRow</SplitHorizontal><TopRowBottomPane>$headerRow</TopRowBottomPane><ActivePane>2</ActivePane>")
    [void]$sb.AppendLine('  </WorksheetOptions>')
    [void]$sb.AppendLine(' </Worksheet>')
    [void]$sb.AppendLine('</Workbook>')

    $sb.ToString() | Out-File -FilePath $Path -Encoding UTF8
}

# --- Подключение ---
Write-Host "Подключение к $vCenterServer..." -ForegroundColor Cyan
try { Connect-VIServer -Server $vCenterServer -ErrorAction Stop | Out-Null }
catch { Write-Error "Не удалось подключиться: $($_.Exception.Message)"; return }

try {
    $folder = @(Get-Folder -Name $FolderName -Type VM -ErrorAction SilentlyContinue)[0]
    if (-not $folder) {
        Write-Error "Папка '$FolderName' не найдена."
        Get-Folder -Type VM | Select-Object Name, Id | Format-Table -AutoSize | Out-Host
        return
    }

    Write-Host "Папка: $($folder.Name) | Период: $($startTime.ToString('yyyy-MM-dd')) — $($finishTime.ToString('yyyy-MM-dd'))" -ForegroundColor Green

    $vms = Get-VMsRecursive -Folder $folder | Group-Object Id | ForEach-Object { $_.Group[0] }
    if ($vms.Count -eq 0) { Write-Host "ВМ не найдено." -ForegroundColor Yellow; return }
    Write-Host "Найдено ВМ: $($vms.Count)`n" -ForegroundColor Green

    $rows = New-Object System.Collections.Generic.List[object]
    $i = 0

    foreach ($vm in $vms) {
        $i++
        Write-Progress -Activity "Обработка ВМ" -Status "$($vm.Name) ($i из $($vms.Count))" -PercentComplete (($i / $vms.Count) * 100)

        try {
            $last = Get-VIEvent -Entity $vm -Start $startTime -Finish $finishTime -ErrorAction Stop |
                    Where-Object { $eventTypes -contains $_.GetType().Name } |
                    Sort-Object CreatedTime -Descending |
                    Select-Object -First 1
        }
        catch {
            $rows.Add([PSCustomObject]@{
                'ВМ' = $vm.Name; 'Пользователь' = '-'
                'Время последнего подключения' = 'Ошибка'
                'Сообщение' = $_.Exception.Message
            })
            continue
        }

        if ($last) {
            $rows.Add([PSCustomObject]@{
                'ВМ'                          = $vm.Name
                'Пользователь'                = if ($last.UserName) { $last.UserName } else { '-' }
                'Время последнего подключения' = $last.CreatedTime.ToString('yyyy-MM-dd HH:mm:ss')
                'Сообщение'                   = [string]$last.FullFormattedMessage
            })
        }
        else {
            $rows.Add([PSCustomObject]@{
                'ВМ'                          = $vm.Name
                'Пользователь'                = '-'
                'Время последнего подключения' = '-'
                'Сообщение'                   = 'Нет событий подключений'
            })
        }
    }

    Write-Progress -Activity "Обработка ВМ" -Completed

    $outDir = Split-Path -Parent $OutputPath
    if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir -Force | Out-Null }

    $title = "Последнее подключение к консоли — vCenter: $vCenterServer, папка: '$FolderName', период: $($startTime.ToString('yyyy-MM-dd')) — $($finishTime.ToString('yyyy-MM-dd'))"
    Export-ToSpreadsheetML -Rows $rows -Path $OutputPath -Title $title

    $withEvents = @($rows | Where-Object { $_.'Время последнего подключения' -ne '-' }).Count
    Write-Host "`nГотово. ВМ: $($vms.Count), с подключениями: $withEvents, без: $($vms.Count - $withEvents)" -ForegroundColor Green
    Write-Host "Файл: $OutputPath" -ForegroundColor Green
}
finally {
    Disconnect-VIServer -Server $vCenterServer -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
}

Комментариев нет:

Отправить комментарий