Powershell : Robocopy Log Parser

Cela faisait quelques temps que j’avais cette idée dans mon backlog cérébral, mais je m’y suis vraiment penché aujourd’hui car il a fallu que je contrôle un gros nombre de fichiers de log générés par une instruction Robocopy (350+).

Le principal challenge pour ce script est d’arriver à gérer correctement les lignes de statistiques de Robocopy puisque l’export est au simple format texte :

------------------------------------------------------------------------------

               Total    Copied   Skipped  Mismatch    FAILED    Extras
    Dirs :      3765      3765         0         0         0         0
   Files :    243503    243503         0         0         0         0
   Bytes :  19.238 g  19.238 g         0         0         0         0
   Times :  44:22:58   6:23:45                       0:00:00   0:26:35
   Ended : Monday, February 30, 1998 3:25:57 AM

Ces lignes ne sont pas directement interprétables comme telles par Powershell, il faut donc jouer du regex pour arriver à obtenir les valeurs qui nous intéressent. Si au départ je voulais simplement ressortir les FAILED, l’effort supplémentaire pour sortir l’intégralité des valeurs pour les répertoires et les fichiers est minime, ce script va donc inclure dans le fichier CSV de sortie l’intégralité des statistiques pour les répertoires et fichiers.

function parse {
    param($inputstr)
    $newstr = $inputstr -replace "[^0-9]" , '-'
    $newstr -match '[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)'
    $stats = @($matches[1],$matches[2],$matches[3],$matches[4],$matches[5],$matches[6])
    return $stats
}

$output=@()
$logfiles = Get-ChildItem "*.log" | Select-Object Name
foreach($logfile in $logfiles) {
    Write-Host "Parsing $($logfile.Name)"
    $logcontent = Get-Content $logfile.Name
    $strf = $logcontent[$logcontent.Count-5]
    $strd = $logcontent[$logcontent.Count-6]
    $statf = parse($strf)
    $statd = parse($strd)
    $dump = New-Object PSCustomObject
    $dump | Add-Member -Name "Filename" -Value $($logfile.Name) -MemberType NoteProperty
    $dump | Add-Member -Name "Files Total" -Value $statf[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Copied" -Value $statf[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Skipped" -Value $statf[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Mismatched" -Value $statf[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Files FAILED" -Value $statf[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Extras" -Value $statf[6] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Total" -Value $statd[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Copied" -Value $statd[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Skipped" -Value $statd[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Mismatched" -Value $statd[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs FAILED" -Value $statd[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Extras" -Value $statd[6] -MemberType NoteProperty
    $output+=$dump
}
$output | Export-Csv robocopy_logs_stats.csv -Delimiter ";" -Encoding utf8

Le script récupère la liste des fichiers en .log dans le répertoire d’exécution pour ensuite les lire. Par chance, les logs Robocopy ont toujours la même structure, ce qui permet d’avoir des valeurs fixes pour séparer les colonnes et récupérer les valeurs qui nous intéressent.

Ensuite, le fichier CSV est facilement interprété par Excel et on peut voir en un clin d’oeil les éventuelles erreurs de transfert et regarder dans le log concerné quels fichiers ou répertoires sont en échec.

Une version commentée du script est disponible en téléchargement.

Robocopy KO sur partage Azure Files

Azure Files est une solution proposée par Microsoft via Azure permettant de créer des partages SMB liés à des Storage Account. Ainsi, il est possible de pousser ses répertoires partagés sans avoir de serveur de fichiers en tant que tel.

Je travaille actuellement sur un sujet portant sur le déploiement de plusieurs partages portés par Azure Files. Une fois la solution mise en place, ma tâche aura été de faire un robocopy des données locales vers le partage SMB porté par Azure Files. Cependant, en montant le partage sur le serveur de transfert avec un compte ayant les bons privilèges RBAC et NTFS, Robocopy se heurtait à un manque de permissions, y compris en mode backup :

2021/08/10 15:35:59 ERROR 5 (0x00000005) Copying NTFS Security to Destination Directory \\dundermifflin.inc\shares\iso\
Access is denied.

Voici plus ou moins à quoi ressemblait mon instruction Robocopy exécutée par Powershell.

Start-Process "robocopy.exe" -ArgumentList "$src $dst /S /E /COPY:DATSOU /PURGE /MIR /MT:4 /R:1 /W:1" 

Ce qui cause l’erreur de permissions est l’argument S de /copy, ce qui demande à Robocopy de conserver les permissions NTFS des fichiers lors du transfert.

Il faut savoir que les permissions sont habituellement découpées en 2 couches au niveau d’un partage : les privilèges SMB et NTFS. Les permissions sont indépendantes ; si sur un serveur managé il est possible d’intervenir sur les permissions SMB, ce n’est pas le cas avec Azure Files car l’accès est donné au service portant les partages, pas au serveur en lui-même. Par conséquent, les permissions sont redescendues via RBAC. Il existe des rôles Storage File Data SMB qui permettent de placer des droits en lecture et/ou en écriture sur les partages portés par Azure Files et qui positionnent ensuite au niveau SMB les permissions. Dans ce cas, mon compte ayant le rôle Storage File Data SMB Share Elevated Contributor était celui utilisé.

En fouinant un peu sur le net, je suis tombé sur cette question posée sur la section Q&A du site de Microsoft. Un utilisateur a été très récemment confronté au même problème. Cette erreur de permissions est due au fait que Robocopy a besoin de devenir propriétaire du fichier qu’il vient d’écrire à destination pour lui attribuer les permissions NTFS du fichier d’origine. Or, sur Azure Files, même avec un compte ayant les privilèges RBAC et NTFS au maximum, ce n’est pas possible. Robocopy va donc se heurter à un blocage et va renvoyer le message d’erreur plus haut.

En conséquence, pour contourner le problème qui est reconnu — et pour lequel Microsoft travaille sur une solution qui consisterait à donner à un compte un privilège RBAC permettant d’autoriser de devenir propriétaire d’un fichier — Microsoft conseille de monter le partage distant Azure Files avec un compte local d’administration qui lui, possède tous les droits.

Le partage doit donc être monté en tant que AZURE\NomDuStorageAccount ; le mot de passe est l’Access Key qui est générée et déclarée au niveau du Storage Account.

Au niveau du Storage Account, dans la section Security + networking du blade, il suffit de se rendre dans Access keys pour arriver sur cette page. La première clef est le mot de passe du compte local à utiliser. Ainsi, si le compte de stockage s’appelle storagedundermifflin, le partage doit être monté avec l’utilisateur AZURE\storagedundermifflin.

Ce compte possède tous les droits sur le partage et court-circuite les privilèges RBAC. En utilisant ce compte, Robocopy est donc capable de devenir propriétaire des fichiers copiés et d’y appliquer les permissions NTFS d’origine. En exécutant de nouveau le Robocopy, tout s’est bien déroulé et les permissions NTFS ont pu être conservées entre la source et la destination.

Faciliter sa migration de fileserver DFS avec Powershell

Ayant éprouvé ces scripts lors d’une migration d’un serveur de fichiers, je vais les partager dans ce billet. Le but n’est pas d’automatiser complètement la migration mais de faciliter le travail et de limiter les risques d’erreurs humaines.

A noter qu’il est important de vérifier le bon fonctionnement des scripts sur votre environnement : en effet, il est possible que les espaces dans les noms de chemin pour les scripts Robocopy puissent causer des dysfonctionnements. Il est également important que les noms de répertoires sur le serveur correspondent au nom des répertoires DFS. Il peut donc être nécessaire d’adapter le code pour qu’il fonctionne correctement.

Ce snippet appelle Robocopy pour faire le transfert des données.

$servername = "fs001.dundermifflin.inc"
cd c:\mig\robocopy
$FoldersToDo = Get-Item \\$ServerName\C$\shares\* | select name
foreach($Folder in $FoldersToDo){
    $FolderName = $Folder.Name
	start-process "robocopy.exe" -ArgumentList "/MIR /COPYALL /ZB /W:0 /R:0 /MT:16 \\$ServerName\C$\shares\$FolderName C:\shares\$FolderName /log:robocopy_$ServerName_$FolderName.log /v" -wait
}

Ce script va s’occuper de la partie DFS ; il va ajouter le nouveau serveur de fichier en tant que répertoire cible pour chaque répertoire DFS qui contient un lien vers l’ancien serveur. Ce nouveau lien sera en désactivé, ainsi la migration peut être préparée en amont.

Param([Parameter(Mandatory=$true)][string] $OldServer, [Parameter(Mandatory=$true)][string] $NewServer, [Parameter(Mandatory=$true)][string] $Namespace)
$fNameSpace = "$Namespace\*"
$folderlist = Get-DfsnFolder $fNameSpace | foreach { (Get-DfsnFolderTarget $_.Path).TargetPath } | Where-Object { $_ -like "\\$OldServer*" }
foreach($folder in $folderlist){
	$folderName = $folder.SubString($OldServer.Length+3,$folder.Length-$OldServer.Length-3)
    echo $folderName
	New-DfsnFolderTarget -Path "$Namespace\$folderName" -TargetPath "\\$NewServer\$folderName" -State Offline
}

Par exemple, si l’on souhaite migrer de fs001 vers fs002 tous les liens DFS du namespace Documents, le script pourra être appelé de cette manière :

.\ImportationDFS-NewServer.ps1 -OldServer fs001 -NewServer fs002 -Namespace \\dundermifflin.inc\Documents

Ainsi, le jour de la migration, en jouant ce script on réalise la bascule vers le nouveau serveur fs002 puisque celui-ci va activer les liens DFS vers le nouveau serveur en désactivant ceux de l’ancien serveur.

Param([Parameter(Mandatory=$true)][string] $OldServer, [Parameter(Mandatory=$true)][string] $NewServer, [Parameter(Mandatory=$true)][string] $Namespace)
$fNameSpace = "$Namespace\*"
$folderlist = Get-DfsnFolder $fNameSpace | foreach { (Get-DfsnFolderTarget $_.Path).TargetPath } | Where-Object { $_ -like "\\$OldServer*" }
foreach($folder in $folderlist){
	$folderName = $folder.SubString($OldServer.Length+3,$folder.Length-$OldServer.Length-3)
    echo $folderName
	Set-DfsnFolderTarget -Path "$Namespace\$folderName" -TargetPath "\\$NewServer\$folderName" -State Online
    Set-DfsnFolderTarget -Path "$Namespace\$folderName" -TargetPath "\\$OldServer\$folderName" -State Offline
}

Ce script prend les mêmes paramètres que le précédent. En fonction du type de migration effectuée, il peut-être nécessaire de jouer le script plusieurs fois : si le nouveau serveur remplace et conserve le nom de l’ancien, il y a de grandes chances qu’à un moment le nouveau serveur doive être renommé. Dans ce cas, Microsoft conseille de supprimer le répertoire de destination dans DFS avant de renommer le serveur ; cependant si un répertoire DFS n’a plus de répertoire de destination, alors ce répertoire DFS est supprimé (en conservant bien sûr, les données sur le disque du serveur en question). Il sera donc nécessaire de procéder en plusieurs étapes et en jouant le script plusieurs fois afin de toujours conserver un lien DFS, qu’il soit actif ou non.

Par exemple, si l’on considère le serveur fs001 et le serveur fs001new, en admettant que l’on renomme le serveur fs001 en fs001old une fois la migration effectuée, cela nous donne ceci :

Etape #Serveurs contenus dans les liens DFSDétails
1fs001Etat pré-migration
2fs001 : actif – fs001new : inactifAjout de fs001new
3fs001 : inactif – fs001new : actifActivation de fs001new
Désactivation de fs001
4fs001new : actifRetrait de fs001
Renommage en fs001old
5fs001new : actif – fs001old : inactifAjout de fs001old
6fs001old : inactifRetrait de fs001new
Renommage en fs001
7fs001 : actif – fs001old : inactifAjout de fs001
8fs001Etat post-migration

Il n’y a pas de dysfonctionnement grave à ne pas retirer le serveur des répertoires cibles avant qu’il soit renommé. Il faudra cependant attendre une petite trentaine de minutes que côté AD tous les liens DFS soit remis en état par le système car bien qu’il s’agisse toujours de fs001, l’objet dans l’annuaire n’est pas le même. Il ne reste ensuite plus qu’à valider le bon fonctionnement.

Migration de la définition des shares Windows

C’est ballot, mais Robocopy est incapable de faire une copie d’un répertoire en conservant son statut de répertoire partagé et les détails associés. Lorsqu’on migre un serveur de fichiers avec par exemple des homedirs, il faut donc transférer les dossiers vers le nouveau avec Robocopy, puis ensuite, appliquer les partages. Plutôt contraignant si il y a des centaines de répertoires à refaire.

Heureusement, si le serveur d’origine et de destination ont exactement la même configuration disque et les mêmes chemins, il est possible d’extraire la définition des shares et donc de l’importer sur le nouveau serveur pour que les répertoires soient de nouveau partagés.

La clef HKLM\System\CurrentControlSet\Services\LanmanServer\Shares contient ces définitions :

Ces clés contiennent le nom du partage, ainsi que d’autres informations (chemin, description)… Il est donc important que les chemins concordent ; en effet, ouvrir le fichier .reg contenant la clef et ses valeurs avec un éditeur de texte ne permettra pas une modification directe.

Un redémarrage est nécessaire après l’import de la clef sur le nouveau serveur afin que les partages soient visibles dans la console Computer Management.