Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI 

Рана ці позна любы сістэмны адміністратар VMware даходзіць да аўтаматызацыі руцінных задач. Пачынаецца ўсё з каманднага радка, потым ідзе PowerShell ці VMware PowerCLI.

Дапушчальны, вы асвоілі PowerShell ледзь далей запуску ISE і выкарыстанні стандартных камандлетаў з модуляў, якія працуюць за рахунак «нейкай магіі». Калі вы пачняце лічыць віртуальныя машыны сотнямі, то выявіце, што скрыпты, якія выбаўлялі на малых маштабах, працуюць прыкметна павольней на вялікіх. 

У гэтай сітуацыі выбавяць 2 прылады:

  • PowerShell Runspaces - падыход, які дазваляе распаралеліць выкананне працэсаў у асобных патоках; 
  • Get-View – базавая функцыя PowerCLI, аналаг Get-WMIObject у Windows. Гэты камандлет не цягне за сабой спадарожныя сутнасці аб'екты, а атрымлівае інфармацыю ў выглядзе простага аб'екта з простымі тыпамі даных. У многіх выпадках выходзіць хутчэй.

Далей коратка раскажу пра кожную прыладу і пакажу прыклады выкарыстання. Разбяром пэўныя скрыпты і паглядзім, калі лепш працуе адзін, калі другі. Паехалі!

Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI

Першая ступень: Runspace

Такім чынам, Runspace прызначаны для раўналежнай апрацоўкі задач па-за асноўным модулем. Вядома, можна запусціць яшчэ адзін працэс, які з'есць колькі-то памяці, працэсара і т. д. Калі ваш скрыпт адпрацоўвае за пару хвілін і марнуе гігабайт памяці, хутчэй за ўсё, Runspace вам не запатрабуецца. А вось для скрыптоў на дзясяткі тысяч аб'ектаў ён патрэбны.

Пачаць засваенне можна адгэтуль: 
Beginning Use of PowerShell Runspaces: Part 1

Што дае выкарыстанне Runspace:

  • хуткасць за кошт абмежавання спісу выкананых каманд,
  • паралельнае выкананне задач,
  • бяспеку.

Вось прыклад з інтэрнэту, калі Runspace дапамагае:

«Канкурэнцыя за рэсурсы сховішчы - адна з метрык, якія складана адсочваць у vSphere. Усярэдзіне vCenter нельга проста ўзяць і паглядзець, якая ВМ спажывае больш рэсурсаў сховішчы. На шчасце, сабраць гэтыя дадзеныя можна за хвіліны дзякуючы PowerShell.
Падзялюся скрыптам, які дазволіць сістэмным адміністратарам VMware хутка выконваць пошук па ўсім vCenter і атрымліваць ліст ВМ з дадзенымі па іх сярэднім спажыванні.  
Скрыпт выкарыстоўвае PowerShell runspaces, каб кожны хост ESXi збіраў інфармацыю па спажыванні яго ўласных ВМ у асобным Runspace і адразу паведамляў аб завяршэнні. Гэта дазваляе PowerShell адразу зачыняць джобы, а не перабіраць паслядоўна хасты і не чакаць, пакуль кожны завершыць свой запыт».

Крыніца: How to Show Virtual Machine I/O на an ESXi Dashboard

У выпадку ніжэй Runspace ужо не ў спраў:

«Спрабую напісаць скрыпт, які збірае шмат дадзеных з ВМ і пры неабходнасці запісвае новыя дадзеныя. Праблема ў тым, што ВМ дастаткова шмат, і на адну машыну марнуецца па 5-8 секунд». 

Крыніца: Multithreading PowerCLI with RunspacePool

Тут спатрэбіцца Get-View, пяройдзем да яго. 

Другая прыступка: Get-View

Каб разабрацца, чым карысны Get-View, варта ўспомніць, як працуюць камандлеты ўвогуле. 

Камандлеты патрэбныя для зручнага атрымання інфармацыі без неабходнасці штудзіраваць даведнікі па API і вынаходзіць чарговы ровар. Тое, што ў старыя часы распісвалася ў сотню-іншую радкоў кода, PowerShell дазваляе зрабіць адной камандай. За гэтую зручнасць мы плацім хуткасцю. Унутры саміх камандлетаў ніякай магіі няма: той жа скрыпт, але ніжэйшага ўзроўню, напісаны ўмелымі рукамі майстра з сонечнай Індыі.

Зараз для параўнання з Get-View возьмем камандлет Get-VM: ён звяртаецца да віртуальнай машыны і вяртае складовай аб'ект, гэта значыць прыкладае да яго іншыя спадарожныя аб'екты: VMHost, Datastore і т. д.  

Get-View на яго месцы не прыкручвае ў які вяртаецца аб'ект нічога лішняга. Больш за тое, ён дазваляе жорстка паказаць, якая менавіта інфармацыя нам патрэбная, што аблегчыць аб'ект на выхадзе. У Windows Server у цэлым і ў Hyper-V, у прыватнасці, прамым аналогам з'яўляецца камандлет Get-WMIObject - ідэя абсалютна тая ж.

Get-View няёмкі ў руцінных аперацыях над кропкавымі аб'ектамі. Але калі гаворка заходзіць аб тысячах і дзясятках тысяч аб'ектаў, яму няма кошту.

Пачытаць падрабязней можна ў блогу VMware: Introduction to Get-View

Цяпер усё пакажу на рэальным кейсе. 

Пішам скрыпт для выгрузкі ВМ

Аднойчы мой калега папрасіў мяне аптымізаваць ягоны скрыпт. Задача - звычайная руціна: знайсці ўсё ВМ з дублюючым параметрам cloud.uuid (так, такое магчыма пры кланаванні ВМ у vCloud Director). 

Відавочны варыянт рашэння, які прыходзіць на розум:

  1. Атрымаць спіс усіх ВМ.
  2. Нейкім чынам распарсіць спіс.

Зыходным варыянтам быў такі немудрагелісты скрыпт:

function Get-CloudUUID1 {
   # Получаем список всех ВМ
   $vms = Get-VM
   $report = @()

   # Обрабатываем каждый объект, получая из него только 2 свойства: Имя ВМ и Cloud UUID.
   # Заносим данные в новый PS-объект с полями VM и UUID
   foreach ($vm in $vms)
   {
       $table = "" | select VM,UUID

       $table.VM = $vm.name
       $table.UUID = ($vm | Get-AdvancedSetting -Name cloud.uuid).Value
          
       $report += $table
   }
# Возвращаем все объекты
   $report
}
# Далее РУКАМИ парсим полученный результат

Усё лімітава проста і зразумела. Пішацца за пару хвілін з перапынкам на каву. Прыкруціць фільтраванне, і справа зроблена.

Але замерым час:

Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI

Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI

2 хвіліны 47 секунд пры апрацоўцы амаль 10k ВМ. Бонусам - адсутнасць фільтраў і неабходнасць уручную сартаваць вынік. Відавочна, што скрыпт патрабуе аптымізацыі.

Ранспейсы першымі прыходзяць на дапамогу, калі трэба адначасова атрымаць метрыкі хастоў з vCenter або патрабуецца апрацаваць дзясяткі тысяч аб'ектаў. Паглядзім, што дасць гэты падыход.

Уключаем першую хуткасць: PowerShell Runspaces

Першае, што прыходзіць на розум для гэтага скрыпту: выканаць цыкл не паслядоўна, а ў паралельных патоках, сабраць усе дадзеныя ў адзін аб'ект і адфільтраваць. 

Але ёсць праблема: PowerCLI не дазволіць нам адчыняць мноства незалежных сесій да vCenter і выкіне вясёлую памылку:

You have modified the global:DefaultVIServer and global:DefaultVIServers system variables. This is not allowed. Please reset them to $null and reconnect to the vSphere server.

Каб яе вырашыць, трэба спачатку перадаць унутр струменя інфармацыю аб сесіі. Успамінаем, што PowerShell працуе з аб'ектамі, якія можна перадаваць у якасці параметру хоць у функцыю, хоць у ScriptBlock. Перададзім сесію ў выглядзе такога аб'екта ў абыход $global:DefaultVIServers (Connect-VIServer з ключом -NotDefault):

$ConnectionString = @()
foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

Цяпер рэалізуем мультыструменнасць праз Runspace Pools.  

Алгарытм наступны:

  1. Атрымліваем спіс усіх ВМ.
  2. У паралельных патоках атрымліваем cloud.uuid.
  3. Дадзеныя з плыняў збіраем у адзін аб'ект.
  4. Фільтруем аб'ект праз групоўку па значэнні поля CloudUUID: тыя, дзе колькасць унікальных значэнняў большая за 1, і ёсць шуканыя ВМ.

У выніку атрымліваем скрыпт:


function Get-VMCloudUUID {
   param (
       [string[]]
       [ValidateNotNullOrEmpty()]
       $vCenters = @(),
       [int]$MaxThreads,
       [System.Management.Automation.PSCredential]
       [System.Management.Automation.Credential()]
       $Credential
   )

   $ConnectionString = @()

   # Создаем объект с сессионным ключом
   foreach ($vCenter in $vCenters)
   {
       try {
           $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -AllLinked -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
       }
       catch {
           if ($er.Message -like "*not part of a linked mode*")
           {
               try {
                   $ConnectionString += Connect-VIServer -Server $vCenter -Credential $Credential -NotDefault -Force -ErrorAction stop -WarningAction SilentlyContinue -ErrorVariable er
               }
               catch {
                   throw $_
               }
              
           }
           else {
               throw $_
           }
       }
   }

   # Получаем список всех ВМ
   $Global:AllVMs = Get-VM -Server $ConnectionString

   # Поехали!
   $ISS = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
   $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $MaxThreads, $ISS, $Host)
   $RunspacePool.ApartmentState = "MTA"
   $RunspacePool.Open()
   $Jobs = @()

# ScriptBlock с магией!)))
# Именно он будет выполняться в потоке
   $scriptblock = {
       Param (
       $ConnectionString,
       $VM
       )

       $Data = $VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

       return $Data
   }
# Генерируем потоки

   foreach($VM in $AllVMs)
   {
       $PowershellThread = [PowerShell]::Create()
# Добавляем скрипт
       $null = $PowershellThread.AddScript($scriptblock)
# И объекты, которые передадим в качестве параметров скрипту
       $null = $PowershellThread.AddArgument($ConnectionString)
       $null = $PowershellThread.AddArgument($VM)
       $PowershellThread.RunspacePool = $RunspacePool
       $Handle = $PowershellThread.BeginInvoke()
       $Job = "" | Select-Object Handle, Thread, object
       $Job.Handle = $Handle
       $Job.Thread = $PowershellThread
       $Job.Object = $VM.ToString()
       $Jobs += $Job
   }

# Ставим градусник, чтобы наглядно отслеживать выполнение заданий
# И здесь же прибиваем отработавшие задания
   While (@($Jobs | Where-Object {$_.Handle -ne $Null}).count -gt 0)
   {
       $Remaining = "$($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).object)"

       If ($Remaining.Length -gt 60) {
           $Remaining = $Remaining.Substring(0,60) + "..."
       }

       Write-Progress -Activity "Waiting for Jobs - $($MaxThreads - $($RunspacePool.GetAvailableRunspaces())) of $MaxThreads threads running" -PercentComplete (($Jobs.count - $($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False}).count)) / $Jobs.Count * 100) -Status "$(@($($Jobs | Where-Object {$_.Handle.IsCompleted -eq $False})).count) remaining - $remaining"

       ForEach ($Job in $($Jobs | Where-Object {$_.Handle.IsCompleted -eq $True})){
           $Job.Thread.EndInvoke($Job.Handle)     
           $Job.Thread.Dispose()
           $Job.Thread = $Null
           $Job.Handle = $Null
       }
   }

   $RunspacePool.Close() | Out-Null
   $RunspacePool.Dispose() | Out-Null
}


function Get-CloudUUID2
{
   [CmdletBinding()]
   param(
   [string[]]
   [ValidateNotNullOrEmpty()]
   $vCenters = @(),
   [int]$MaxThreads = 50,
   [System.Management.Automation.PSCredential]
   [System.Management.Automation.Credential()]
   $Credential)

   if(!$Credential)
   {
       $Credential = Get-Credential -Message "Please enter vCenter credentials."
   }

   # Вызов функции Get-VMCloudUUID, где мы распараллеливаем операцию
   $AllCloudVMs = Get-VMCloudUUID -vCenters $vCenters -MaxThreads $MaxThreads -Credential $Credential
   $Result = $AllCloudVMs | Sort-Object Value | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group
   $Result
}

Хараство гэтага скрыпту ў тым, што яго можна выкарыстоўваць і ў іншых падобных выпадках, проста замяніўшы ScriptBlock і параметры, якія будуць перададзены ў струмень. Exploit it!

Замяраем час:

Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI

55 секунд. Ужо лепш, але ўсё роўна мага хутчэй. 

Пераходзім на другую хуткасць: GetView

Высвятляем, што не так.
Першае і відавочнае: камандлет Get-VM выконваецца доўга.
Другое: камандлет Get-AdvancedOptions выконваецца яшчэ даўжэй.
Спачатку разбяромся з другім. 

Get-AdvancedOptions зручны на асобных аб'ектах ВМ, але вельмі непаваротлівы пры працы са мноствам аб'ектаў. Тую ж самую інфармацыю мы можам атрымаць з самага аб'екта віртуальнай машыны (Get-VM). Проста яна добра закапана ў аб'екце ExtensionData. Узброіўшыся фільтраваннем, паскараем працэс атрымання патрэбных дадзеных.

Лёгкім рухам рукі гэта:


VM | Get-AdvancedSetting -Name Cloud.uuid -Server $ConnectionString | Select-Object @{N="VMName";E={$_.Entity.Name}},@{N="CloudUUID";E={$_.Value}},@{N="PowerState";E={$_.Entity.PowerState}}

Ператвараецца ў гэта:


$VM | Where-Object {($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.ExtensionData.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}}

Выснова тая ж, што і ў Get-AdvancedOptions, але працуе ў разы хутчэй. 

Цяпер да Get-VM. Ён выконваецца не хутка, бо мае справу са складанымі аб'ектамі. Устае лагічнае пытанне: а навошта нам у дадзеным выпадку лішняя інфармацыя і монструозны PSObject, калі нам усяго-то трэба імя ВМ, яе стан і значэнне хітрага атрыбуту?  

Да таго ж, са скрыпту сышоў тормаз у асобе Get-AdvancedOptions. Ужыванне Runspace Pools зараз выглядае празмернасцю, бо больш няма неабходнасці ў распаралельванні павольнай задачы ў струменях з прысяданнямі пры перадачы сесіі. Інструмент добры, але не для гэтага кейса. 

Глядзім на выснову ExtensionData: гэта не што іншае, як аб'ект Get-View. 

Паклічам старажытную тэхніку майстроў PowerShell: one line з ужываннем фільтраў, сартаванняў і групоўкі. Увесь папярэдні жах элегантна схлопваецца ў адзін радок і выконваецца ў адной сесіі:


$AllVMs = Get-View -viewtype VirtualMachine -Property Name,Config.ExtraConfig,summary.runtime.powerstate | Where-Object {($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value -ne $null} | Select-Object @{N="VMName";E={$_.Name}},@{N="CloudUUID";E={($_.Config.ExtraConfig | Where-Object {$_.key -eq "cloud.uuid"}).Value}},@{N="PowerState";E={$_.summary.runtime.powerstate}} | Sort-Object CloudUUID | Group-Object -Property CloudUUID | Where-Object -FilterScript {$_.Count -gt 1} | Select-Object -ExpandProperty Group

Замяраем час:

Як пабудаваць ракетны паскаральнік для скрыптоў PowerCLI

9 секунд для амаль 10k аб'ектаў з фільтраваннем па патрэбнай умове. Выдатна!

замест заключэння

Прымальны вынік напрамую залежыць ад выбару прылады. Часцяком складана сказаць напэўна, што менавіта трэба абраць для яго дасягнення. Кожны з пералічаных метадаў паскарэння скрыптоў добры ў межах сваёй дастасавальнасці. Спадзяюся, дадзены артыкул дапаможа вам у нялёгкай справе зразумення асноў аўтаматызацыі працэсаў і іх аптымізацыі ў вашай інфраструктуры.

PS: Аўтар дзякуе ўсім удзельнікам камуны за дапамогу і падтрымку пры падрыхтоўцы артыкула. Нават тых, у каго лапкі. І нават у каго лапак няма, як у ўдава.

Крыніца: habr.com

Дадаць каментар