Un bon système informatique ne peut pas exister sans un bon système de sauvegarde. Certains multiplient les disques sur site, d'autres font de l'externalisation, et d'autres utilisent des cassettes de sauvegarde. Et c'est à cette dernière catégorie que nous allons nous intéresser dans cet article, et plus spécialement aux utilisateurs de BACKUP EXEC 2014.

L'un de nos clients utilise des sauvegardes sur cassette, et pour avoir une durée de retour en arrière en cas d'incident suffisante, en a acheté plusieurs dizaines qui sont ensuite stockées dans un lieu sûr prévu à cet effet.

Le premier problème était que l'espace de stockage actuel (physique) ne permettait pas d'aligner les supports de sauvegarde chronologiquement, nous nous sommes retrouvés à plusieurs reprises à casser certaines possibilités de retour en arrière en insérant dans le lecteur une cassette qui n'était pas la plus ancienne.

Un autre problème venait du fait qu'en nous trompant, nous n'avions parfois pas assez de cassettes dans l'autoloader pour effectuer la sauvegarde entière.

Comment faire?

Pour éviter ceci, j'ai crée un script utilisant le module BEMCLI installé avec Backup Exec 2014. Plus qu'à utiliser Powershell et laisser la magie ouvrer!

Ce script vous permettra de :

  • Retrouver les numéros de série des cassettes les plus anciennes
  • Vous assurer que vous avez assez de médias disponibles pour effectuer vos sauvegardes pour les X prochains jours
  • De faire un inventaire automatique des nouvelles cassettes ajoutées au pool de sauvegarde
  • Ne plus vous embêter à noter sur papier les dates d'utilisation des cassettes grâce à un mail récapitulatif

Les commentaires sont dans le script, et ça n'est pas que pour faire joli ;)


 Script final

Vous pouvez télécharger mon script sur mon espace git personnel.

param(
[int]$joursprochains = 0
)
#Ce paramètre vous permettra de choisir sur combien de jours vous souhaitez analyser les sauvegardes

#Le serveur SMTP pour l'envoi du mail
$mailserver = "smtpserver.domain.local"
#L'expéditeur
$from = "moi@domain.local"
#Le / Les destinateurs
$to = @("moi@domain.local","lui@domain.local","eux@autredomaine.fr")

#On démarre un fichier de log, utile en cas de problème
Start-Transcript C:\Temp\BECheck.txt -Force

#On essaie d'importer le module BEMCLI. S'il n'existe pas, alors on arrête le script
try
{
    Import-Module BEMCLI
}
catch
{
    Write-Output "Impossible d'importer le module BEMCLI"
    throw("No BEMCLI")
    exit
}

#Un peu de texte pour alimenter le fichier de log
if ($joursprochains -ne 0)
{
    Write-Output "$((get-date).tolongtimestring()) : Vérification pour les $joursprochains prochains jours."
}

Write-Output "$((get-date).tolongtimestring()) : Récupération de l'objet de librairie"

#On essaie d'obtenir l'objet de libraire. Si c'est impossible, on coupe.
try
{
    $librairie = Get-BERoboticLibraryDevice
}
catch
{
    Write-Output "$((get-date).tolongtimestring()) : Impossible de récupérer la libraire : $($Error[0])"
    throw("Library error")
    exit
}

# On essaie ensuite de récuperer la liste des backups. Si une erreur se présente, on coupe.
try
{
    $listofjobs = Get-BEJob -JobType "Backup"
}
catch
{
    Write-Output "$((get-date).tolongtimestring()) : Impossible de récuperer les jobs : $($error[0])"
    throw("Backup error")
    exit
}

#region inventaire

#Lancement d'un inventaire. Si pas d'inventaire... et bien... la aussi, on coupe.
try
{
    $inventory = Submit-BEInventoryJob -RoboticLibraryDevice $librairie
}
catch
{
    Write-Output "$((get-date).tolongtimestring()) : Une erreur s'est produite durant le lancement de l'inventaire : $($Error[0])"
    throw("Inventory error")
    exit

}
#Attente du lancement du job d'inventaire

#L'outil BEMCLI est assez mal concu sur certains points. Il y a un décallage entre le lancement de l'inventaire et sa détection dans les jobs actifs. 
#Nous devons donc attendre que l'inventaire déclenché précedemment soit détecté et actif.

Write-Output "Attente du lancement du job`n"
do
{
    try
    {
        $jobcheck = Get-BEActiveJobDetail
    }
    catch
    {
        Write-host -NoNewline "-"
    }
        $jobfinished = Get-BEJobHistory -FromStartTime (get-date).Date | Where-Object {$_.name -eq $($inventory.name)}

    Start-Sleep -Seconds 10
    Write-Host -NoNewline "-"
}
until($jobcheck.Name -eq $inventory.Name -or $jobfinished -ne $null)
Write-host " " 

#Couleurs jolies pour patienter. Oui. Comme ça.

$i = 1
$z = 15
Write-Host $(" " * $((get-host).ui.rawui.buffersize.width)) -BackgroundColor 15
Write-Host "Attente de la fin du job d'inventaire"
Write-Host $(" " * $((get-host).ui.rawui.buffersize.width)) -BackgroundColor 15

do {

    if ($i % $((get-host).UI.RawUI.BufferSize.Width) -eq 0)
    {
        if ($z -eq 0){$z = 16}
        $z--
        Write-host " " -BackgroundColor $z
    }
    else
    {

        Write-Host -NoNewline " " -BackgroundColor $z
    }
    $i++
    Start-Sleep -Seconds 1
    $endofjob = Get-BEJobHistory -FromStartTime (get-date).Date | Where-Object {$_.name -eq $($inventory.name)}
}while ($endofjob -eq $null)

#Si le job n'est pas en succès, on coupe la et on avertit. Sinon, on continue.

if ($endofjob.JobStatus -ne "Succeeded")
{
    Write-Output "$((get-date).tolongtimestring()) : Fin de l'inventaire."
    Send-MailMessage -From $from -To $to -SmtpServer $mailserver -Subject "Erreur durant l'inventaire" -Body [string]($endofjob | ConvertTo-Html) -BodyAsHtml
    Stop-Transcript
    throw("ERREUR DURANT L'INVENTAIRE")
    Exit
}
    Write-Output "$((get-date).tolongtimestring()) : Fin de l'inventaire."

#endregion

#Récupération du pool de media géré par BE. Nous recalculons à l'aide d'une propriété dynamique la date car elle est au format texte.
#Nous en aurons besoin au format DateTime pour pouvoir faire le tri.

Write-output "$((get-date).tolongtimestring()) : Récupération des médias gérés par BE."
try
{
    $mediapool = (get-bemedia) | Select-Object Name,AllocatedDate,@{Name = "Date ajoutable";Expression = {Get-date $_.AppendableUntilDate}} | Sort-Object "date ajoutable" -Descending
}
catch
{
    Write-output "$((get-date).tolongtimestring()) : Erreur lors de la récupération du pool de médias : $($error[0])"
}

#Récupération du contenu de l'autoloader.
Write-output "$((get-date).tolongtimestring()) : Récupération du contenu de l'autoloader."
try
{
    $slots = Get-BERoboticLibrarySlot -RoboticLibraryDevice $librairie -IsCleaningSlot $false
}
catch
{
    Write-output "$((get-date).tolongtimestring()) : Erreur lors de la récupération des slots de la librairie : $($Error[0])"
}

#Récupération des jobs dont le prochain démarrage est entre aujourd'hui et le nombre de jours précisé.
$jobrunningtoday = $listofjobs | Where-Object {($_.nextstartdate.date -ge (get-date).Date -and $_.nextstartdate.date -le (get-date).AddDays($joursprochains).Date)} | Select-Object Name,Storage,nextStartDate

if ($jobrunningtoday -eq $null)
{
    Write-output "$((get-date).tolongtimestring()) : Aucune sauvegarde detectée. Fin du script"
    throw("No backup")
    exit
}

$tapedefinitions = New-Object System.Collections.ArrayList
$validtapes = New-Object System.Collections.ArrayList
$analysed = @()

# Dans la liste des jobs qui seront lancés, nous récupérons l'emplacement utilisé dans la librairie et vérifions le nombre de cassettes sur lequel nous pouvons écrire.
foreach ($job in $jobrunningtoday)
{
    if ($job.storage -notin $analysed)
    {
        $analysed += $job.storage
        $slots = Get-BERoboticLibrarySlot -RoboticLibraryPartition $job.storage
        foreach ($tape in $slots)
        {
            try {
                $writedate = $($($mediapool | Where-Object {$_.name -eq $tape.media})."date ajoutable" | Sort-Object -Property "date ajoutable" -Descending) | Select-Object -First 1 -ErrorAction Stop
            }
            catch
            {
                Write-Host "Pas de date d'écriture sur une cassette"
                $writedate = get-date "01/01/2001"
            }
            $tapedefinitions.Add([PSCUSTOMOBJECT]@{
            "Media" = $tape.Media
            "Slot" = $tape.SlotNumber
            "Storage" = $job.storage
            "Ecrasable" = $writedate
            }) | Out-Null
        }
        try
        {
            $toadd = $($tapedefinitions | Where-Object {$_.storage -eq $job.storage -and $_.ecrasable -lt $job.nextstartdate -and $_.media -and $_.media -ne $null})
            foreach ($addit in $toadd)
            {
                $validtapes.add($addit) | Out-Null
            }
        }
        catch
        {
            Write-output "$((get-date).tolongtimestring()) : Détection d'une erreur sur l'ajout de $($toadd.media) dans la liste des cassettes valides"
        }

    }

}
$groupedjobs = $jobrunningtoday | Group-Object Storage
$groupedtapes = $validtapes | Group-Object Storage
$errorarray = New-Object System.Collections.ArrayList

# En fonction de si l'on a le nombre précis de cassettes necessaires pour les sauvegardes (Ex : 2 backups a faire et 2 cassettes à disposition),
# nous affichons un message d'alerte.
# Si c'est un nombre inférieur, nous affichons une erreur
foreach ($line in $groupedjobs)
{
    $oktapes = $($groupedtapes | Where-Object {$_.name -eq $line.name})
    if ($line.count -gt $oktapes.count)
    {
        $errorarray.Add([PSCUSTOMOBJECT]@{
        "Niveau" = "DANGER"
        "Erreur" = "$($line.count) sauvegardes ($($line.Group.name)) dans les logements $($line.name), mais seulement $($oktapes.Count) disponible(s) $($oktapes.group.media)"
        })
    }
    elseif ($line.count -eq $oktapes.count)
    {
        $errorarray.Add([PSCUSTOMOBJECT]@{
        "Niveau" = "Avertissement"
        "Erreur" = "Juste assez de cassettes disponibles en écriture dans les logements $($line.name) pour faire la/les sauvegarde(s) $($line.Group.name). Si une sauvegarde demande plus d'une cassette,  une sera bloquée en attente de média!"
        })
    }

}

#Et nous envoyons le mail avec toutes les informations nécessaires
$body = ""
$subject = "Liste des cassettes"
$priority = "Normal"
if ($errorarray.Count -gt 0)
{
    $subject = "!!DANGER SUR LES PROCHAINES SAUVEGARDES!!"
    $priority = "High"
    $body += $errorarray | ConvertTo-Html
    $body += "
"*5
}
    $choosein = $mediapool | Where-object {$_.name -notin $tapedefinitions.media} | Sort-Object "Date ajoutable"

    $body += "Liste des cartouches triées par date d'écrasement 
"
    $body += $choosein | Select-Object Name,"Date ajoutable" | ConvertTo-Html
    Send-MailMessage -SmtpServer $mailserver -From $from -To $to -Body $body -BodyAsHtml -Subject $subject -Encoding UTF8 -Priority $priority

Stop-Transcript

bemcli

Une fois ce script placé en tâche planifiée, vous recevrez un mail avec les cassettes identifiées et triées par ordre de date d'écrasement. Plus qu'a retrouver les plus anciennes, et le tour est joué!

Une cassette portant une date du 01/01/1900 signifie qu'elle n'a jamais été écrite, et n'a pas de date d'écrasement.

Ajouter un commentaire

Article précédent Article suivant