Comment créer un booster de fusée pour les scripts PowerCLI 

Tôt ou tard, tout administrateur système VMware en vient à automatiser les tâches de routine. Tout commence par la ligne de commande, puis vient PowerShell ou VMware PowerCLI.

Disons que vous maîtrisez PowerShell un peu plus loin que le lancement d'ISE et l'utilisation d'applets de commande standard à partir de modules qui fonctionnent grâce à « une sorte de magie ». Lorsque vous commencez à compter les machines virtuelles par centaines, vous constaterez que les scripts utiles à petite échelle s’exécutent nettement plus lentement à grande échelle. 

Dans cette situation, 2 outils vous aideront :

  • Espaces d'exécution PowerShell – une approche qui permet de paralléliser l'exécution de processus dans des threads séparés ; 
  • Obtenir une vue – une fonction PowerCLI de base, un analogue de Get-WMIObject sous Windows. Cette applet de commande n'extrait pas les objets accompagnant les entités, mais reçoit des informations sous la forme d'un objet simple avec des types de données simples. Dans de nombreux cas, cela sort plus rapidement.

Ensuite, je parlerai brièvement de chaque outil et montrerai des exemples d’utilisation. Analysons des scripts spécifiques et voyons quand l'un fonctionne mieux que l'autre. Aller!

Comment créer un booster de fusée pour les scripts PowerCLI

Première étape : Runspace

Ainsi, Runspace est conçu pour le traitement parallèle de tâches en dehors du module principal. Bien sûr, vous pouvez lancer un autre processus qui consommera de la mémoire, du processeur, etc. Si votre script s'exécute en quelques minutes et consomme un gigaoctet de mémoire, vous n'aurez probablement pas besoin de Runspace. Mais pour les scripts concernant des dizaines de milliers d’objets, cela est nécessaire.

Vous pouvez commencer à apprendre ici : 
Début de l'utilisation des espaces d'exécution PowerShell : partie 1

Que donne l'utilisation de Runspace :

  • la vitesse en limitant la liste des commandes exécutées,
  • exécution parallèle des tâches,
  • la sécurité.

Voici un exemple tiré d'Internet où Runspace peut vous aider :

« Les conflits de stockage sont l'une des mesures les plus difficiles à suivre dans vSphere. Dans vCenter, vous ne pouvez pas simplement aller voir quelle VM consomme le plus de ressources de stockage. Heureusement, vous pouvez collecter ces données en quelques minutes grâce à PowerShell.
Je partagerai un script qui permettra aux administrateurs système VMware de rechercher rapidement dans vCenter et de recevoir une liste de machines virtuelles avec des données sur leur consommation moyenne.  
Le script utilise les espaces d'exécution PowerShell pour permettre à chaque hôte ESXi de collecter des informations de consommation à partir de ses propres machines virtuelles dans un espace d'exécution distinct et de signaler immédiatement l'achèvement. Cela permet à PowerShell de fermer les tâches immédiatement, plutôt que de parcourir les hôtes et d'attendre que chacun termine sa demande.

Source: Comment afficher les E/S d'une machine virtuelle sur un tableau de bord ESXi

Dans le cas ci-dessous, Runspace n'est plus utile :

« J'essaie d'écrire un script qui collecte beaucoup de données à partir d'une VM et écrit de nouvelles données si nécessaire. Le problème est qu’il y a beaucoup de machines virtuelles et que 5 à 8 secondes sont passées sur une seule machine. 

Source: PowerCLI multithread avec RunspacePool

Ici, vous aurez besoin de Get-View, passons à cela. 

Deuxième étape : Get-View

Pour comprendre pourquoi Get-View est utile, il convient de rappeler le fonctionnement général des applets de commande. 

Les applets de commande sont nécessaires pour obtenir facilement des informations sans avoir besoin d'étudier les ouvrages de référence sur les API et de réinventer la roue suivante. Ce qui nécessitait autrefois cent ou deux lignes de code, PowerShell vous permet de le faire avec une seule commande. Nous payons cette commodité avec rapidité. Il n'y a pas de magie à l'intérieur des applets de commande elles-mêmes : le même script, mais à un niveau inférieur, écrit par les mains habiles d'un maître de l'Inde ensoleillée.

Maintenant, à titre de comparaison avec Get-View, prenons l'applet de commande Get-VM : elle accède à la machine virtuelle et renvoie un objet composite, c'est-à-dire qu'elle y attache d'autres objets associés : VMHost, Datastore, etc.  

Get-View à sa place n'ajoute rien d'inutile à l'objet renvoyé. De plus, cela nous permet de spécifier strictement les informations dont nous avons besoin, ce qui facilitera la production de l'objet de sortie. Dans Windows Server en général et dans Hyper-V en particulier, la cmdlet Get-WMIObject est un analogue direct - l'idée est exactement la même.

Get-View n'est pas pratique pour les opérations de routine sur des objets ponctuels. Mais lorsqu’il s’agit de milliers et de dizaines de milliers d’objets, cela n’a pas de prix.

Vous pouvez en savoir plus sur le blog VMware : Introduction à Get-View

Maintenant, je vais tout vous montrer en utilisant un cas réel. 

Écrire un script pour décharger une VM

Un jour mon collègue m'a demandé d'optimiser son script. La tâche est une routine courante : rechercher toutes les machines virtuelles avec un paramètre cloud.uuid en double (oui, cela est possible lors du clonage d'une VM dans vCloud Director). 

La solution évidente qui me vient à l’esprit est la suivante :

  1. Obtenez une liste de toutes les machines virtuelles.
  2. Analysez la liste d'une manière ou d'une autre.

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

Tout est extrêmement simple et clair. Il peut être rédigé en quelques minutes avec une pause-café. Vissez la filtration et le tour est joué.

Mais mesurons le temps :

Comment créer un booster de fusée pour les scripts PowerCLI

Comment créer un booster de fusée pour les scripts PowerCLI

2 minutes 47 secondes lors du traitement de près de 10 XNUMX machines virtuelles. Un bonus est l'absence de filtres et la nécessité de trier manuellement les résultats. Évidemment, le script nécessite une optimisation.

Les espaces d'exécution sont les premiers à venir à la rescousse lorsque vous devez obtenir simultanément des métriques d'hôte de vCenter ou traiter des dizaines de milliers d'objets. Voyons ce qu'apporte cette approche.

Activez la première vitesse : PowerShell Runspaces

La première chose qui vient à l'esprit pour ce script est d'exécuter la boucle non pas séquentiellement, mais dans des threads parallèles, de collecter toutes les données dans un seul objet et de les filtrer. 

Mais il y a un problème : PowerCLI ne nous permettra pas d'ouvrir de nombreuses sessions indépendantes sur vCenter et générera une drôle d'erreur :

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.

Pour résoudre ce problème, vous devez d'abord transmettre les informations de session dans le flux. Rappelons que PowerShell fonctionne avec des objets qui peuvent être passés en paramètre soit à une fonction, soit à un ScriptBlock. Passons la session sous la forme d'un tel objet, en contournant $global:DefaultVIServers (Connect-VIServer avec la clé -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 $_
           }
       }
   }

Implémentons maintenant le multithreading via Runspace Pools.  

L'algorithme est le suivant:

  1. Nous obtenons une liste de toutes les VM.
  2. Dans les flux parallèles, nous obtenons cloud.uuid.
  3. Nous collectons les données des flux dans un seul objet.
  4. Nous filtrons l'objet en le regroupant par la valeur du champ CloudUUID : celles où le nombre de valeurs uniques est supérieur à 1 sont les VM que nous recherchons.

En conséquence, nous obtenons le script :


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
}

La beauté de ce script est qu'il peut être utilisé dans d'autres cas similaires en remplaçant simplement le ScriptBlock et les paramètres qui seront passés au flux. Exploitez-le !

Nous mesurons le temps :

Comment créer un booster de fusée pour les scripts PowerCLI

55 secondes. C'est mieux, mais cela peut encore être plus rapide. 

Passons à la deuxième vitesse : GetView

Voyons ce qui ne va pas.
Tout d’abord, l’applet de commande Get-VM prend beaucoup de temps à s’exécuter.
Deuxièmement, l’exécution de l’applet de commande Get-AdvancedOptions prend encore plus de temps.
Parlons d'abord du deuxième. 

Get-AdvancedOptions est pratique pour les objets VM individuels, mais très maladroit lorsque vous travaillez avec de nombreux objets. Nous pouvons obtenir les mêmes informations à partir de l'objet machine virtuelle lui-même (Get-VM). C'est juste bien enfoui dans l'objet ExtensionData. Armés de filtrage, nous accélérons le processus d'obtention des données nécessaires.

D'un léger mouvement de la main, cela donne :


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 transforme en ceci :


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

Le résultat est le même que celui de Get-AdvancedOptions, mais il fonctionne plusieurs fois plus rapidement. 

Passons maintenant à Get-VM. Ce n'est pas rapide car il s'agit d'objets complexes. Une question logique se pose : pourquoi avons-nous besoin d’informations supplémentaires et d’un PSObject monstrueux dans ce cas, alors que nous avons juste besoin du nom de la VM, de son état et de la valeur d’un attribut délicat ?  

De plus, l'obstacle sous la forme de Get-AdvancedOptions a été supprimé du script. L’utilisation de Runspace Pools semble désormais exagérée car il n’est plus nécessaire de paralléliser une tâche lente sur des threads squats lors du transfert d’une session. L'outil est bon mais pas pour ce cas. 

Regardons le résultat d'ExtensionData : ce n'est rien de plus qu'un objet Get-View. 

Faisons appel à l'ancienne technique des maîtres du PowerShell : une ligne utilisant des filtres, un tri et un regroupement. Toute l’horreur précédente est élégamment regroupée en une seule ligne et exécutée en une seule session :


$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

Nous mesurons le temps :

Comment créer un booster de fusée pour les scripts PowerCLI

9 secondes pour près de 10 XNUMX objets avec filtrage selon la condition souhaitée. Super!

Au lieu d'une conclusion

Un résultat acceptable dépend directement du choix de l'outil. Il est souvent difficile de dire avec certitude ce qu’il faut choisir exactement pour y parvenir. Chacune des méthodes répertoriées pour accélérer les scripts est bonne dans la limite de son applicabilité. J'espère que cet article vous aidera dans la tâche difficile de comprendre les bases de l'automatisation et de l'optimisation des processus dans votre infrastructure.

PS: L'auteur remercie tous les membres de la communauté pour leur aide et leur soutien dans la préparation de l'article. Même ceux qui ont des pattes. Et même ceux qui n’ont pas de pattes, comme le boa constrictor.

Source: habr.com

Ajouter un commentaire