Comment construire un accélérateur de fusée pour les scripts PowerCLI 

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

Supposons que vous maîtrisiez PowerShell un peu plus loin que de démarrer ISE et d'utiliser des applets de commande standard à partir de modules qui fonctionnent avec une sorte de magie. Lorsque vous commencez à compter des centaines de machines virtuelles, vous constaterez que les scripts qui ont aidé à petite échelle fonctionnent beaucoup plus lentement sur les grandes. 

Dans cette situation, 2 outils vous aideront:

  • PowerShell Runspaces - une approche qui vous permet de paralléliser l'exécution de processus dans des threads séparés; 
  • Get-View est une fonction PowerCLI de base, un analogue de Get-WMIObject sur Windows. Cette applet de commande ne fait pas glisser les objets qui lui sont liés, mais reçoit des informations sous la forme d'un objet simple avec des types de données simples. Dans de nombreux cas, il sort plus rapidement.

Ensuite, je vais parler brièvement de chaque outil et montrer des exemples d'utilisation. Nous analyserons des scripts spécifiques et verrons quand l'un fonctionne le mieux, quand le second. Aller!



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 démarrer un autre processus qui consommera de la mémoire, du processeur, etc. Si votre script s'exécute en quelques minutes et dépense un gigaoctet de mémoire, vous n'aurez probablement pas besoin de Runspace. Mais pour les scripts sur des dizaines de milliers d'objets, c'est nécessaire.
Vous pouvez démarrer le développement à partir d'ici: 
Début de l'utilisation des espaces d'exécution PowerShell: Partie 1

Ce qui donne l'utilisation de Runspace:

  • vitesse en limitant la liste des commandes exécutables,
  • tâches parallèles
  • sécurité.

Voici un exemple tiré d'Internet lorsque Runspace vous aide:
« – , vSphere. vCenter , . , PowerShell.
, VMware vCenter .  
PowerShell runspaces, ESXi Runspace . PowerShell , , ».

: How to Show Virtual Machine I/O on an ESXi Dashboard

Dans le cas ci-dessous, Runspace n'est plus en activité:
«J'essaie d'écrire un script qui recueille beaucoup de données de la machine virtuelle et, si nécessaire, écrit de nouvelles données. Le problème est qu'il y a beaucoup de VM, et cela prend 5-8 secondes pour une machine. » 

Source: Multithreading PowerCLI avec RunspacePool

Get-View est nécessaire ici, passons à cela. 

Deuxième étape: Get-View


Pour comprendre l'utilité de Get-View, n'oubliez pas comment les applets de commande fonctionnent en général. 

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

Maintenant, pour comparer avec Get-View, prenez 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 ne visse rien de plus dans l'objet retourné. De plus, il vous permet d'indiquer de manière rigide exactement les informations dont nous avons besoin, ce qui facilitera la sortie de l'objet. Dans Windows Server en général, et dans Hyper-V en particulier, l'applet de commande Get-WMIObject est un analogue direct - l'idée est exactement la même.

Get-View n'est pas pratique dans les opérations de routine sur les entités ponctuelles. Mais quand il s'agit de milliers et de dizaines de milliers d'objets, il n'a pas de prix.
En savoir plus sur le blog VMware: Introduction à Get-View

Maintenant, je vais tout montrer sur un vrai cas. 

Nous écrivons un script pour décharger une VM


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

La solution évidente qui vient à l'esprit:

  1. Obtenez une liste de toutes les machines virtuelles.
  2. En quelque sorte analyser la liste.

La version originale était un script si simple:

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 est écrit en quelques minutes avec une pause-café. Vissez le filtre et c'est fait.

Mais mesurez le temps:





2 minutes 47 secondes lors du traitement de près de 10k VM. Un bonus est le manque de filtres et la nécessité de trier manuellement le résultat. De toute évidence, le script demande une optimisation.

Les ransepses sont les premiers à venir à la rescousse lorsque vous devez obtenir des métriques d'hôte avec vCenter en même temps ou que vous devez traiter des dizaines de milliers d'objets. Voyons ce que cette approche donnera.

Nous activons 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 en série, mais dans des threads parallèles, collecter toutes les données dans un objet et 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 erreur amusante:

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 le résoudre, vous devez d'abord transmettre les informations de session dans le flux. Nous rappelons que PowerShell fonctionne avec des objets qui peuvent être passés en paramètre à au moins une fonction, au moins à ScriptBlock. Passons la session en tant qu'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 $_
           }
       }
   }

Maintenant, nous implémentons le multithreading via les pools d'espace d'exécution.  

L'algorithme est le suivant:

  1. Obtenez une liste de toutes les machines virtuelles.
  2. Dans les threads 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: ceux où le nombre de valeurs uniques est supérieur à 1 et où il y a les machines virtuelles souhaitées.

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 transférés au flux. Exploitez-le!

Nous mesurons le temps:



55 secondes. Déjà mieux, mais toujours plus rapide. 

Nous passons à la deuxième vitesse: GetView

Nous découvrons ce qui ne va pas.
Première et évidente: l'applet de commande Get-VM prend beaucoup de temps.
Deuxièmement: l'applet de commande Get-AdvancedOptions s'exécute encore plus longtemps.
Commençons par le second. 

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

Avec un coup de poignet, c'est:


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

La conclusion est la même que Get-AdvancedOptions, mais cela fonctionne plusieurs fois plus rapidement. 

Passons maintenant à Get-VM. Il n'est pas exécuté rapidement, car il traite des 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 de l'attribut délicat?  

De plus, le frein face à Get-AdvancedOptions a laissé le script. L'utilisation des pools d'espace d'exécution semble désormais exagérée, car il n'est plus nécessaire de paralléliser une tâche lente dans des flux avec des squats lors du transfert d'une session. L'outil est bon, mais pas pour ce cas. 

Nous regardons la sortie d'ExtensionData: ce n'est rien d'autre qu'un objet Get-View. 

Appelons l'ancienne technique des maîtres PowerShell: une ligne utilisant des filtres, des tris et des regroupements. Toutes les horreurs précédentes s'effondrent avec élégance sur une seule ligne et sont exécutées 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:



9 secondes pour près de 10 000 objets avec filtrage en fonction de la condition souhaitée. Bien!

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 qui devrait être choisi exactement pour y parvenir. Chacune des méthodes d'accélération de script répertoriées est bonne dans les limites de son applicabilité. J'espère que cet article vous aidera dans la tâche difficile de comprendre les bases de l'automatisation des processus et leur optimisation dans votre infrastructure.

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

All Articles