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

par | Mar 20, 2026 | PowerShell, Script | 0 commentaires

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"
 Add-Content -Path $Log -Value $Text
 Write-Host $Text -ForegroundColor $Couleur
}


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


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 
    If (Check-IfPendingUpdates -Target $Target)
            {
            $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
         }







0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *