blob: 92e83030a007a39bc4dfd6f66a364f3c7b76bfc9 [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är5fdca562025-04-14 11:33:29 +000018 "source.monogon.dev/osbase/oci"
Jan Schärc1b6df42025-03-20 08:52:18 +000019 "source.monogon.dev/osbase/structfs"
Lorenz Brun57479bb2021-10-26 14:01:06 +020020)
21
Lorenz Brun57479bb2021-10-26 14:01:06 +020022type MakeInstallerImageArgs struct {
23 // Path to either a file or a disk which will contain the installer data.
24 TargetPath string
25
26 // Reader for the installer EFI executable. Mandatory.
Jan Schärc1b6df42025-03-20 08:52:18 +000027 Installer structfs.Blob
Lorenz Brun57479bb2021-10-26 14:01:06 +020028
29 // Optional NodeParameters to be embedded for use by the installer.
30 NodeParams *api.NodeParameters
31
Jan Schär5fdca562025-04-14 11:33:29 +000032 // Optional OS image for use by the installer.
33 Image *oci.Image
Lorenz Brun57479bb2021-10-26 14:01:06 +020034}
35
Lorenz Brunad131882023-06-28 16:42:20 +020036// MakeInstallerImage generates an installer disk image containing a Table
Lorenz Brun57479bb2021-10-26 14:01:06 +020037// partition table and a single FAT32 partition with an installer and optionally
Jan Schär5fdca562025-04-14 11:33:29 +000038// with an OS image and/or Node Parameters.
Lorenz Brun57479bb2021-10-26 14:01:06 +020039func MakeInstallerImage(args MakeInstallerImageArgs) error {
40 if args.Installer == nil {
Tim Windelschmidt73e98822024-04-18 23:13:49 +020041 return errors.New("installer is mandatory")
Lorenz Brun57479bb2021-10-26 14:01:06 +020042 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020043
Jan Schärc1b6df42025-03-20 08:52:18 +000044 var espRoot structfs.Tree
Lorenz Brun57479bb2021-10-26 14:01:06 +020045
Lorenz Brun57479bb2021-10-26 14:01:06 +020046 // This needs to be a "Removable Media" according to the UEFI Specification
47 // V2.9 Section 3.5.1.1. This file is booted by any compliant UEFI firmware
48 // in absence of another bootable boot entry.
Lorenz Brunad131882023-06-28 16:42:20 +020049 if err := espRoot.PlaceFile("EFI/BOOT/BOOTx64.EFI", args.Installer); err != nil {
50 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020051 }
Lorenz Brunad131882023-06-28 16:42:20 +020052
Lorenz Brun57479bb2021-10-26 14:01:06 +020053 if args.NodeParams != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020054 nodeParamsRaw, err := proto.Marshal(args.NodeParams)
Lorenz Brun57479bb2021-10-26 14:01:06 +020055 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020056 return fmt.Errorf("failed to marshal node params: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020057 }
Jan Schärc1b6df42025-03-20 08:52:18 +000058 if err := espRoot.PlaceFile("metropolis-installer/nodeparams.pb", structfs.Bytes(nodeParamsRaw)); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020059 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020060 }
61 }
Jan Schär5fdca562025-04-14 11:33:29 +000062 if args.Image != nil {
63 imageLayout, err := oci.CreateLayout(args.Image)
64 if err != nil {
65 return err
66 }
67 if err := espRoot.PlaceDir("metropolis-installer/osimage", imageLayout); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020068 return err
69 }
70 }
71 var targetDev blockdev.BlockDev
72 var err error
73 targetDev, err = blockdev.Open(args.TargetPath)
74 if err != nil {
75 if errors.Is(err, os.ErrNotExist) {
76 targetDev, err = blockdev.CreateFile(args.TargetPath, 512, 1024*1024+4096)
77 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020078 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020079 return fmt.Errorf("unable to open target device: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020080 }
Lorenz Brunad131882023-06-28 16:42:20 +020081 }
82 partTable, err := gpt.New(targetDev)
83 if err != nil {
84 return fmt.Errorf("target device has invalid geometry: %w", err)
85 }
86 esp := gpt.Partition{
87 Type: gpt.PartitionTypeEFISystem,
88 Name: "MetropolisInstaller",
89 }
90 fatOpts := fat32.Options{Label: "METRO_INST"}
91 // TODO(#254): Build and use dynamically-grown block devices
92 var espSize int64 = 512 * 1024 * 1024
93 if err := partTable.AddPartition(&esp, espSize); err != nil {
94 return fmt.Errorf("unable to create partition layout: %w", err)
95 }
96 if esp.BlockSize() > math.MaxUint16 {
97 return fmt.Errorf("block size (%d) too large for FAT32", esp.BlockSize())
98 }
99 fatOpts.BlockSize = uint16(esp.BlockSize())
100 fatOpts.BlockCount = uint32(esp.BlockCount())
101 if err := fat32.WriteFS(blockdev.NewRWS(esp), espRoot, fatOpts); err != nil {
102 return fmt.Errorf("failed to write FAT32: %w", err)
103 }
104 if err := partTable.Write(); err != nil {
105 return fmt.Errorf("unable to write partition table: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +0200106 }
107 return nil
108}