Mise à jour et restart des serveurs avec WSUS

Intro


Aujourd'hui il est primordial de mettre à jour ces serveurs Windows, si vous ne souhaitez pas avoir le même problème que certaines sociétés

Source :
https://www.lemagit.fr/etude/Recit-comment-Manutan-sest-sorti-de-la-cyberattaque-du-21-fevrier
que j'ai vu par le site zwinder
https://blog.zwindler.fr/2022/07/30/comment-veulent-ils-que-nous-mettions-a-jour/

Pour cela, j'ai créé deux scripts (du moins 3) qui vont permettre de mettre à jour vos serveurs et de choisir quel jour les redémarrer, mais aussi d'avertir les personnes qui utilisent ces serveurs

Besoin

Nous allons nous baser sur les besoins suivants

  • Téléchargement et Installation automatique des mise à jours
  • Restart un jour sur une plage souhaitée

Wsus est un bon produit, mais pas assez personnalisable pour les cas cités précédemment.
Pour le choix numéro 4 - Téléchargement automatique et planification des installations, le redémarrage sera forcement pendant la plage de maintenance.
Sauf dans les cas suivants (si les options sont activées)

  • Ne pas restart si un utilisateur est connecté
  • Ne pas restart sur une plage prédéfinie (ne peut dépasser 18h d'affilée)

Donc si vous activez la planification (option 4) qu'aucun utilisateur n'est connecté et que vous êtes hors de votre plage, votre serveur va redémarrer
C'est pas rassurant sur de la production

Pour améliorer cela, j'ai activé l'option 2 - Notification des téléchargements et des installations et j'ai créé des scripts permettant

  • Le téléchargement et l'installation des mises à jour (sans restart les serveurs, même si la mise à jour le requière)
  • Planification du restart entre 20h et 23h30 (vous pouvez le modifier)
  • Mise en pause des sondes PRTG des serveurs sur une plage de 5Mn (au moment du redémarrage)

Cela corrige donc les problèmes cités ci-dessus

Voici les scripts à adapter selon vos besoins, ils peuvent être améliorés car je ne suis pas expert en powershell

  • j'utilise des scripts déjà existants (API PRTG) et le téléchargement et l'installation des mises à jour Windows update (PSWindowsUpdate)

fonctionnement et scripts

Les scripts sont utilisables de différentes façons

  • En les lançant avec un argument (les OU impactées)
  • En les exécutant sans argument et en suivant les indications

Cela dans le but de permettre une gestion manuelle ou automatique des scripts

Si vous souhaitez automatiser :

  • Il suffira de créer une tâche planifiée sur un serveur avec un utilisateur domain admin (c'est moche), soit crée un GMSA en donnant les droits adéquats, beaucoup plus long mais plus propre
  • Dans la tâche planifiée, il faudra mettre le script de la facon suivante : LECHEMINDUSCRIPT PREPROD,INTE,TEST

    (PREPROD,INTE,TEST) équivaut au OU sélectionné
    Attention vous devez adapter le fichier OuCharger.ps1 pour indiquer les bons chemins

En manuel :

  • Il suffit de lancer le script et de suivre les étapes

Conseil :

  • Concernant la méthode de mise à jour est restart, vous pouvez sectionner vos serveurs dans deux ou trois ... OU prod pour éviter d'appliquer les MAJ et le restart en même temps, surtout sur les clusters de serveurs (HA)
  • Pour les mises à jours, il faudra faire une tâche planifiée le jeudi (car microsoft descend les mises à jour le mercredi),
  • Le script du redémarrage serait à planifier de préférence le lundi (pour vous laissez du mardi au vendredi afin de corriger les éventuels problèmes liés aux mise à jour)

    le script de mise à jour envoie un mail avec les KB installées, ce qui vous permettra de débeugger plus rapidement,
    Attention certaines mises à jour pourrait ne pas s'installer car elle serait bloquée par l'une des mises à jour en attente d'un restart

Le script peut être amélioré pour attendre plusieurs heures et remonter les informations des fichiers de logs présent sur les serveurs mise à jour.

Script

Script de téléchargement et installation Windows update

  • Le script de téléchargement se connecte aux serveurs
  • Vérifie les mises à jour disponibles, les télécharges et les installe
  • Envoi un mail avec les mises à jour installées ainsi que les serveurs en erreurs et les détails des erreurs

Ligne à modifier :
OU : 49 / 52 / 63 / 66
Modifier les champs des email expediteurs et destinaires mais aussi le relaiSMTP (IP ou DNS) : 215

# Je fais appel à une fonction commune à plusieurs scripts
. ".\OuCharger.ps1"

# Je vide toutes les variables au cas ou elle sont deja utilisées, je met en silencieux les erreurs, car il risque d'indiquer que les varibles n'existe pas
clear-variable -Name "ServeurListe"-ErrorAction SilentlyContinue
clear-variable -Name "ListSRVError" -ErrorAction SilentlyContinue
clear-variable -Name "LogSrvRestart" -ErrorAction SilentlyContinue
clear-variable -Name "LogSrvInstall" -ErrorAction SilentlyContinue

# s'il a un argument, alors je crée la variable manuellement pour la tâche plannifier, le fonctionnement se fera par OU
if ($args[0] -ne $null)
{
    $readstart = "ou"
    $listConSrv = $args[0]
    $listConSrv = $listConSrv.Split(',')
}

#Je met le script en mode debug
$debug = 1

#J'initialise la variable dédiée aux erreurs
$ListSRVError = "Voici la liste des erreurs rencontrées, veuillez vérifier sur les serveurs si cela pourrait être corrigés `n"
$ListSRVError += "`n---------------------------------------------------------------------------------`n`n"

# Vérifie s'il a un argument
if ($args[0] -eq $null)
{
    $readstart = Read-Host "OU ou Poste (ou|poste)"
    if ($readstart -notmatch "ou|poste")
    {
        write-host "Veuillez indiquer OU ou POSTE lors de la demande, je quitte"
        exit
    }

    if ($readstart -eq "poste") 
    {
        $ServeurListe = @()
        Do{
            $StrSrvif = Read-Host "Veuillez indiquer le nom du serveur, si vous souhaitez vous arretez la appuyer sur entrée"
            if ($StrSrvif -ne "")
            {
                $ServeurListe += [pscustomobject]@{Name=$StrSrvif}
            }
        }Until($StrSrvif -eq "")
    }

    if ($readstart -eq "ou") 
    {
        Write-Host "OU dispo `n INTE PREPOD PROD TESTING"
        $listConSrv = Read-Host "Veuillez indiquer l'OU, si vous en indiquez plusieurs, veuillez mettre des **","** entre chaque OU (sans espace)"
        $listConSrv = $listConSrv.Split(',')
        if ($listConSrv -notmatch '^INTE$|^PREPOD$|^PROD$|^TESTING$')
        {
            write-host "Erreur, aucun élément correspondant pour le choix de l'OU (variable listConSrv) en debut de script, Veuillez entrer les bonnes valeurs. Je quitte"
            Exit
        }
    }
}
else
{
    if ($readstart -eq "ou") 
    {
        if ($listConSrv -notmatch '^INTE$|^PREPOD$|^PROD$|^TESTING$')
        {
            write-host "Erreur, L'argument donné lors de l'execution du script ne correspond pas au OU existante."
            write-host "Veuillez indiquer une ou plusieurs OU (séparées par une virgule) parmis le choix suivant : INTE PREPROD PROD TESTING"
            Exit
        }
    }
}

# fonction qui permet de concaténer la variable dédiée aux erreurs avec le nom de la machine et l'erreur rencontrée
function ErrReturn ($FuncErr, $FuncErrserveur)
{
    $FuncErrorLog +=  "Serveur : $FuncErrserveur`n"
    $FuncErrorLog +=  "`n---------------------------------------------------------------------------------`n"
    $FuncErrorLog +=  $FuncErr
    $FuncErrorLog +=  "`n---------------------------------------------------------------------------------`n`n"
    return $FuncErrorLog
}

# Je vide la variable dédiée aux erreurs pour ne pas avoir les erreurs précédentes
$Error.Clear()

# Je verifie si le module que je souhaite utiliser est bien installé
$ModInstallSrvRoot = Get-Module -ListAvailable -Name PSWindowsUpdate
#Si cela ne retourne pas d'erreur et que la variable est vide alors j'install le module
# Si la variable $ModInstallSrvRoot retourne une erreur alors je l'ajoute dans la variable des erreurs
if  (($Error.count -eq 0) -and ($ModInstallSrvRoot -eq $null))
{ 
    #j'installe les paquet requis et charge le module
    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
    Install-Module PSWindowsUpdate -Confirm:$false -Force
    Import-module PSWindowsUpdate
}
elseif ($Error.count -ne 0)
{
    $RootSrvUse = $env:COMPUTERNAME
    $ListSRVError += ErrReturn $ErrorFuncSend $RootSrvUse
}

# Suivant les choix effectués, je crée une liste de serveur. J'exclu le nom des OU de la liste pour que ca ne soit pas pris pour un nom de serveur
if ($readstart -eq "ou")
{
    # j'utilise la fonction commune à plusieurs script (déclaré en debut de script)
    $ServeurListe = OuCharger $listConSrv, $null
}

# J'utilise la liste des serveurs pour faire li'nstallation du module, vérifier les mises à jour et les installer sans restart le serveur
# Le restart des serveurs se  fera avec le script PRTG
foreach ($serv in $ServeurListe)
{
    if (Test-Connection -ComputerName $serv.Name -Quiet)
    {
        if ($debug -eq 1)
        {
            $serv.name
        }

        $Error.Clear()

        $InvokeTest = Invoke-Command -ComputerName $serv.Name -ScriptBlock {Get-Module -ListAvailable -Name PSWindowsUpdate}
        if  (($Error.count -eq 0) -and ($InvokeTest -eq $null))
        { 
            $Error.Clear()
            Invoke-Command -ComputerName $serv.Name -ScriptBlock {Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force}
            if (Select-String -InputObject $Error[0].FullyQualifiedErrorId -Pattern 'NoMatchFoundForProvider' -Quiet) 
            {
                Invoke-Command -ComputerName $serv.Name -ScriptBlock {Set-ItemProperty -Path "HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319" -Name "SchUseStrongCrypto" -Value "1" -Type DWord}
                Invoke-Command -ComputerName $serv.Name -ScriptBlock {Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319" -Name "SchUseStrongCrypto" -Value "1" -Type DWord}
                Invoke-Command -ComputerName $serv.Name -ScriptBlock {Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force}
                $Error.Clear()
            }
            Invoke-Command -ComputerName $serv.Name -ScriptBlock {Install-Module PSWindowsUpdate -Confirm:$false -Force}
            Invoke-Command -ComputerName $serv.Name -ScriptBlock {Import-module PSWindowsUpdate}
            sleep 5
            $VerifModuleInstall = Invoke-Command -ComputerName $serv.Name -ScriptBlock {Get-Module -ListAvailable -Name PSWindowsUpdate}
            if ($VerifModuleInstall -eq $null)
            {
                $ErrorFuncSend = $Error[0].Exception.message 
                $ListSRVError += ErrReturn $ErrorFuncSend $serv.Name
                Continue
            }
        }
        elseif ($Error.count -ne 0)
        {
            $ErrorFuncSend = $Error[0].Exception.message 
            $ListSRVError += ErrReturn $ErrorFuncSend $serv.Name
            Continue
        }

        $Error.Clear()
        # Je verifie la liste des mises à jour sur le serveur interrogé, dont les mise à jour exigent le redémarrage
        $LstUpdateReq = Get-WindowsUpdate -computername $serv.name

        # Si la variable $LstUpdateReq retourne une erreur, je la note et passe au serveur suivant
        if ($Error.count -ne 0)
        {
            $ErrorFuncSend = $Error[0].Exception.message 
            $ListSRVError += ErrReturn $ErrorFuncSend $serv.Name
            Continue
        }

        # On vérifie les MAJ en ignorant celle  qui ont besoin d'un redémarrage
        $LstUpdateNotReq = Get-WindowsUpdate -computername $serv.name -IgnoreRebootRequired
        if ($LstUpdateReq.count -ne 0)
        {
            # Je verifie que les mise à jour qui ont besoin d'un redémarrage ne sont pas les même que celle n'ayant pas besoin de redémarrage
            if ($LstUpdateReq.count -ne $LstUpdateNotReq.count) {
                $LogSrv += "Serveur : " + $serv.Name
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += "############## Mises à jours ayant un besoin de redémarrage du serveur ##############"
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += $LstUpdateReq | Where-Object {$_.KB -notin $LstUpdateNotReq.KB } | Out-String
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += "########################### Installation mise(s) à jour ###########################" 
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += Get-WindowsUpdate -computername $serv.name -Install -AcceptAll -Verbose -IgnoreReboot | Out-string
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
            }
            else 
            {
                $LogSrv += "Serveur : " + $serv.Name
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += "########################### Installation mise(s) à jour ###########################" 
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
                $LogSrv += Get-WindowsUpdate -computername $serv.name -Install -AcceptAll -Verbose -IgnoreReboot | Out-string
                $LogSrv += "`n---------------------------------------------------------------------------------`n"
            }
        }
    }
    else
    { 
        $ErrorFuncSend = "La machine ne repond pas au ping"
        $ListSRVError += ErrReturn $ErrorFuncSend $serv.Name
        Continue
    }
}

# Si l'une des listes n'est pas vide, elle est convertit en caractère. On envoi le mail
if (($LogSrv -ne "") -or ($ListSRVError -ne ""))  {

    $LogSrv | out-string 
    $ListSRVError | out-string 

    if ($debug -eq 1)
    {
        Write-Host "------------------------------------------- Liste des mise à jours `n $LogSrv `n ------------------------------------------- Serveurs en erreurs `n $ListSRVError `n"   
    }
    else
    {
        #Encodage UTF8
        $encodingMail = [System.Text.Encoding]::UTF8 
        Send-MailMessage -From "EXPEDITEUR@TONDOM.FR" -To "DESTINATAIRE@TONDOM.FR" -Subject "Rapport des serveurs à restart" -SmtpServer "RELAISMTP(IP ou DNS)" -Body "------------------------------------------- Liste des mise à jour `n $LogSrv `n ------------------------------------------- Serveurs en erreurs `n $ListSRVError `n" -Encoding $encodingMail

    }

}

Script PRTG et restart des serveurs

Ligne à modifier :
Chemin du script commun au deux scripts (Mise à jours et restart) : 10
OU : 49 / 57 / 68 / 71
Identifiant et mot de passe PRTG : 108 - 110
Chemin de la liste d'exclusion des serveurs (pour éviter de les restart car trop sensible) : 156
Modifier les champs des email expediteurs et destinaires mais aussi le relaiSMTP (IP ou DNS) : 339

  • Script permettant de restart les serveurs au besoin
  • Il verifie les serveurs ayant un besoin de restart via un module powershell qui fait la vérification dans le registre
  • Si le serveur a besoin de restart alors il crée une tâche planifiée, la plage horaire s'incrémente de 19h à 23h30 par saut de 5Mn par serveur, les serveurs à restart après 23h30 sont indiqués comme non pris en charge dans le rapport. Je n'ai pas fait de liste de report car ça aurait pris du temps pour pas grand chose de mon point de vue
  • Il met en pause la sonde PRTG sur la même plage que le restart et pendant 5mn (pour éviter de déranger l'astreinte)
  • Concernant le HASH, voici le liens PRTG : HASH password PRTG
  • Il envoi un rapport par mail des serveurs qui ont un besoin de restart. Ceux qui n'ont pas de besoin de restart. Ceux en erreur avec le descriptif de l'erreur
# Je fais appel a une fonction commune aux deux script
. “.\OuCharger.ps1”

#Le mode debug sert à avoir plus d'information, il remplace l'envoi de mail par un affichage dans la console
$debug = 0

#On vide la variable
clear-variable -Name "ServeurListe" -ErrorAction SilentlyContinue

# s'il a un argument, alors je crée le variable manuellement pour la tâche plannifier, le fonctionnement se fera par OU
# l'argument en attente et une ou plusieurs OU séparé par des ,
if ($args[0] -ne $null)
{
    $readstart = "ou"
    $listConSrv = $args[0]
    $listConSrv = $listConSrv.Split(',')
}

if (($debug -ne 1) -and ($debug -ne 0))
{
    Write-Host "Veuillez mettre la variable debug à 1 ou 0, je quitte"
    exit
}

if ($args[0] -eq $null)
{
    $readstart = Read-Host "OU ou Poste (ou|poste)"

    if ($readstart -notmatch "ou|poste")
    {
        write-host "Veuillez indiquer OU ou POSTE lors de la demande, je quitte"
        exit
    }

    if ($readstart -eq "poste") 
    {
        $ServeurListe = @()
        Do{
            $StrSrvif = Read-Host "Veuillez indiquer le nom du serveur, si vous souhaitez vous arreter la, appuyer sur entrée"
            if ($StrSrvif -ne "")
            {
                $ServeurListe += [pscustomobject]@{Name=$StrSrvif}
            }
        }Until($StrSrvif -eq "")
    }

    if ($readstart -eq "ou") 
    {
        Write-Host "OU dispo `n INTE/PREPROD/PROD/TESTING"
        $listConSrv = Read-Host "Veuillez indiquer l'OU, si vous en indiquez plusieurs, veuillez mettre des , sans espace"
        $listConSrv = $listConSrv.Split(',')
        $listConSrv
    }

    if ($readstart -eq "ou") 
    {
        if ($listConSrv -notmatch '^INTE$|^PREPROD$|^PROD$|^TESTING$')
        {
            write-host "Erreur, aucun élément correspondant pour le choix de l'OU (variable listConSrv en debut de script), Veuillez entrer les bonnes valeurs. Je quitte"
            Exit
        }
    }
}
else
{
    if ($readstart -eq "ou") 
    {
        if ($listConSrv -notmatch '^INTE$|^PREPROD$|^PROD$|^TESTING$')
        {
            write-host "Erreur, L'argument donné lors de l'execution du script ne correspond pas aux OU existante."
            write-host "Veuillez indiquer une ou plusieurs OU séparé par une virgule (si plusieurs) parmis le choix suivant : INTE PREPROD PROD TESTING"
            Exit
        }
    }
}

# Cette fonction permet de créer la tâche plannifier, attention les vieux serveur n'on pas cette fonction powershell
function Task ($SRVName, $TaskTime)
{
    $TaskExec= New-ScheduledTaskAction -Execute "shutdown.exe" -Argument "/r /t 0 /f"
    $TaskRun = New-ScheduledTaskTrigger -Once -At $TaskTime"pm"
    $TaskUser = "NT AUTHORITY\SYSTEM"
    Invoke-Command -ComputerName $SRVName -ScriptBlock {Register-ScheduledTask -TaskName $Using:NameTask -Trigger $Using:TaskRun -Action $Using:TaskExec -User $Using:TaskUser -RunLevel Highest –Force -Description "Restart le serveur, car en attente de redemarrage"}
}

# Fonction qui permet d'installer des modules, s'il sont manquant
function Module ($Module)
{
    if (-Not (Get-Module -ListAvailable -Name $Module)) 
    {
        Install-Module $Module -Confirm:$false -Force
    }
}

# Installation des modules dont on va avoir besoin
Module "PrtgAPI"
Module "PendingReboot"

# Fonction permettant de mettre en pause les sondes PRTG des serveurs aux heures prévues
function PRTGSched ($Srv, $h, $m, $PRTGopt)
{   
    $ReturnPRTGLog = $null 
    $PRTGParams = $null
    $PRTGId = $null

    # Identifiant de connexion PRTG
    $PRTGLogin = "USERPRTG"
    $PRTGHash = "HASHPASSWORD"
    $PRTGServeur = "https://URLSUPERVISION.LAN"

    # Connexion au serveur PRTG
    Connect-PrtgServer -Server $PRTGServeur (New-Credential $PRTGLogin $PRTGHash) -PassHash

    #Recupération de l'ID de la machine sur PRTG
    $PRTGId = Get-Device $Srv

    if ($PRTGId -ne $null)
    {
        # Suivant la valeur de $PRTGopt, on alimente les variables avec ce que l'on a besoin 
        switch ( $PRTGopt )
        {
            # Cette partie crée la planification, pour les serveurs 
            Schedule 
            {
                $PRTGStart = (get-date -Hour $h -Minute $m -Second 0)
                $PRTGEnd = $PRTGStart.AddMinutes(5)
                $PRTGEnd = $PRTGEnd.ToString("yyyy-MM-dd-HH-mm-ss")
                $PRTGStart = $PRTGStart.ToString("yyyy-MM-dd-HH-mm-ss")

                #api https://www.paessler.com/manuals/prtg/application_programming_interface_api_definition
                $PRTGParams = @{
                    "scheduledependency" = 0
                    "maintenable_" = 1
                    "maintstart_" = $PRTGStart
                    "maintend_" = $PRTGEnd
                }

            }
        }

        # l'appel est faite à PRTG avec les éléments dont il a besoin
        Get-Device -Id $PRTGId.id | Set-ObjectProperty -RawParameters $PRTGParams -Force
        # Je me déconnecte de PRTG
        Disconnect-PrtgServer
    }
    else {
        $ReturnPRTGLog = "True"
        # Je me déconnecte de PRTG
        Disconnect-PrtgServer
        return $ReturnPRTGLog
    }
}

#On recupère la liste des serveurs que l'on va exclure des tâches à effectuer (une ligne par serveur dans le fichier)
$ExcludeSrv = Get-Content ".\RestartSRVTachePlannifierPRTG-ListExcludeSrv.txt"

if ($readstart -eq "ou")
{
    # j'utilise la fonction commune aux deux scripts (déclarées en debut de script)
    $ServeurListe = OuCharger $listConSrv $ExcludeSrv
}

# On défini les variables de temps et du nom de la tâche qui sera crée
$Minute=55
#heure et hour sont crée car les serveurs sont en horaires anglaise alors que PRTG en francais
$Hour=7
$Heure=19
$GardeFou=0
$NameTask = "RebootSRV001"
$ListSRV = ""
$ListSRVError = ""
$ListSRVNotReboot = ""

# Gestion serveur par serveur
foreach ($serv in $ServeurListe)
{
    if (Test-Connection -ComputerName $serv.Name -Quiet)
    {
        # On verifie si le serveur est en attente de redémarrage est on met les warning dans la variable $WarningPending
        switch ((Test-PendingReboot -ComputerName $serv.Name -SkipConfigurationManagerClientCheck -WarningVariable WarningPending).IsRebootPending)
        {
            True
            {

                if ($debug -eq 1)
                {
                    Write-Host "--------------------------------------------------------------------------------------------"
                    Write-Host "Vous etes dans le switch True (en attente de redémarrage)"
                    Write-Host "Nom du serveur : " $serv.Name
                    Write-Host "debug : " $debug
                    Write-Host "warning : " $WarningPending
                    Write-Host "Erreur : " $Error[0].Exception.message
                    Write-Host "--------------------------------------------------------------------------------------------"
                }
                # On verifie si le garde-fou est activé. Le garde-fou sert à éviter de dépasser la plage de redémarrage 20h > 23h30
                if ($GardeFou -ne 1) 
                {

                    #On reinitialise la variable $Error
                    $Error.Clear()

                    # On verifie si la tache plannifier existe
                    $ReturnTask = Invoke-Command -ComputerName $serv.Name -ScriptBlock {Get-ScheduledTask | where-object {$_.TaskName -like $Using:NameTask}}

                    #si $ReturnTask retourne une erreur alors on quitte la boucle et on met le serveur en erreur en indiquant pourquoi
                    if ($Error.count -ne 0)
                    {
                        $ListSRVError +=  "Serveur : " + $serv.Name + "`n"
                        $ListSRVError +=  "`n-------------------------- Voici le message d'erreur ---------------------------------------`n"
                        $ListSRVError +=  $Error[0].Exception.message
                        $ListSRVError +=  "`n-------------------------- Fin du message d'erreur -----------------------------------------`n`n"
                        Continue
                    }

                    # le serveur doit être restart, j'implémente le nombre de minute et converti en heure si besoin
                    $Minute += 5
                    if (($Minute -eq 30) -and ($Hour -eq 11)) 
                    {
                        $GardeFou=1
                        $ListSRV += "############### RESTANT A RESTART PLUS TARD (HORS DELAI)`n"
                    }

                    if ($Minute -eq 60) 
                    {
                        $Minute = 0
                        $Hour += 1
                        $Heure += 1
                    }

                    # Si la variable n'est pas null 
                    if ($ReturnTask -ne $null)
                    {
                        # Alors on supprime la tâche, $using sert à utiliser des variables qui sont en local (dans le script)
                        Invoke-Command -ComputerName $serv.Name -ScriptBlock {Unregister-ScheduledTask $Using:NameTask -Confirm:$false}
                        # Lancement de la fonction avec l'heure défini actuellement
                        Task $serv.Name $Hour":"$Minute
                        # La sonde PRTG est mise en pause à l'heure prévu
                        $ReturnPRTGLog = PRTGSched $serv.Name $Heure $Minute "Schedule"
                    }
                    else
                    {
                        # Si aucune tâche n'est présente alors on lance la fonction avec l'heure défini actuellement
                        Task $serv.Name $Hour":"$Minute
                        # Je met la sonde PRTG en pause à l'heure prévu
                        $ReturnPRTGLog = PRTGSched $serv.Name $Heure $Minute "Schedule"
                    }

                    if ($ReturnPRTGLog -ne $null)
                    {
                       # Incrémentation de la liste des serveurs ayant besoin d'être redémarré
                        $ListSRV += $serv.Name + " restart à  " + $Heure + ":" + $Minute + "`n"     
                    }
                    else 
                    {
                        $ListSRV += $serv.Name + " restart à  " + $Heure + ":" + $Minute + " Pas de sonde PRTG trouvé, Suspension de la sonde non definie `n"
                    }
                }
                else 
                {
                    # Si le garde-fou est activé, alors on note les serveurs en attente de redémarrage. On ne plannifie pas de tâche car on n'a plus de temps disponible
                    $ListSRV += $serv.Name + " restart à  " + $Heure + ":" + $Minute + "`n"
                }
            }
            False
            {
                # On liste les serveurs qui n'ont pas besoin d'être redémarrer
                $ListSRVNotReboot +=  $serv.Name + "`n"
                if ($debug -eq 1)
                {
                    Write-Host "--------------------------------------------------------------------------------------------"
                    Write-Host "Vous etes dans le switch False (Pas besoin de restart)"
                    Write-Host "Nom du serveur : " $serv.Name
                    Write-Host "debug : " $debug
                    Write-Host "warning : " $WarningPending
                    Write-Host "--------------------------------------------------------------------------------------------"
                }

            }

            default
            {
                #Si le warning du switch Test-PendingReboot est egal a l'erreur RPC je l'indique
                if (Select-String -InputObject $WarningPending -Pattern '0x800706BA' -Quiet) 
                {
                    $ListSRVError +=  "Serveur : " + $serv.Name + "`n"
                    $ListSRVError +=  "`n-------------------------- Voici le message d'erreur ---------------------------------------`n"
                    $ListSRVError +=  "N'arrive pas à se connecter à la machine (RPC) `n"
                    $ListSRVError +=  "`n-------------------------- Fin du message d'erreur -----------------------------------------`n`n"                                                    
                }
                else 
                {
                    # On note toutes les autres erreurs et indique les serveurs comme étant en erreurs
                    $ListSRVError +=  "Serveur : " + $serv.Name + "`n"
                    $ListSRVError +=  "`n-------------------------- Voici le message d'erreur ---------------------------------------`n"
                    $ListSRVError +=  "$WarningPending `n"
                    $ListSRVError +=  "`n-------------------------- Fin du message d'erreur -----------------------------------------`n`n"

                }
                #Sert de test pour savoir ou resort le warning
                if ($debug -eq 1)
                {
                    Write-Host "--------------------------------------------------------------------------------------------"
                    Write-Host "Vous etes dans le switch default (autre cas que redémarrage ou pas de redémarrage)"
                    Write-Host "Nom du serveur : " $serv.Name
                    Write-Host "debug : " $debug
                    Write-Host "warning : " $WarningPending
                    Write-Host "error : " $Error[0].FullyQualifiedErrorId
                    Write-Host "--------------------------------------------------------------------------------------------"
                }

            }
        }
    }
    else
    {
        $ListSRVError +=  "Serveur : " + $serv.Name + "`n"
        $ListSRVError +=  "`n-------------------------- Voici le message d'erreur ---------------------------------------`n"
        $ListSRVError +=  "N'arrive pas à se connecter à la machine (PING)  `n"
        $ListSRVError +=  "`n-------------------------- Fin du message d'erreur -----------------------------------------`n`n"
    }
}

# Si l'une des listes n'est pas vide, elle est convertit en caractère. On envoi le mail
if (($ListSRV -ne "") -or ($ListSRVError -ne "") -or ($ListSRVNotReboot -ne ""))  {

    $ListSRV | out-string 
    $ListSRVError | out-string 
    $ListSRVNotReboot | out-string

    if ($debug -eq 1)
    {
        Write-Host "------------------------------------------- Serveurs qui n'ont pas besoin de redémarrage `n $ListSRVNotReboot `n ------------------------------------------- Serveurs en erreurs `n $ListSRVError `n ------------------------------------------- Serveurs qui vont redémarrer ce soir `n $ListSRV "   
    }
    else
    {
        #Encodage UTF8
        $encodingMail = [System.Text.Encoding]::UTF8 
        Send-MailMessage -From "EMAILEXPEDITEUR@TONDOM.FR" -To "DESTINATAIRE1@TONDOM.FR", "DESTINATAIRE2@TONDOM.FR" -Subject "Rapport des serveurs à restart" -SmtpServer "SERVEURSMTPRELAI" -Body "------------------------------------------- Serveurs qui n'ont pas besoin de redémarrage `n $ListSRVNotReboot `n ------------------------------------------- Serveurs en erreurs `n $ListSRVError `n ------------------------------------------- Serveurs qui vont redémarrer ce soir `n $ListSRV " -Encoding $encodingMail
    }

}

Script Commun .\OuCharger.ps1

Modifier les chemins des OUs de vos serveurs en fonction de votre AD (option -SearchBase)

#Fonction commune à plusieurs scripts

function OuCharger ($listOUSrv, $ExcludeSrvList)
{
    foreach ( $i in $listOUSrv )
    {
        switch ( $i )
        {
            'INTE'
            {
                $ServeurListe += @(Get-ADObject -LDAPFilter "(objectClass=Computer)" -SearchBase 'OU=INTE,OU=Serveur,DC=TONDOM,DC=lan' | Where-Object {$_.Name -notin $ExcludeSrvList} | Select-Object Name)
            }
            'PREPROD'
            {
                $ServeurListe += @(Get-ADObject -LDAPFilter "(objectClass=Computer)" -SearchBase 'OU=PREPROD,OU=Serveur,DC=TONDOM,DC=lan' | Where-Object {$_.Name -notin $ExcludeSrvList} | Select-Object Name)
            }
            'PROD'
            {
                $ServeurListe += @(Get-ADObject -LDAPFilter "(objectClass=Computer)" -SearchBase 'OU=PROD,OU=Serveur,DC=TONDOM,DC=lan' | Where-Object {$_.Name -notin $ExcludeSrvList} | Select-Object Name)
            }
            'TESTING'
            {
                $ServeurListe += @(Get-ADObject -LDAPFilter "(objectClass=Computer)" -SearchBase 'OU=TESTING,OU=Serveur,DC=TONDOM,DC=lan' | Where-Object {$_.Name -notin $ExcludeSrvList} | Select-Object Name)
            }
            default
            {
                Write-Information 'Veuillez remplir la variable$listOUSrv correctement'
                Exit
            }

        }

    }
    return $ServeurListe
}

 Récapitulatif


Attention faite des test en INTEGRATION avant, ne faite pas vos test directement en PROD

  1. Vous récupérer les trois script
  2. Vous les modifier en conséquence, suivant votre infra (suivre mes indications sur les lignes à modifier. Servez-vous de notepad++ ou VScode)
  3. Vous créez deux tâche plannifier avec les éléments suivants :
    • Indiquer un utilisateur GMSA (si vous avez tout configurer pour, ou sinon un compte admin du domaine (c'est moche))
    • Mettez les droits maximum sur la tâche plannifiée
    • Retenir les identifiants et mot de passe, car la tâche doit se lancer même si le compte n'est pas connecté
    • Indiquez une heure et un jour en respectant ce que j'ai indiqué précédemment (de préférence)
    • Mettre la commande suivante en l'adaptant à vos OU (LECHEMINDUSCRIPT.PS1 PREPROD,INTE,TEST )
  4. Buvez un bon café en pensant au temps que vous venez de gagner :)