diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2efe4c3a..073b525b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,9 @@ jobs: install: mingw-w64-x86_64-gcc autoconf automake make - run: autoreconf -i - run: ./configure --enable-warning-as-error - - run: make all dist check + - run: make all + - run: make snapcheck_ntfs + - run: make dist check build-freebsd: runs-on: ubuntu-latest steps: diff --git a/Makefile.am b/Makefile.am index d36ced64..2e86bde9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -178,6 +178,7 @@ EXTRA_DIST = \ $(LIST_MAN) \ $(LIST_TXT) \ test/test-snap.conf \ + test/snapcheck-ntfs.ps1 \ test/test-par1.conf \ test/test-par2.conf \ test/test-par3.conf \ @@ -340,6 +341,7 @@ REALLOC_SPLIT2 = $(srcdir)/test/test-realloc-split2.conf SNAP = $(srcdir)/test/test-snap.conf MSG = @echo ===== SNAPRAID = ./checker$(EXEEXT) +SNAPCHECK_NTFS = powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(srcdir)/test/snapcheck-ntfs.ps1" CLEANFILES += rm.exe mv.exe touch.exe @@ -355,7 +357,7 @@ if !HAVE_POSIX endif # !HAVE_POSIX # Not included in main check because it requires root permissions -snapcheck_btrfs: mktest checker setupcheck +snapcheck_btrfs: mktest$(EXEEXT) checker$(EXEEXT) setupcheck sudo $(MAKE) snapcheck_btrfs_run; \ status=$$?; \ sudo $(MAKE) snapcheck_btrfs_cleanup; \ @@ -364,7 +366,7 @@ snapcheck_btrfs: mktest checker setupcheck exit $$status # Not included in main check because it requires root permissions -snapcheck_zfs: mktest checker setupcheck +snapcheck_zfs: mktest$(EXEEXT) checker$(EXEEXT) setupcheck sudo $(MAKE) snapcheck_zfs_run; \ status=$$?; \ sudo $(MAKE) snapcheck_zfs_cleanup; \ @@ -372,6 +374,15 @@ snapcheck_zfs: mktest checker setupcheck if test $$cleanup_status -ne 0; then exit $$cleanup_status; fi; \ exit $$status +# Not included in main check because it requires administrator privileges +snapcheck_ntfs: mktest$(EXEEXT) checker$(EXEEXT) setupcheck + $(MAKE) snapcheck_ntfs_run; \ + status=$$?; \ + $(MAKE) snapcheck_ntfs_cleanup; \ + cleanup_status=$$?; \ + if test $$cleanup_status -ne 0; then exit $$cleanup_status; fi; \ + exit $$status + snapcheck_btrfs_run: mkdir bench/snap1 mkdir bench/snap2 @@ -401,6 +412,11 @@ snapcheck_zfs_run: zfs create -o mountpoint=$(CURDIR)/bench/snap3 -o snapdir=visible $$(cat bench/snap-zpool)/snap3 $(MAKE) snapcheck_run SNAPCHECK_SHOW=snapcheck_zfs_show +snapcheck_ntfs_run: + $(MSG) Create NTFS storages + $(SNAPCHECK_NTFS) setup + $(MAKE) snapcheck_run SNAPCHECK_SHOW=snapcheck_ntfs_show + snapcheck_btrfs_show: @for dir in bench/snap1 bench/snap2 bench/snap3; do \ echo "$$dir/.snapraid:"; \ @@ -411,6 +427,9 @@ snapcheck_zfs_show: @pool=$$(cat bench/snap-zpool); \ zfs list -t snapshot -r "$$pool" +snapcheck_ntfs_show: + @$(SNAPCHECK_NTFS) show + snapcheck_run: $(MSG) Create some files $(TESTENV) ./mktest$(EXEEXT) generate 1 snap 3 $(CHECKCOUNT) $(CHECKSIZE) @@ -503,6 +522,11 @@ snapcheck_zfs_cleanup: rm -rf bench rm -f test.log +snapcheck_ntfs_cleanup: + $(MSG) Cleanup NTFS storages + $(SNAPCHECK_NTFS) cleanup + rm -f test.log + check-local: setupcheck mkdir bench/disk1 mkdir bench/disk2 diff --git a/test/snapcheck-ntfs.ps1 b/test/snapcheck-ntfs.ps1 new file mode 100644 index 00000000..695a2ee7 --- /dev/null +++ b/test/snapcheck-ntfs.ps1 @@ -0,0 +1,295 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet("setup", "cleanup", "show")] + [string]$Action +) + +$ErrorActionPreference = "Stop" + +$root = (Get-Location).Path +$bench = Join-Path $root "bench" + + +function Assert-Administrator +{ + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($identity) + + if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { + throw "NTFS snapshot tests require Administrator privileges." + } +} + + +function Invoke-DiskPart +{ + param( + [Parameter(Mandatory = $true)] + [string[]]$Commands + ) + + $scriptPath = Join-Path $env:TEMP ("snapraid-diskpart-" + [Guid]::NewGuid().ToString() + ".txt") + + try { + [System.IO.File]::WriteAllLines($scriptPath, $Commands, [System.Text.Encoding]::ASCII) + + $output = & diskpart.exe /s $scriptPath 2>&1 + $exitCode = $LASTEXITCODE + + $output | ForEach-Object { + Write-Host $_ + } + + if ($exitCode -ne 0) { + throw "diskpart failed with exit code $exitCode." + } + } + finally { + Remove-Item -LiteralPath $scriptPath -Force -ErrorAction SilentlyContinue + } +} + + +function Get-MountedVolumeName +{ + param( + [Parameter(Mandatory = $true)] + [string]$MountPath + ) + + $mountArg = $MountPath.TrimEnd([char]92) + "\" + $output = & mountvol.exe $mountArg /L 2>$null + $exitCode = $LASTEXITCODE + + if ($exitCode -ne 0) { + return $null + } + + foreach ($line in $output) { + $value = "$line".Trim() + + if ($value -match '^\\\\\?\\Volume\{[^}]+\}\\$') { + return $value + } + } + + return $null +} + + +function Normalize-VolumeName +{ + param( + [Parameter(Mandatory = $true)] + [string]$VolumeName + ) + + return $VolumeName.Trim().TrimEnd([char]92) +} + + +function Setup-Volume +{ + param( + [Parameter(Mandatory = $true)] + [int]$Index + ) + + $mountPath = Join-Path $bench "snap$Index" + $vhdPath = Join-Path $bench "snap$Index.vhdx" + $markerPath = Join-Path $bench "snap$Index.volume" + + if (Test-Path -LiteralPath $mountPath) { + throw "Mount path '$mountPath' already exists." + } + + if (Test-Path -LiteralPath $vhdPath) { + throw "VHD '$vhdPath' already exists." + } + + if (Test-Path -LiteralPath $markerPath) { + throw "Volume marker '$markerPath' already exists." + } + + New-Item -ItemType Directory -Path $mountPath | Out-Null + + $mountForDiskPart = $mountPath.TrimEnd([char]92) + "\" + + Invoke-DiskPart @( + "create vdisk file=`"$vhdPath`" maximum=1024 type=expandable", + "select vdisk file=`"$vhdPath`"", + "attach vdisk", + "convert gpt", + "create partition primary", + "format fs=ntfs quick label=SNAPRAID$Index", + "assign mount=`"$mountForDiskPart`"" + ) + + $volumeName = Get-MountedVolumeName $mountPath + + if (-not $volumeName) { + throw "Unable to resolve the volume name mounted at '$mountPath'." + } + + Set-Content -LiteralPath $markerPath -Value $volumeName -Encoding ASCII + + Write-Host "Mounted NTFS test volume $volumeName at $mountPath" +} + + +function Remove-TestShadows +{ + param( + [Parameter(Mandatory = $true)] + [string]$VolumeName + ) + + $wanted = Normalize-VolumeName $VolumeName + + $shadows = @( + Get-WmiObject -Class Win32_ShadowCopy | + Where-Object { + $_.VolumeName -and ((Normalize-VolumeName $_.VolumeName) -ieq $wanted) + } + ) + + foreach ($shadow in $shadows) { + Write-Host "Deleting VSS snapshot $($shadow.ID) from $VolumeName" + Remove-WmiObject -InputObject $shadow + } +} + + +function Cleanup-Volume +{ + param( + [Parameter(Mandatory = $true)] + [int]$Index + ) + + $mountPath = Join-Path $bench "snap$Index" + $vhdPath = Join-Path $bench "snap$Index.vhdx" + $markerPath = Join-Path $bench "snap$Index.volume" + + if (Test-Path -LiteralPath $markerPath) { + $volumeName = (Get-Content -LiteralPath $markerPath -Raw).Trim() + + if ($volumeName) { + Remove-TestShadows $volumeName + } + } + + if (Test-Path -LiteralPath $vhdPath) { + Invoke-DiskPart @( + "select vdisk file=`"$vhdPath`"", + "detach vdisk noerr" + ) + + Remove-Item -LiteralPath $vhdPath -Force + } + + if (Test-Path -LiteralPath $mountPath) { + Remove-Item -LiteralPath $mountPath -Recurse -Force + } + + if (Test-Path -LiteralPath $markerPath) { + Remove-Item -LiteralPath $markerPath -Force + } +} + + +function Setup +{ + if (-not (Test-Path -LiteralPath $bench)) { + New-Item -ItemType Directory -Path $bench | Out-Null + } + + Setup-Volume 1 + Setup-Volume 2 + Setup-Volume 3 +} + + +function Cleanup +{ + $failed = $false + + foreach ($index in 1..3) { + try { + Cleanup-Volume $index + } + catch { + Write-Warning "Cleanup of NTFS test volume $index failed: $($_.Exception.Message)" + $failed = $true + } + } + + if ($failed) { + throw "Cleanup of one or more NTFS test volumes failed." + } + + if (Test-Path -LiteralPath $bench) { + Remove-Item -LiteralPath $bench -Recurse -Force + } +} + + +function Show +{ + foreach ($index in 1..3) { + $mountPath = Join-Path $bench "snap$Index" + $snapshotPath = Join-Path $mountPath ".snapraid" + $markerPath = Join-Path $bench "snap$Index.volume" + + Write-Host "${snapshotPath}:" + + if (Test-Path -LiteralPath $snapshotPath) { + Get-ChildItem -LiteralPath $snapshotPath -Force | + Sort-Object Name | + ForEach-Object { + Write-Host " $($_.Name)" + } + } + else { + Write-Host " (missing)" + } + + if (Test-Path -LiteralPath $markerPath) { + $volumeName = (Get-Content -LiteralPath $markerPath -Raw).Trim() + $wanted = Normalize-VolumeName $volumeName + + Get-WmiObject -Class Win32_ShadowCopy | + Where-Object { + $_.VolumeName -and ((Normalize-VolumeName $_.VolumeName) -ieq $wanted) + } | + ForEach-Object { + Write-Host " VSS $($_.ID)" + } + } + } +} + + +try { + switch ($Action) { + "setup" { + Assert-Administrator + Setup + } + + "cleanup" { + Assert-Administrator + Cleanup + } + + "show" { + Show + } + } +} +catch { + [Console]::Error.WriteLine("error: " + $_.Exception.Message) + exit 1 +} + +exit 0