n/b/mkimage/osimage: init

Common parts of mkimage were moved to osimage.

mkimage remains in use as a frontend to osimage, with backward
compatible use semantics.

Change-Id: I08c1c44afdf7bf0c07333ab3c6bbebf53ff4d0db
Reviewed-on: https://review.monogon.dev/c/monogon/+/324
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/build/mkimage/main.go b/metropolis/node/build/mkimage/main.go
index f41d643..e5c25df 100644
--- a/metropolis/node/build/mkimage/main.go
+++ b/metropolis/node/build/mkimage/main.go
@@ -14,166 +14,78 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// mkimage is a tool to generate Metropolis node disk images.
+// It can be used both to initialize block devices and to create image
+// files.
+//
+// The tool takes a path to an EFI payload (--efi), and a path to a
+// Metropolis system image (--system) as its only required inputs. In
+// addition, an output path must be supplied (--out).
+// Node parameters file path (--node_parameters) may also be supplied, in
+// which case the file will be copied to the EFI system partition.
+// Partition sizes are fixed and may be overridden by command line flags.
 package main
 
-// mkimage is a tool to generate a Metropolis node disk image containing the
-// given EFI payload, and optionally, a given external initramfs image and
-// node parameters
-
 import (
 	"flag"
-	"fmt"
-	"io"
-	"io/ioutil"
 	"log"
 	"os"
 
-	diskfs "github.com/diskfs/go-diskfs"
-	"github.com/diskfs/go-diskfs/disk"
-	"github.com/diskfs/go-diskfs/filesystem"
-	"github.com/diskfs/go-diskfs/partition/gpt"
+	"source.monogon.dev/metropolis/node/build/mkimage/osimage"
 )
 
-var NodeDataPartition gpt.Type = gpt.Type("9eeec464-6885-414a-b278-4305c51f7966")
-var NodeSystemPartition gpt.Type = gpt.Type("ee96055b-f6d0-4267-8bbb-724b2afea74c")
-
-var (
-	flagEFI                 string
-	flagOut                 string
-	flagSystemPath          string
-	flagNodeParameters      string
-	flagDataPartitionSize   uint64
-	flagESPPartitionSize    uint64
-	flagSystemPartitionSize uint64
-)
-
-func mibToSectors(size uint64) uint64 {
-	return (size * 1024 * 1024) / 512
-}
-
-type devZeroReader struct{}
-
-func (_ devZeroReader) Read(b []byte) (n int, err error) {
-	for i := range b {
-		b[i] = 0
-	}
-	return len(b), nil
-}
-
-// devZero is a /dev/zero-like reader which reads an infinite number of zeroes
-var devZero = devZeroReader{}
-
 func main() {
-	flag.StringVar(&flagEFI, "efi", "", "UEFI payload")
-	flag.StringVar(&flagOut, "out", "", "Output disk image")
-	flag.StringVar(&flagSystemPath, "system", "", "System partition [optional]")
-	flag.StringVar(&flagNodeParameters, "node_parameters", "", "Node parameters [optional]")
-	flag.Uint64Var(&flagDataPartitionSize, "data_partition_size", 2048, "Override the data partition size (default 2048 MiB)")
-	flag.Uint64Var(&flagESPPartitionSize, "esp_partition_size", 128, "Override the ESP partition size (default: 128MiB)")
-	flag.Uint64Var(&flagSystemPartitionSize, "system_partition_size", 1024, "Override the System partition size (default: 1024MiB)")
+	// Fill in the image parameters based on flags.
+	var (
+		efiPayload  string
+		systemImage string
+		nodeParams  string
+		cfg         osimage.Params
+	)
+	flag.StringVar(&efiPayload, "efi", "", "Path to the UEFI payload used")
+	flag.StringVar(&systemImage, "system", "", "Path to the system partition image used")
+	flag.StringVar(&nodeParams, "node_parameters", "", "Path to Node Parameters to be written to the ESP (default: don't write Node Parameters)")
+	flag.StringVar(&cfg.OutputPath, "out", "", "Path to the resulting disk image or block device")
+	flag.Uint64Var(&cfg.PartitionSize.Data, "data_partition_size", 2048, "Override the data partition size (default 2048 MiB). Used only when generating image files.")
+	flag.Uint64Var(&cfg.PartitionSize.ESP, "esp_partition_size", 128, "Override the ESP partition size (default: 128MiB)")
+	flag.Uint64Var(&cfg.PartitionSize.System, "system_partition_size", 1024, "Override the System partition size (default: 1024MiB)")
+	flag.StringVar(&cfg.DiskGUID, "GUID", "", "Disk GUID marked in the resulting image's partition table (default: randomly generated)")
 	flag.Parse()
 
-	if flagEFI == "" || flagOut == "" {
-		log.Fatalf("efi and initramfs must be set")
-	}
-
-	_ = os.Remove(flagOut)
-	diskImg, err := diskfs.Create(flagOut, 4*1024*1024*1024, diskfs.Raw)
+	// Open the input files for osimage.Create, fill in reader objects and
+	// metadata in osimage.Params.
+	// Start with the EFI Payload the OS will boot from.
+	p, err := os.Open(efiPayload)
 	if err != nil {
-		log.Fatalf("diskfs.Create(%q): %v", flagOut, err)
+		log.Fatalf("while opening the EFI payload at %q: %s", efiPayload, err.Error())
 	}
+	defer p.Close()
+	cfg.EFIPayload = p
 
-	table := &gpt.Table{
-		// This is appropriate at least for virtio disks. Might need to be
-		// adjusted for real ones.
-		LogicalSectorSize:  512,
-		PhysicalSectorSize: 512,
-		ProtectiveMBR:      true,
-		Partitions: []*gpt.Partition{
-			{
-				Type:  gpt.EFISystemPartition,
-				Name:  "ESP",
-				Start: mibToSectors(1),
-				End:   mibToSectors(flagESPPartitionSize) - 1,
-			},
-			{
-				Type:  NodeSystemPartition,
-				Name:  "METROPOLIS-SYSTEM",
-				Start: mibToSectors(flagESPPartitionSize),
-				End:   mibToSectors(flagESPPartitionSize+flagSystemPartitionSize) - 1,
-			},
-			{
-				Type:  NodeDataPartition,
-				Name:  "METROPOLIS-NODE-DATA",
-				Start: mibToSectors(flagESPPartitionSize + flagSystemPartitionSize),
-				End:   mibToSectors(flagESPPartitionSize+flagSystemPartitionSize+flagDataPartitionSize) - 1,
-			},
-		},
-	}
-	if err := diskImg.Partition(table); err != nil {
-		log.Fatalf("Failed to apply partition table: %v", err)
-	}
-
-	if flagSystemPath != "" {
-		systemPart, err := os.Open(flagSystemPath)
+	// Attempt to open the system image if its path is set. In case the path
+	// isn't set, the system partition will still be created, but no
+	// contents will be written into it.
+	if systemImage != "" {
+		img, err := os.Open(systemImage)
 		if err != nil {
-			log.Fatalf("Failed to open system partition: %v", err)
+			log.Fatalf("while opening the system image at %q: %s", systemImage, err.Error())
 		}
-		defer systemPart.Close()
-		systemPartMeta, err := systemPart.Stat()
+		defer img.Close()
+		cfg.SystemImage = img
+	}
+
+	// Attempt to open the node parameters file if its path is set.
+	if nodeParams != "" {
+		np, err := os.Open(nodeParams)
 		if err != nil {
-			log.Fatalf("Failed to stat system partition: %v", err)
+			log.Fatalf("while opening node parameters at %q: %s", nodeParams, err.Error())
 		}
-		padding := int64(flagSystemPartitionSize*1024*1024) - systemPartMeta.Size()
-		systemPartMulti := io.MultiReader(systemPart, io.LimitReader(devZero, padding))
-		if _, err := diskImg.WritePartitionContents(2, systemPartMulti); err != nil {
-			log.Fatalf("Failed to write system partition: %v", err)
-		}
+		defer np.Close()
+		cfg.NodeParameters = np
 	}
 
-	fs, err := diskImg.CreateFilesystem(disk.FilesystemSpec{Partition: 1, FSType: filesystem.TypeFat32, VolumeLabel: "ESP"})
-	if err != nil {
-		log.Fatalf("Failed to create filesystem: %v", err)
-	}
-
-	// Create EFI partition structure.
-	for _, dir := range []string{"/EFI", "/EFI/BOOT", "/EFI/metropolis"} {
-		if err := fs.Mkdir(dir); err != nil {
-			log.Fatalf("Mkdir(%q): %v", dir, err)
-		}
-	}
-
-	put(fs, flagEFI, "/EFI/BOOT/BOOTX64.EFI")
-
-	if flagNodeParameters != "" {
-		put(fs, flagNodeParameters, "/EFI/metropolis/parameters.pb")
-	}
-
-	if err := diskImg.File.Close(); err != nil {
-		log.Fatalf("Failed to finalize image: %v", err)
-	}
-	log.Printf("Success! You can now boot %v", flagOut)
-}
-
-// put copies a file from the host filesystem into the target image.
-func put(fs filesystem.FileSystem, src, dst string) {
-	target, err := fs.OpenFile(dst, os.O_CREATE|os.O_RDWR)
-	if err != nil {
-		log.Fatalf("fs.OpenFile(%q): %v", dst, err)
-	}
-	source, err := os.Open(src)
-	if err != nil {
-		log.Fatalf("os.Open(%q): %v", src, err)
-	}
-	defer source.Close()
-	// If this is streamed (e.g. using io.Copy) it exposes a bug in diskfs, so
-	// do it in one go.
-	data, err := ioutil.ReadAll(source)
-	if err != nil {
-		log.Fatalf("Reading %q: %v", src, err)
-	}
-	if _, err := target.Write(data); err != nil {
-		fmt.Printf("writing file %q: %v", dst, err)
-		os.Exit(1)
+	// Write the parametrized OS image.
+	if _, err := osimage.Create(&cfg); err != nil {
+		log.Fatalf("while creating a Metropolis OS image: %s", err.Error())
 	}
 }