Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Simplifier l’injection des paramètres dynamiques dans Azure Machine Configuration avec Terraform

Dans notre précédent article, nous avons vu comment Azure Machine Configuration permettait d’injecter dynamiquement des paramètres dans des configurations DSC génériques.
La mécanique fonctionne, mais personne n’utilise de template ARM pour gérer ses déploiements au quotidien!
Heureusement, la ressource azurerm_policy_virtual_machine_configuration_assignment du provider AzureRM de Terraform supporte également l’injection des paramètres via un ou plusieurs attributs « parameter » dans le bloc « configuration » :

resource "azurerm_policy_virtual_machine_configuration_assignment" "iis_config" {
  name               = "IIS_Standard"
  location           = azurerm_resource_group.rg.location
  virtual_machine_id = azurerm_windows_virtual_machine.web_server.id

  configuration {
    assignment_type = "ApplyAndAutoCorrect"
    version         = "1.0"
    content_uri     = "https://mystorage.blob.core.windows.net/configs/IIS.zip"
    content_hash    = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D0E1F2"

    parameter {
      name  = "[WebSite]MyCompanySite;PhysicalPath"
      value = "D:\\ProductionWeb\\MySite"
    }

    parameter {
      name  = "[WebSite]MyCompanySite;State"
      value = "Started"
    }

  parameter {
      name  = "[WindowsFeature]AspNet45;Ensure"
      value = "Present"
    }
  }
}

Le problème? Même si ce code est plus lisible qu’un template ARM en JSON, il souffre du même défaut structurel : la syntaxe name/value pour l’injection des paramètres est strictement la même, toujours aussi verbeuse et répétitive.
Le code n’en est pas moins fonctionnel, mais sa lisibilité devient douteuse lorsqu’il s’agit de configurer des déploiements complexes avec des dizaines de pramètres !

Mais contrairement à ARM, Terraform dispose d’une solution évidente pour améliorer la situation : les modules, qui permettent de simplifier la manipulation de structures complexes et répétitives. Mon objectif devient alors évident : créer un module qui permet la saisie de paramètres en suivant la syntaxe naturelle d’un script DSC, et laisser Terraform faire le travail de formatage complexe en coulisses.

Un exemple valant mieux qu’un long discours, voici la syntaxe souhaitée :

parameters = {
  "[WebSite]MyCompanySite" = {
     PhysicalPath = "D:\\ProductionWeb\\MySite"
     State        = "Started"
  }
}

Voilà qui est bien plus simple, lisible et proche de la syntaxe DSC d’origine!

Pour que les informations contenues dans cette structure (techniquement un dictionnaire de dictionnaires, ou map(map(string)) soit compréhensible par la ressource azurerm_policy_virtual_machine_configuration_assignment, notre module doit la transformer. Terraform propose pour cela des outils de manipulation de données très puissants : les boucles for imbriquées et la fonction flatten.

Voici le cœur du moteur de notre module (dans le fichier main.tf) :

resource "azurerm_policy_virtual_machine_configuration_assignment" "this" {
  for_each = local.guest_configurations_map

  name               = each.key
  location           = var.location
  virtual_machine_id = var.virtual_machine_id

  configuration {
    assignment_type = var.assignment_type
    version         = var.configuration_version
    content_uri     = each.value.content_uri
    content_hash    = each.value.content_hash

    dynamic "parameter" {
      for_each = flatten([
        for param_name, param_values in each.value.parameters : [
          for key, value in param_values : {
            name  = "${param_name};${key}"
            value = value
          }
        ]
      ])
      content {
        name  = parameter.value.name
        value = parameter.value.value
      }
    }
  }
}

Que se passe-t-il exactement ?

Terraform parcourt notre dictionnaire principal parameters (for param_name, param_values…).

Pour chaque ressource DSC trouvée (ex: [WebSite]MyCompanySite), il parcourt ses propriétés (for key, value…).

Il concatène dynamiquement la clé principale, un point-virgule et la sous-clé : name = « ${param_name};${key} ».

La fonction flatten « écrase » ces listes imbriquées pour en faire une liste simple à un seul niveau, qui va ensuite être parcourue de facon classique par le for_each du bloc dynamic et transformé en autant de blocs « parameter » que nécessaire.

Maintenant que notre module est prêt, son appel depuis notre code principal devient un jeu d’enfant. Non seulement nous pouvons gérer facilement les paramètres complexes, mais nous pouvons également assigner plusieurs packages DSC à la même machine virtuelle en une seule fois :

module "machine_configuration" {
  source  = "github.com/Cyr-Az/terraform-azurerm-cyraz-machine-configuration"
  # version = "x.x.x"

  virtual_machine_id = azurerm_windows_virtual_machine.example.id
  location           = azurerm_resource_group.example.location

  guest_configurations =[
    {
      name         = "IIS_Standard"
      content_uri  = "https://mystorage.blob.core.windows.net/configs/IIS_Standard.zip"
      content_hash = "A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6A7B8C9D0E1F2"
      
      parameters = {
        "[WebSite]MyCompanySite" = {
          PhysicalPath = "D:\\ProductionWeb\\MySite"
          State        = "Started"
        }
        "[WindowsFeature]AspNet45" = {
          Ensure = "Present"
        }
      }
    },
    {
      name         = "Other_Config"
      content_uri  ="https://mystorage.blob.core.windows.net/configs/Other_Config.zip"
      content_hash = "B2C3D4E5..."

      parameters = {
        "[SomeResource]Name" = {
          Parameter = "Value"
        }
      }
    }
  ]
}

Vous pouvez retrouver ce module complet avec un example plus détaillé sur mon Github : Cyr-Az/terraform-azurerm-cyraz-machine-configuration: Terraform module to simplify Machine Configuration assignments with custom parameters

Azure – Utiliser des packages DSC génériques dans Machine Configuration

Le service Azure Machine Configuration (anciennement connu sous le nom de Guest Configuration) est une fonctionnalité qui parait encore méconnue ou boudée au profit d’autres mécanismes comme Ansible. Elle est pourtant la digne descendante du vénérable  PowerShell DSC (Desired State Configuration). et de ses implémentations Azure (extension DSC et Azure Automation DSC, dont la dépréciation a été annoncée pour 2027) : elle permet d’auditer et de configurer l’intérieur de vos machines virtuelles (paramètres de l’OS, registres, applications) à l’aide de packages constitués de modules et de configurations DSC classiques accessibles en HTTPS (souvent via un storage account).

A la différence des méthodes précédentes, Machine Configuration n’utilise pas le Local Configuration Manager (LCM) natif de Windows mais plutôt une extension Azure dédiée qui se charge de la récupération des packages, de l’application des configurations qu’ils contiennent et de la remontée des informations d’audit. Machine Configuration permet également d’appliquer plusieurs configurations (assignments) à une machine, alors que les méthodes traditionnelles ne pouvaient prendre en compte qu’une seule.

Un défaut historique de DSC réside dans ses configurations qui sont des fichiers .mof statiques, obtenus après « compilation » d’un script Powershell DSC. Il était donc nécessaire de créer un fichier .mof pour chaque serveur, au lieu de pouvoir simplement utiliser une configuration générique et lui passer les paramètres propres à chaque serveur.

Mais réjouissez-vous : c’est de l’histoire ancienne! Même si Machine Configuration repose toujours sur des packages contenant des .mof compilés et donc des valeurs statiques, il introduit la possibilité de les modifier directement via la propriété configurationParameter de la ressource guestConfigurationAssignments. Autrement dit, il est possible de créer des .mof contenant les ressources nécessaires à la configuration d’un role sur vos serveurs (par exemple IIS) avec une série de paramètres standardisés, mais avec des valeurs fictives; puis de modifier ces valeurs à la volée dans vos templates ARM en fonction du serveur auxquels vous appliquez les guestConfigurationAssignments!

Concrètement, comment cela fonctionne-t-il ? Lors de l’assignation de la configuration via l’API Azure (par exemple lors du déploiement de votre template ARM), le service transmet vos paramètres à l’agent local (l’extension Azure présente sur la VM). Ce dernier va alors lire le fichier MOF téléchargé et écraser littéralement les valeurs textuelles statiques avec celles que vous avez fournies, juste avant l’application de l’état désiré. Vous n’avez plus besoin d’exécuter la moindre commande à l’intérieur de la machine ou de regénérer un package par environnement. Un seul package IIS.zip compilé une bonne fois pour toutes suffit désormais pour tous vos serveurs !

Pour bien visualiser, voici à quoi ressemblerait le code source de cette configuration DSC générique (avant sa compilation en fichier .mof). On y installe le rôle IIS natif, et on y configure un site web basique à l’aide du module WebAdministrationDsc. Remarquez comment les valeurs utilisées sont fictives (placeholders) et n’ont pas vocation à rester telles quelles en production :

Configuration IIS {
    # Import des ressources natives et du module IIS
    Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
    Import-DscResource -ModuleName 'WebAdministrationDsc'

    Node localhost {
        # 1. Installation du rôle IIS
        WindowsFeature InstallIIS {
            Ensure = "Present"
            Name   = "Web-Server"
        }

        # 2. Création et paramétrage du site web (avec des valeurs fictives)
        WebSite MyCompanySite {
            Ensure       = "Present"
            Name         = "MySite"
            State        = "Started"
            PhysicalPath = "C:\inetpub\wwwroot\dummy" # Valeur statique destinée à être écrasée
            DependsOn    = "[WindowsFeature]InstallIIS"
        }
    }
}

Pour modifier une de ces valeurs (par exemple PhysicalPath) ou même le statut du site (State) à la volée dans vos templates ARM, il faut utiliser le paramètre configurationParameter de votre ressource  Microsoft.Compute/virtualMachines/providers/guestConfigurationAssignments. Attention, la syntaxe n’est pas très intuitive : la clé name du paramètre doit cibler précisément la ressource DSC et sa propriété selon le modèle suivant : [ClasseDeLaRessource]NomDeLaRessource;NomDeLaPropriete.

En reprenant le code DSC ci-dessus (la ressource nommée MyCompanySite de classe WebSite), pour définir le véritable chemin d’accès aux fichiers du site pour un serveur spécifique de production, la définition dans le template ARM inclura ce bloc JSON :

"properties": {
  "guestConfiguration": {
    "name": "IIS",
    "assignmentType": "ApplyAndMonitor",
    "configurationParameter":[
      {
        "name": "[WebSite]MyCompanySite;PhysicalPath",
        "value": "D:\ProductionWeb\MySite"
      }
    ]
  }
}

Attention cependant : tous les paramètres passés à Machine Configuration doivent être de type string. L’API Azure ne supporte pas le passage de tableaux (arrays) via la propriété configurationParameter et ce, même si la ressource DSC sous-jacente est techniquement capable de les interpréter (comme c’est le cas avec la propriété BindingInfo du module WebAdministrationDsc par exemple).

Si ce mécanisme vous semble familier, c’est normal : c’est en effet exactement le même qui se cache derrière les « Azure Security Baselines » déployées via Azure Policy. Lorsque vous configurez une policy Azure pour imposer des paramètres d’OS à votre parc, le service se contente d’orchestrer la création de ces assignations en injectant dynamiquement vos valeurs dans les fichiers MOF pré-packagés par Microsoft.

Le même résultat est bien sûr atteignable à l’aide de Terraform, mais cela sera l’objet du prochain article!

Powershell – Script custom d’application des MAJ Windows avec gestion du reboot

J’ai eu a creer un script un peu customisé pour gerer l’application de patch windows, pour des machines qui ne peuvent pas etre géré automatiquement. Dans mon contexte cette action devait en plus etre associé a la creation d’un snapshot de machine virtuelle, apres la mise a jour de cette machine.

Le script:

  • vérifie si des mises à jour sont réellement en attente ou si un reboot est déjà nécessaire.
  • lance le processus d’installation en arrière-plan (via un Job) pour ne pas bloquer votre session.
  • Si l’option -reboot est donnée, il redémarre la machine et attend qu’elle soit vraiment prête. Apres le reboot Il attends a la fois la disponibilité de la connexion winrm et l’occurence de l’event ID 6009 (validant que la machine viens de redemarrer) avant de valider le fait que la machine a bien redémarré.

#####################################################################################################################
#
# check_and_apply_windows_updates.ps1
# 
# Ce script:
# - Verifie sur un serveur cible si il y a des mise a jour Windows Update en attente ou un reboot en attente
# - Execute ces mise a jour 
# - Gere ou pas le reboot s'il doit avoir lieu (option -reboot) (attente que la machine cible soit joignable)
########################################################################################################################

<# Exemples

Sans reboot :
.\check_and_apply_windows_updates.ps1 -Target MyServer

Avec reboot si requis suite au patching :
.\check_and_apply_windows_updates.ps1 -Target MyServer -reboot

#>

################# Declaration des variables et fonctions ###############################################

Param(
[ValidateNotNullOrEmpty()]$Target,
[switch]$reboot,
$SecTimeOutForReboot=300,
$SecRetryIntervalReboot=10
)


$Log = "$PSScriptRoot\Launch_Maj.log"
$ScriptMaj = "$env:LOCALAPPDATA\check_and_apply_windows_updates.ps1"


$getUpdateParam = @{            
            NameSpace = 'root/ccm/ClientSDK'
            ClassName = 'CCM_SoftwareUpdate'
            Filter = 'EvaluationState < 8'
            }   

$ContScriptMaj = @'
$installUpdateParam = @{
        NameSpace = 'root/ccm/ClientSDK'
        ClassName = 'CCM_SoftwareUpdatesManager'
        MethodName = 'InstallUpdates'
    }

    $getUpdateParam = @{            
        NameSpace = 'root/ccm/ClientSDK'
        ClassName = 'CCM_SoftwareUpdate'
        Filter = 'EvaluationState < 8'
    }       

    [ciminstance[]]$updates = Get-CimInstance @getUpdateParam
    
    if ($updates) {
        Invoke-CimMethod @installUpdateParam  -Arguments @{ CCMUpdates = $updates } 
        
        while(Get-CimInstance @getUpdateParam){
            Start-Sleep -Seconds 30
        }
    }

'@

Function Add-ToLog {
Param ([string]$Text,
[string]$Couleur="Green"
 )
 #Write-Output "[LOG] $Text"
 $Text = "[$([DateTime]::Now)] - $Text"
 
 # Retry logic for file contention when multiple scripts write simultaneously
 $maxRetries = 10
 $retryCount = 0
 $retryDelayMs = 100
 
 while ($retryCount -lt $maxRetries) {
     try {
         Add-Content -Path $Log -Value $Text -ErrorAction Stop
         break
     } catch {
         $retryCount++
         if ($retryCount -lt $maxRetries) {
             Start-Sleep -Milliseconds $retryDelayMs
         } else {
             Write-Host "[LOG WARNING] Could not write to log file after $maxRetries attempts (file may be locked)" -ForegroundColor Yellow
         }
     }
 }
 
 Write-Host $Text -ForegroundColor $Couleur
}


Function Check-IfPendingUpdates($Target)
{
[ciminstance[]]$updates = Invoke-Command -ComputerName $Target -ScriptBlock {$getUpdateParam = $using:getUpdateParam;Get-CimInstance @getUpdateParam}
if ($updates)
    {
    return $updates
    }
else
    {
    return $null
    }
}


Function Check-IfRebootPending($Target)
{
$RebootPending = (Invoke-Command -ComputerName $Target -ScriptBlock {Invoke-CimMethod -Namespace root/ccm/ClientSDK -ClassName CCM_ClientUtilities -MethodName DetermineIfRebootPending}).Rebootpending
if ($RebootPending)
    {
    return $True
    }
else
    {
    return $false
    }
}



function Wait-ForWinRMAndEvent {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)] [string] $ComputerName,
        [Parameter()] [int] $TimeoutMinutes = 15,
        [Parameter()] [int] $PollSeconds = 5,
        [Parameter()] [datetime] $RebootStartTime = ((Get-Date).ToUniversalTime()),
        [Parameter()] [int] $ToleranceSeconds = 60,
        [Parameter()] [int] $global:EventToFind = 6009
    )

    $deadline = (Get-Date).AddMinutes($TimeoutMinutes)
    Write-Verbose "Waiting for $ComputerName WinRM and EventID $EventToFind (timeout $TimeoutMinutes min, poll every $PollSeconds s)"

    while ((Get-Date) -lt $deadline) {
        # 1) Test WinRM reachability
        $winrmOk = $false
        try {
            # Test-WSMan uses WinRM; returns successfully when WSMan endpoint responsive
            Test-WSMan -ComputerName $ComputerName -ErrorAction Stop | Out-Null
            $winrmOk = $true
        } catch {
            $winrmOk = $false
        }

        if ($winrmOk) {
            Write-Verbose "WinRM reachable on $ComputerName, checking EventID $EventToFind..."
            try {
                # Le ScriptBlock utilise $using:EventToFind pour transmettre la variable dans la session distante
                $script = {
                    # Récupère la dernière occurrence de l'EventID $using:EventToFind dans System
                    $evt = Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = $using:EventToFind } -MaxEvents 1 -ErrorAction SilentlyContinue
                    if ($null -eq $evt) { return $null }
                    # Retourner le TimeCreated en UTC (format ISO)
                    return $evt.TimeCreated.ToUniversalTime().ToString("o")
                }

                $lastIso = Invoke-Command -ComputerName $ComputerName -ScriptBlock $script -ErrorAction Stop

                # Invoke-Command peut renvoyer un tableau ; prendre la première valeur utile
                if ($lastIso -is [array] -and $lastIso.Length -gt 0) { $lastIso = $lastIso[0] }

                if ($lastIso) {
                    # Parse en datetime UTC
                    try {
                        $evtDt = [datetime]::Parse($lastIso).ToUniversalTime()
                    } catch {
                        Write-Verbose "Impossible de parser l'ISO renvoye: $lastIso"
                        $evtDt = $null
                    }

                    if ($evtDt) {
                        $threshold = $RebootStartTime.AddSeconds(-1 * [int]$ToleranceSeconds)
                        if ($evtDt -ge $threshold) {
                            Write-Verbose "EventID $EventToFind trouve a $evtDt (>= $threshold). Condition satisfaite."
                            return $true
                        } else {
                            Write-Verbose "EventID $EventToFind trouve mais antérieur au démarrage attendu: $evtDt < $threshold"
                        }
                    } else {
                        Write-Verbose "Aucune date d'evenement valide retournee."
                    }
                } else {
                    Write-Verbose "Aucun EventID $EventToFind trouve encore."
                }

            } catch {
                Write-Verbose "Erreur lors de la requete d'evenements sur $ComputerName : $_"
                # On continue (peut-être WinRM venait juste de s'ouvrir et l'auth a échoué)
            }
        } else {
            Write-Verbose "WinRM non joignable sur $ComputerName (probablement toujours en reboot)."
        }

        Start-Sleep -Seconds $PollSeconds
    }

    throw "Timeout: $ComputerName n'a pas repondu à WinRM ET/OU n'a pas loggue l'EventID $EventToFind dans les $TimeoutMinutes minutes."
}

################# Debut du script ###############################################

#Test d'acces au serveur cible
 
    Try
    {
    Test-WSMan -ComputerName $Target -ErrorAction Stop | Out-Null
    }
    catch
    {
    Add-Tolog "$Target est injoignable" -Couleur Red
    exit 1
    }


 
    # Message de connexion
    $message = "Connexion au serveur $Target..."
    Add-ToLog -text $message -Couleur White

    # Si il y a des updates en attente on les applique 
    $updates = Check-IfPendingUpdates -Target $Target
    If ($updates)
            {
            $message = "Il y a $(@($updates).count) mises a jour a faire sur $Target"
            Add-ToLog -Text $message -Couleur White
            $message = "Installation des mises a jour sur le serveur $Target..." 
            Add-ToLog -text $message -Couleur White
            Set-Content -Value $ContScriptMaj -Path $ScriptMaj -Force
            Invoke-Command -ComputerName $Target -FilePath "$ScriptMaj" -AsJob -JobName "$($Target)_job"

            # On attend la fin de l'execution des mise a jours
            Add-Tolog "En attente de la fin de la mise a jour de $Target, cette etape peut prendre plus d'une heure..." -couleur Yellow
            $job = Get-Job -Name "$($Target)_job"
            Wait-Job $job | Out-Null
            Remove-Job $job | Out-Null
            # Si le job est est OK - Fin de l'installation des updates
            If ($job[-1].state -like "Completed")
                {
                $message = "$Target - Fin de l'installation des mises a jour"
                Add-ToLog -Text $message -Couleur White
                }
            # Si le job est est KO - On affiche le probleme et Fin du script
            Else
                {
                $message = "$Target - Erreur pendant l'installation de une ou plusieurs mise a jour - Etat du job: $($job[-1].state) - Verifier l'Etat de la machine"
                Add-ToLog $message -Couleur Red
                Exit 1
                #$MajJobState = "Error"
                }
        
            # On verifie si les updates appliques requiert une redemarrage
            If (Check-IfRebootPending -Target $Target)
                {
                $message = "$Target : Il y a des mises a jour installees qui necessitent un redemarrage du serveur !"
                #write-host $message -F Yellow
                Add-ToLog $message -Couleur Yellow
                If ($reboot.IsPresent -eq $true)
                    {
                    $message = "Redemarrage du serveur $Target pour integrer les mises a jours...Le script attendra que le serveur $Target soit a nouveau joignable"
                    Add-ToLog $message -Couleur Yellow
                    #write-host $message -F Yellow
                    # LE RESTART-COMPUTER NE FONCTIONNE PAS!!!
                    # Restart-Computer -ComputerName $Target -Force -Wait -For PowerShell -Delay 5 -Timeout 300 -WhatIf
                    # Execution du reboot
                    Invoke-Command -ComputerName $target -ScriptBlock {shutdown.exe /f /r /c \"Reboot suite a installation de Mise a jour"}
                    # On attend que la machine ne soit plus joignable
                    Start-Sleep -Seconds 60

                # On attend que ça reboot
                $endTime = (Get-Date).AddSeconds($SecTimeOutForReboot)
                # tant que la date actuelle est inferieure a la date de fin
                while ((Get-Date) -lt $endTime)
                {
                
                try {
                    Wait-ForWinRMAndEvent -ComputerName $target -RebootStartTime $((Get-Date).ToUniversalTime()) -TimeoutMinutes 10 -PollSeconds 10 -Verbose
                    Write-Host "Machine $target de retour et EventID $EventToFind detecte."
                    # sortie immédiate de la boucle dès que la condition est satisfaite
                    break
                    } 
                catch 
                    {
                    Write-Warning $_
                    exit 1
                            }
                        }
                    }
                Else
                    {
                    $message = "Le redemarrage du serveur $Target n'a pas ete demande, reexecuter le script avec l'option -reboot pour que le serveur ne reste pas dans l'etat REBOOTPENDING"
                Add-ToLog $message -Couleur Yellow
                    #write-host $message -F Yellow
                    }

                }


       }
 



    # Si il y a un reboot en attente Meme sans MAJ ET si l'option reboot est precisee on redemarre
    ElseIf (Check-IfRebootPending -Target $Target)
        {
        $message = "$Target : Il y a des mises a jour installees qui necessitent un redemarrage du serveur !"
        #write-host $message -F Yellow
        Add-ToLog $message -Couleur Yellow
            If ($reboot.IsPresent -eq $true)
                {
                $message = "Redemarrage du serveur $Target...Le script attendra que le serveur $Target soit a nouveau joignable"
                Add-ToLog $message -Couleur Yellow
                #write-host $message -F Yellow
                # LE RESTART-COMPUTER NE FONCTIONNE PAS!!!
                # Restart-Computer -ComputerName $Target -Force -Wait -For PowerShell -Delay 5 -Timeout 300 -WhatIf
                # Execution du reboot
                Invoke-Command -ComputerName $target -ScriptBlock {shutdown.exe /f /r /c \"Reboot suite a installation de Mise a jour"}
                # On attend que la machine ne soit plus joignable
                Start-Sleep -Seconds 60

                # On attend le reboot
                $endTime = (Get-Date).AddSeconds($SecTimeOutForReboot)
                # tant que la date actuelle est inferieure a la date de fin
                while ((Get-Date) -lt $endTime)
                {
                
                try {
                    Wait-ForWinRMAndEvent -ComputerName $target -RebootStartTime $((Get-Date).ToUniversalTime()) -TimeoutMinutes 10 -PollSeconds 10 -Verbose
                    Write-Host "Machine $target de retour et EventID $EventToFind detecte."
                    # sortie immédiate de la boucle dès que la condition est satisfaite
                    break
                    } 
                catch 
                    {
                    Write-Warning $_
                    exit 1
                            }
                        }
            
            Else
                {
                $message = "Le redemarrage du serveur $Target n'a pas ete demande, reexecuter le script avec l'option -reboot pour que le serveur ne reste pas dans l'etat REBOOTPENDING"
                Add-ToLog $message -Couleur Yellow
                #write-host $message -F Yellow
                }

        }
        }

    # Si il n'y a pas d'update en attente et pas de reboot en attente
    Else {
        $message = "OK - Pas de mise a jour en attente et pas de reboot en attente"
        Add-ToLog -text $message -Couleur White
        start-sleep -Seconds 60
         Exit 0
         }