Add NTFS snapshot testing on Windows using isolated VHD volumes

This commit is contained in:
Andrea Mazzoleni 2026-09-09 00:01:12 +02:00
parent 9c985612ea
commit 0e94163371
3 changed files with 324 additions and 3 deletions

View file

@ -78,7 +78,9 @@ jobs:
install: mingw-w64-x86_64-gcc autoconf automake make install: mingw-w64-x86_64-gcc autoconf automake make
- run: autoreconf -i - run: autoreconf -i
- run: ./configure --enable-warning-as-error - run: ./configure --enable-warning-as-error
- run: make all dist check - run: make all
- run: make snapcheck_ntfs
- run: make dist check
build-freebsd: build-freebsd:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -178,6 +178,7 @@ EXTRA_DIST = \
$(LIST_MAN) \ $(LIST_MAN) \
$(LIST_TXT) \ $(LIST_TXT) \
test/test-snap.conf \ test/test-snap.conf \
test/snapcheck-ntfs.ps1 \
test/test-par1.conf \ test/test-par1.conf \
test/test-par2.conf \ test/test-par2.conf \
test/test-par3.conf \ test/test-par3.conf \
@ -340,6 +341,7 @@ REALLOC_SPLIT2 = $(srcdir)/test/test-realloc-split2.conf
SNAP = $(srcdir)/test/test-snap.conf SNAP = $(srcdir)/test/test-snap.conf
MSG = @echo ===== MSG = @echo =====
SNAPRAID = ./checker$(EXEEXT) SNAPRAID = ./checker$(EXEEXT)
SNAPCHECK_NTFS = powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(srcdir)/test/snapcheck-ntfs.ps1"
CLEANFILES += rm.exe mv.exe touch.exe CLEANFILES += rm.exe mv.exe touch.exe
@ -355,7 +357,7 @@ if !HAVE_POSIX
endif # !HAVE_POSIX endif # !HAVE_POSIX
# Not included in main check because it requires root permissions # 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; \ sudo $(MAKE) snapcheck_btrfs_run; \
status=$$?; \ status=$$?; \
sudo $(MAKE) snapcheck_btrfs_cleanup; \ sudo $(MAKE) snapcheck_btrfs_cleanup; \
@ -364,7 +366,7 @@ snapcheck_btrfs: mktest checker setupcheck
exit $$status exit $$status
# Not included in main check because it requires root permissions # 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; \ sudo $(MAKE) snapcheck_zfs_run; \
status=$$?; \ status=$$?; \
sudo $(MAKE) snapcheck_zfs_cleanup; \ 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; \ if test $$cleanup_status -ne 0; then exit $$cleanup_status; fi; \
exit $$status 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: snapcheck_btrfs_run:
mkdir bench/snap1 mkdir bench/snap1
mkdir bench/snap2 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 zfs create -o mountpoint=$(CURDIR)/bench/snap3 -o snapdir=visible $$(cat bench/snap-zpool)/snap3
$(MAKE) snapcheck_run SNAPCHECK_SHOW=snapcheck_zfs_show $(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: snapcheck_btrfs_show:
@for dir in bench/snap1 bench/snap2 bench/snap3; do \ @for dir in bench/snap1 bench/snap2 bench/snap3; do \
echo "$$dir/.snapraid:"; \ echo "$$dir/.snapraid:"; \
@ -411,6 +427,9 @@ snapcheck_zfs_show:
@pool=$$(cat bench/snap-zpool); \ @pool=$$(cat bench/snap-zpool); \
zfs list -t snapshot -r "$$pool" zfs list -t snapshot -r "$$pool"
snapcheck_ntfs_show:
@$(SNAPCHECK_NTFS) show
snapcheck_run: snapcheck_run:
$(MSG) Create some files $(MSG) Create some files
$(TESTENV) ./mktest$(EXEEXT) generate 1 snap 3 $(CHECKCOUNT) $(CHECKSIZE) $(TESTENV) ./mktest$(EXEEXT) generate 1 snap 3 $(CHECKCOUNT) $(CHECKSIZE)
@ -503,6 +522,11 @@ snapcheck_zfs_cleanup:
rm -rf bench rm -rf bench
rm -f test.log rm -f test.log
snapcheck_ntfs_cleanup:
$(MSG) Cleanup NTFS storages
$(SNAPCHECK_NTFS) cleanup
rm -f test.log
check-local: setupcheck check-local: setupcheck
mkdir bench/disk1 mkdir bench/disk1
mkdir bench/disk2 mkdir bench/disk2

295
test/snapcheck-ntfs.ps1 Normal file
View file

@ -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