m/test/l/cluster: use sparse-aware copy for images

This speeds up the image copy significantly as the file is less than 10%
allocated. Especially for many concurrent runs this greatly reduces disk
bottlenecking.

This has been tested by comparing the SHA256 hash of both the original
and the copied image, which is still the same.

Change-Id: I65f62537f048bc180cd732c53a03f980e016b723
Reviewed-on: https://review.monogon.dev/c/monogon/+/2874
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/test/launch/cluster/cluster.go b/metropolis/test/launch/cluster/cluster.go
index 69d9667..e73385d 100644
--- a/metropolis/test/launch/cluster/cluster.go
+++ b/metropolis/test/launch/cluster/cluster.go
@@ -29,6 +29,7 @@
 	"github.com/cenkalti/backoff/v4"
 	"go.uber.org/multierr"
 	"golang.org/x/net/proxy"
+	"golang.org/x/sys/unix"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -408,10 +409,39 @@
 	}
 	defer out.Close()
 
-	_, err = io.Copy(out, in)
+	endPos, err := in.Seek(0, io.SeekEnd)
 	if err != nil {
-		return fmt.Errorf("when copying file: %w", err)
+		return fmt.Errorf("when getting source end: %w", err)
 	}
+
+	// Copy the file while preserving its sparseness. The image files are very
+	// sparse (less than 10% allocated), so this is a lot faster.
+	var lastHoleStart int64
+	for {
+		dataStart, err := in.Seek(lastHoleStart, unix.SEEK_DATA)
+		if err != nil {
+			return fmt.Errorf("when seeking to next data block: %w", err)
+		}
+		holeStart, err := in.Seek(dataStart, unix.SEEK_HOLE)
+		if err != nil {
+			return fmt.Errorf("when seeking to next hole: %w", err)
+		}
+		lastHoleStart = holeStart
+		if _, err := in.Seek(dataStart, io.SeekStart); err != nil {
+			return fmt.Errorf("when seeking to current data block: %w", err)
+		}
+		if _, err := out.Seek(dataStart, io.SeekStart); err != nil {
+			return fmt.Errorf("when seeking output to next data block: %w", err)
+		}
+		if _, err := io.CopyN(out, in, holeStart-dataStart); err != nil {
+			return fmt.Errorf("when copying file: %w", err)
+		}
+		if endPos == holeStart {
+			// The next hole is at the end of the file, we're done here.
+			break
+		}
+	}
+
 	return out.Close()
 }