metropolis/node/core/bios_bootcode: Add legacy bootcode

This change provides a legacy bootcode that shows the user that they
are using an invalid configuration, e.g. not use UEFI. This can be
tested with "qemu-system-i386 -hda bazel-bin/metropolis/node/image.img".

Closes monogon-dev/monogon#142

Change-Id: I3337a70125010aec110ad75647346310cac76d37
Reviewed-on: https://review.monogon.dev/c/monogon/+/3748
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/osbase/build/mkimage/def.bzl b/osbase/build/mkimage/def.bzl
index 3f9327b..33323d7 100644
--- a/osbase/build/mkimage/def.bzl
+++ b/osbase/build/mkimage/def.bzl
@@ -1,22 +1,30 @@
 def _node_image_impl(ctx):
     img_file = ctx.actions.declare_file(ctx.label.name + ".img")
+
+    arguments = ctx.actions.args()
+    arguments.add_all([
+        "-efi",
+        ctx.file.kernel.path,
+        "-system",
+        ctx.file.system.path,
+        "-abloader",
+        ctx.file.abloader.path,
+        "-out",
+        img_file.path,
+    ])
+
+    if len(ctx.files.bios_bootcode) != 0:
+        arguments.add_all(["-bios_bootcode", ctx.file.bios_bootcode.path])
+
     ctx.actions.run(
         mnemonic = "MkImage",
         executable = ctx.executable._mkimage,
-        arguments = [
-            "-efi",
-            ctx.file.kernel.path,
-            "-system",
-            ctx.file.system.path,
-            "-abloader",
-            ctx.file.abloader.path,
-            "-out",
-            img_file.path,
-        ],
+        arguments = [arguments],
         inputs = [
             ctx.file.kernel,
             ctx.file.system,
             ctx.file.abloader,
+            ctx.file.bios_bootcode,
         ],
         outputs = [img_file],
     )
@@ -45,6 +53,14 @@
             mandatory = True,
             allow_single_file = True,
         ),
+        "bios_bootcode": attr.label(
+            doc = """
+            Optional label to the BIOS bootcode which gets placed at the start of the first block of the image.
+            Limited to 440 bytes, padding is not required. It is only used by legacy BIOS boot.
+        """,
+            mandatory = False,
+            allow_single_file = True,
+        ),
         "_mkimage": attr.label(
             doc = "The mkimage executable.",
             default = "//osbase/build/mkimage",
diff --git a/osbase/build/mkimage/main.go b/osbase/build/mkimage/main.go
index 37691a9..5e2b082 100644
--- a/osbase/build/mkimage/main.go
+++ b/osbase/build/mkimage/main.go
@@ -40,17 +40,19 @@
 func main() {
 	// Fill in the image parameters based on flags.
 	var (
-		efiPayload      string
-		systemImage     string
-		abLoaderPayload string
-		nodeParams      string
-		outputPath      string
-		diskUUID        string
-		cfg             osimage.Params
+		efiPayload          string
+		systemImage         string
+		abLoaderPayload     string
+		biosBootCodePayload string
+		nodeParams          string
+		outputPath          string
+		diskUUID            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(&abLoaderPayload, "abloader", "", "Path to the abloader payload used")
+	flag.StringVar(&biosBootCodePayload, "bios_bootcode", "", "Optional path to the BIOS bootcode which gets placed at the start of the first block of the image. Limited to 440 bytes, padding is not required. It is only used by legacy BIOS boot.")
 	flag.StringVar(&nodeParams, "node_parameters", "", "Path to Node Parameters to be written to the ESP (default: don't write Node Parameters)")
 	flag.StringVar(&outputPath, "out", "", "Path to the resulting disk image or block device")
 	flag.Int64Var(&cfg.PartitionSize.Data, "data_partition_size", 2048, "Override the data partition size (default 2048 MiB). Used only when generating image files.")
@@ -95,6 +97,14 @@
 		cfg.NodeParameters = np
 	}
 
+	if biosBootCodePayload != "" {
+		bp, err := os.ReadFile(biosBootCodePayload)
+		if err != nil {
+			log.Fatalf("while opening BIOS bootcode at %q: %v", biosBootCodePayload, err)
+		}
+		cfg.BIOSBootCode = bp
+	}
+
 	// TODO(#254): Build and use dynamically-grown block devices
 	cfg.Output, err = blockdev.CreateFile(outputPath, 512, 10*1024*1024)
 	if err != nil {
diff --git a/osbase/build/mkimage/osimage/osimage.go b/osbase/build/mkimage/osimage/osimage.go
index d139d6b..2f497a8 100644
--- a/osbase/build/mkimage/osimage/osimage.go
+++ b/osbase/build/mkimage/osimage/osimage.go
@@ -92,6 +92,9 @@
 	// PartitionSize specifies a size for the ESP, Metropolis System and
 	// Metropolis data partition.
 	PartitionSize PartitionSizeInfo
+	// BIOSBootCode provides the optional contents for the protective MBR
+	// block which gets executed by legacy BIOS boot.
+	BIOSBootCode []byte
 }
 
 type plan struct {
@@ -155,6 +158,7 @@
 	}
 
 	params.tbl.ID = params.DiskGUID
+	params.tbl.BootCode = p.BIOSBootCode
 	params.efiPartition = &gpt.Partition{
 		Type: gpt.PartitionTypeEFISystem,
 		Name: ESPLabel,