Cómo construir un propulsor de cohetes para scripts PowerCLI 

Tarde o temprano, cualquier administrador de sistemas VMware llega a automatizar las tareas rutinarias. Todo comienza con la línea de comando, luego viene PowerShell o VMware PowerCLI.

Supongamos que domina PowerShell un poco más que iniciar ISE y utilizar cmdlets estándar de módulos que funcionan gracias a "algún tipo de magia". Cuando empiece a contar máquinas virtuales por cientos, descubrirá que los scripts que ayudan a pequeña escala se ejecutan notablemente más lentos a gran escala. 

En esta situación, 2 herramientas ayudarán:

  • Espacios de ejecución de PowerShell – un enfoque que le permite paralelizar la ejecución de procesos en subprocesos separados; 
  • Obtener-Ver – una función básica de PowerCLI, análoga a Get-WMIObject en Windows. Este cmdlet no extrae objetos que acompañan a las entidades, sino que recibe información en forma de un objeto simple con tipos de datos simples. En muchos casos sale más rápido.

A continuación, hablaré brevemente sobre cada herramienta y mostraré ejemplos de uso. Analicemos guiones específicos y veamos cuándo uno funciona mejor que el otro. ¡Ir!

Cómo construir un propulsor de cohetes para scripts PowerCLI

Primera etapa: Runspace

Entonces, Runspace está diseñado para el procesamiento paralelo de tareas fuera del módulo principal. Por supuesto, puedes iniciar otro proceso que consumirá algo de memoria, procesador, etc. Si tu script se ejecuta en un par de minutos y consume un gigabyte de memoria, lo más probable es que no necesites Runspace. Pero para scripts de decenas de miles de objetos es necesario.

Puedes empezar a aprender aquí: 
Uso inicial de los espacios de ejecución de PowerShell: Parte 1

¿Qué aporta el uso de Runspace?

  • velocidad limitando la lista de comandos ejecutados,
  • ejecución paralela de tareas,
  • seguridad

Aquí hay un ejemplo de Internet en el que Runspace ayuda:

“La contención de almacenamiento es una de las métricas más difíciles de rastrear en vSphere. Dentro de vCenter, no se puede simplemente ir a ver qué VM está consumiendo más recursos de almacenamiento. Afortunadamente, puedes recopilar estos datos en minutos gracias a PowerShell.
Compartiré un script que permitirá a los administradores de sistemas VMware buscar rápidamente en vCenter y recibir una lista de VM con datos sobre su consumo promedio.  
El script utiliza espacios de ejecución de PowerShell para permitir que cada host ESXi recopile información de consumo de sus propias máquinas virtuales en un espacio de ejecución separado e informe inmediatamente sobre la finalización. Esto permite que PowerShell cierre trabajos inmediatamente, en lugar de iterar a través de los hosts y esperar a que cada uno complete su solicitud”.

Fuente: Cómo mostrar E/S de máquina virtual en un panel de ESXi

En el siguiente caso, Runspace ya no es útil:

“Estoy intentando escribir un script que recopile una gran cantidad de datos de una máquina virtual y escriba datos nuevos cuando sea necesario. El problema es que hay bastantes máquinas virtuales y se dedican entre 5 y 8 segundos a una máquina”. 

Fuente: PowerCLI multiproceso con RunspacePool

Aquí necesitará Get-View, pasemos a ello. 

Segunda etapa: Obtener-Ver

Para comprender por qué Get-View es útil, vale la pena recordar cómo funcionan los cmdlets en general. 

Se necesitan cmdlets para obtener información cómodamente sin la necesidad de estudiar libros de referencia de API y reinventar la siguiente rueda. Lo que antes requería cien o dos líneas de código, PowerShell te permite hacerlo con un solo comando. Pagamos por esta comodidad con rapidez. No hay magia dentro de los propios cmdlets: el mismo guión, pero en un nivel inferior, escrito por las hábiles manos de un maestro de la soleada India.

Ahora, para comparar con Get-View, tomemos el cmdlet Get-VM: accede a la máquina virtual y devuelve un objeto compuesto, es decir, le adjunta otros objetos relacionados: VMHost, Datastore, etc.  

Get-View en su lugar no agrega nada innecesario al objeto devuelto. Además, nos permite especificar estrictamente qué información necesitamos, lo que facilitará la salida del objeto. En Windows Server en general y en Hyper-V en particular, el cmdlet Get-WMIObject es un análogo directo: la idea es exactamente la misma.

Get-View es inconveniente para operaciones de rutina en objetos puntuales. Pero cuando se trata de miles y decenas de miles de objetos, no tiene precio.

Puede leer más en el blog de VMware: Introducción a Get-View

Ahora os lo mostraré todo utilizando un caso real. 

Escribir un script para descargar una VM

Un día mi colega me pidió que optimizara su guión. La tarea es una rutina común: encontrar todas las máquinas virtuales con un parámetro cloud.uuid duplicado (sí, esto es posible al clonar una máquina virtual en vCloud Director). 

La solución obvia que me viene a la mente es:

  1. Obtenga una lista de todas las máquinas virtuales.
  2. Analiza la lista de alguna manera.

La versión original era este sencillo script:

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
}
# Далее РУКАМИ парсим полученный результат

Todo es extremadamente simple y claro. Se puede escribir en un par de minutos con una pausa para el café. Enroscamos la filtración y listo.

Pero midamos el tiempo:

Cómo construir un propulsor de cohetes para scripts PowerCLI

Cómo construir un propulsor de cohetes para scripts PowerCLI

2 minutos 47 segundos al procesar casi 10 máquinas virtuales. Una ventaja es la ausencia de filtros y la necesidad de ordenar los resultados manualmente. Obviamente, el script requiere optimización.

Los espacios de ejecución son los primeros en acudir al rescate cuando necesita obtener simultáneamente métricas de host de vCenter o procesar decenas de miles de objetos. Veamos qué aporta este enfoque.

Active la primera velocidad: PowerShell Runspaces

Lo primero que me viene a la mente para este script es ejecutar el bucle no secuencialmente, sino en subprocesos paralelos, recopilar todos los datos en un objeto y filtrarlos. 

Pero hay un problema: PowerCLI no nos permitirá abrir muchas sesiones independientes a vCenter y arrojará un error curioso:

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.

Para resolver esto, primero debe pasar la información de la sesión dentro de la transmisión. Recordemos que PowerShell trabaja con objetos que se pueden pasar como parámetro ya sea a una función o a un ScriptBlock. Pasemos la sesión en forma de dicho objeto, sin pasar por $global:DefaultVIServers (Connect-VIServer con la clave -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 $_
           }
       }
   }

Ahora implementemos subprocesos múltiples a través de Runspace Pools.  

El algoritmo es como sigue:

  1. Obtenemos una lista de todas las máquinas virtuales.
  2. En transmisiones paralelas obtenemos cloud.uuid.
  3. Recopilamos datos de transmisiones en un solo objeto.
  4. Filtramos el objeto agrupándolo por el valor del campo CloudUUID: aquellas donde el número de valores únicos es mayor que 1 son las VM que buscamos.

Como resultado, obtenemos el guión:


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
}

Lo bueno de este script es que se puede usar en otros casos similares simplemente reemplazando el ScriptBlock y los parámetros que se pasarán a la secuencia. ¡Explotalo!

Medimos el tiempo:

Cómo construir un propulsor de cohetes para scripts PowerCLI

55 segundos. Es mejor, pero aún puede ser más rápido. 

Pasemos a la segunda velocidad: GetView

Averigüemos qué pasa.
En primer lugar, el cmdlet Get-VM tarda mucho en ejecutarse.
En segundo lugar, el cmdlet Get-AdvancedOptions tarda aún más en completarse.
Ocupémonos primero del segundo. 

Get-AdvancedOptions es conveniente para objetos VM individuales, pero muy complicado cuando se trabaja con muchos objetos. Podemos obtener la misma información del propio objeto de la máquina virtual (Get-VM). Simplemente está bien enterrado en el objeto ExtensionData. Armados con filtrado, aceleramos el proceso de obtención de los datos necesarios.

Con un ligero movimiento de la mano esto es:


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

Se convierte en esto:


$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}}

El resultado es el mismo que Get-AdvancedOptions, pero funciona muchas veces más rápido. 

Ahora a Get-VM. No es rápido porque trata con objetos complejos. Surge una pregunta lógica: ¿por qué necesitamos información adicional y un PSObject monstruoso en este caso, cuando solo necesitamos el nombre de la VM, su estado y el valor de un atributo complicado?  

Además, se ha eliminado del script el obstáculo en forma de Get-AdvancedOptions. El uso de Runspace Pools ahora parece excesivo, ya que ya no es necesario paralelizar una tarea lenta entre subprocesos en cuclillas al transferir una sesión. La herramienta es buena, pero no para este caso. 

Veamos el resultado de ExtensionData: no es más que un objeto Get-View. 

Recurramos a la antigua técnica de los maestros de PowerShell: una línea usando filtros, ordenando y agrupando. Todo el horror anterior se resume elegantemente en una línea y se ejecuta en una sola sesión:


$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

Medimos el tiempo:

Cómo construir un propulsor de cohetes para scripts PowerCLI

segundos 9 para casi 10k objetos con filtrado por la condición deseada. ¡Excelente!

En lugar de una conclusión

Un resultado aceptable depende directamente de la elección de la herramienta. A menudo es difícil decir con certeza qué se debe elegir exactamente para lograrlo. Cada uno de los métodos enumerados para acelerar los scripts es bueno dentro de los límites de su aplicabilidad. Espero que este artículo le ayude en la difícil tarea de comprender los conceptos básicos de la automatización y optimización de procesos en su infraestructura.

PD: El autor agradece a todos los miembros de la comunidad por su ayuda y apoyo en la preparación del artículo. Incluso aquellos con patas. E incluso aquellos que no tienen piernas, como la boa constrictor.

Fuente: habr.com

Añadir un comentario