blob: 5d43a893cf09b4f076d61b9a0f4313badad08689 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun57479bb2021-10-26 14:01:06 +02004package core
5
6import (
7 "errors"
8 "fmt"
Lorenz Brunad131882023-06-28 16:42:20 +02009 "math"
Lorenz Brun57479bb2021-10-26 14:01:06 +020010 "os"
11
Lorenz Brun57479bb2021-10-26 14:01:06 +020012 "google.golang.org/protobuf/proto"
Serge Bazanski44d2ad42021-11-10 17:05:34 +010013
Lorenz Brun57479bb2021-10-26 14:01:06 +020014 "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020015 "source.monogon.dev/osbase/blockdev"
16 "source.monogon.dev/osbase/fat32"
17 "source.monogon.dev/osbase/gpt"
Jan Schärc1b6df42025-03-20 08:52:18 +000018 "source.monogon.dev/osbase/structfs"
Lorenz Brun57479bb2021-10-26 14:01:06 +020019)
20
Lorenz Brun57479bb2021-10-26 14:01:06 +020021type MakeInstallerImageArgs struct {
22 // Path to either a file or a disk which will contain the installer data.
23 TargetPath string
24
25 // Reader for the installer EFI executable. Mandatory.
Jan Schärc1b6df42025-03-20 08:52:18 +000026 Installer structfs.Blob
Lorenz Brun57479bb2021-10-26 14:01:06 +020027
28 // Optional NodeParameters to be embedded for use by the installer.
29 NodeParams *api.NodeParameters
30
31 // Optional Reader for a Metropolis bundle for use by the installer.
Jan Schärc1b6df42025-03-20 08:52:18 +000032 Bundle structfs.Blob
Lorenz Brun57479bb2021-10-26 14:01:06 +020033}
34
Lorenz Brunad131882023-06-28 16:42:20 +020035// MakeInstallerImage generates an installer disk image containing a Table
Lorenz Brun57479bb2021-10-26 14:01:06 +020036// partition table and a single FAT32 partition with an installer and optionally
37// with a bundle and/or Node Parameters.
38func MakeInstallerImage(args MakeInstallerImageArgs) error {
39 if args.Installer == nil {
Tim Windelschmidt73e98822024-04-18 23:13:49 +020040 return errors.New("installer is mandatory")
Lorenz Brun57479bb2021-10-26 14:01:06 +020041 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020042
Jan Schärc1b6df42025-03-20 08:52:18 +000043 var espRoot structfs.Tree
Lorenz Brun57479bb2021-10-26 14:01:06 +020044
Lorenz Brun57479bb2021-10-26 14:01:06 +020045 // This needs to be a "Removable Media" according to the UEFI Specification
46 // V2.9 Section 3.5.1.1. This file is booted by any compliant UEFI firmware
47 // in absence of another bootable boot entry.
Lorenz Brunad131882023-06-28 16:42:20 +020048 if err := espRoot.PlaceFile("EFI/BOOT/BOOTx64.EFI", args.Installer); err != nil {
49 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020050 }
Lorenz Brunad131882023-06-28 16:42:20 +020051
Lorenz Brun57479bb2021-10-26 14:01:06 +020052 if args.NodeParams != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020053 nodeParamsRaw, err := proto.Marshal(args.NodeParams)
Lorenz Brun57479bb2021-10-26 14:01:06 +020054 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020055 return fmt.Errorf("failed to marshal node params: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020056 }
Jan Schärc1b6df42025-03-20 08:52:18 +000057 if err := espRoot.PlaceFile("metropolis-installer/nodeparams.pb", structfs.Bytes(nodeParamsRaw)); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020058 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020059 }
60 }
61 if args.Bundle != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020062 if err := espRoot.PlaceFile("metropolis-installer/bundle.bin", args.Bundle); err != nil {
63 return err
64 }
65 }
66 var targetDev blockdev.BlockDev
67 var err error
68 targetDev, err = blockdev.Open(args.TargetPath)
69 if err != nil {
70 if errors.Is(err, os.ErrNotExist) {
71 targetDev, err = blockdev.CreateFile(args.TargetPath, 512, 1024*1024+4096)
72 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020073 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020074 return fmt.Errorf("unable to open target device: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020075 }
Lorenz Brunad131882023-06-28 16:42:20 +020076 }
77 partTable, err := gpt.New(targetDev)
78 if err != nil {
79 return fmt.Errorf("target device has invalid geometry: %w", err)
80 }
81 esp := gpt.Partition{
82 Type: gpt.PartitionTypeEFISystem,
83 Name: "MetropolisInstaller",
84 }
85 fatOpts := fat32.Options{Label: "METRO_INST"}
86 // TODO(#254): Build and use dynamically-grown block devices
87 var espSize int64 = 512 * 1024 * 1024
88 if err := partTable.AddPartition(&esp, espSize); err != nil {
89 return fmt.Errorf("unable to create partition layout: %w", err)
90 }
91 if esp.BlockSize() > math.MaxUint16 {
92 return fmt.Errorf("block size (%d) too large for FAT32", esp.BlockSize())
93 }
94 fatOpts.BlockSize = uint16(esp.BlockSize())
95 fatOpts.BlockCount = uint32(esp.BlockCount())
96 if err := fat32.WriteFS(blockdev.NewRWS(esp), espRoot, fatOpts); err != nil {
97 return fmt.Errorf("failed to write FAT32: %w", err)
98 }
99 if err := partTable.Write(); err != nil {
100 return fmt.Errorf("unable to write partition table: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +0200101 }
102 return nil
103}