blob: 31fe4a0b3b2fe0d594040ec731d16cdbb86c993c [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
Jan Schäre19d2792025-06-23 12:37:58 +000014 "source.monogon.dev/metropolis/installer/install"
Lorenz Brun57479bb2021-10-26 14:01:06 +020015 "source.monogon.dev/metropolis/proto/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020016 "source.monogon.dev/osbase/blockdev"
17 "source.monogon.dev/osbase/fat32"
18 "source.monogon.dev/osbase/gpt"
Jan Schär5fdca562025-04-14 11:33:29 +000019 "source.monogon.dev/osbase/oci"
Jan Schäre19d2792025-06-23 12:37:58 +000020 "source.monogon.dev/osbase/oci/osimage"
Jan Schärc1b6df42025-03-20 08:52:18 +000021 "source.monogon.dev/osbase/structfs"
Lorenz Brun57479bb2021-10-26 14:01:06 +020022)
23
Lorenz Brun57479bb2021-10-26 14:01:06 +020024type MakeInstallerImageArgs struct {
25 // Path to either a file or a disk which will contain the installer data.
26 TargetPath string
27
28 // Reader for the installer EFI executable. Mandatory.
Jan Schärc1b6df42025-03-20 08:52:18 +000029 Installer structfs.Blob
Lorenz Brun57479bb2021-10-26 14:01:06 +020030
31 // Optional NodeParameters to be embedded for use by the installer.
32 NodeParams *api.NodeParameters
33
Jan Schär4b888262025-05-13 09:12:03 +000034 // OS image for use by the installer.
Jan Schär5fdca562025-04-14 11:33:29 +000035 Image *oci.Image
Lorenz Brun57479bb2021-10-26 14:01:06 +020036}
37
Lorenz Brunad131882023-06-28 16:42:20 +020038// MakeInstallerImage generates an installer disk image containing a Table
Lorenz Brun57479bb2021-10-26 14:01:06 +020039// partition table and a single FAT32 partition with an installer and optionally
Jan Schär5fdca562025-04-14 11:33:29 +000040// with an OS image and/or Node Parameters.
Lorenz Brun57479bb2021-10-26 14:01:06 +020041func MakeInstallerImage(args MakeInstallerImageArgs) error {
42 if args.Installer == nil {
Tim Windelschmidt73e98822024-04-18 23:13:49 +020043 return errors.New("installer is mandatory")
Lorenz Brun57479bb2021-10-26 14:01:06 +020044 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020045
Jan Schäre19d2792025-06-23 12:37:58 +000046 osImage, err := osimage.Read(args.Image)
Jan Schär4b888262025-05-13 09:12:03 +000047 if err != nil {
48 return fmt.Errorf("failed to read OS image: %w", err)
49 }
Jan Schäre19d2792025-06-23 12:37:58 +000050 bootPath, err := install.EFIBootPath(osImage.Config.ProductInfo.Architecture())
Jan Schär4b888262025-05-13 09:12:03 +000051 if err != nil {
52 return err
53 }
54
Jan Schärc1b6df42025-03-20 08:52:18 +000055 var espRoot structfs.Tree
Lorenz Brun57479bb2021-10-26 14:01:06 +020056
Jan Schär4b888262025-05-13 09:12:03 +000057 if err := espRoot.PlaceFile(bootPath, args.Installer); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020058 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020059 }
Lorenz Brunad131882023-06-28 16:42:20 +020060
Lorenz Brun57479bb2021-10-26 14:01:06 +020061 if args.NodeParams != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020062 nodeParamsRaw, err := proto.Marshal(args.NodeParams)
Lorenz Brun57479bb2021-10-26 14:01:06 +020063 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020064 return fmt.Errorf("failed to marshal node params: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020065 }
Jan Schärc1b6df42025-03-20 08:52:18 +000066 if err := espRoot.PlaceFile("metropolis-installer/nodeparams.pb", structfs.Bytes(nodeParamsRaw)); err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020067 return err
Lorenz Brun57479bb2021-10-26 14:01:06 +020068 }
69 }
Jan Schär4b888262025-05-13 09:12:03 +000070 imageLayout, err := oci.CreateLayout(args.Image)
71 if err != nil {
72 return err
73 }
74 if err := espRoot.PlaceDir("metropolis-installer/osimage", imageLayout); err != nil {
75 return err
Lorenz Brunad131882023-06-28 16:42:20 +020076 }
77 var targetDev blockdev.BlockDev
Lorenz Brunad131882023-06-28 16:42:20 +020078 targetDev, err = blockdev.Open(args.TargetPath)
79 if err != nil {
80 if errors.Is(err, os.ErrNotExist) {
81 targetDev, err = blockdev.CreateFile(args.TargetPath, 512, 1024*1024+4096)
82 }
Lorenz Brun57479bb2021-10-26 14:01:06 +020083 if err != nil {
Lorenz Brunad131882023-06-28 16:42:20 +020084 return fmt.Errorf("unable to open target device: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +020085 }
Lorenz Brunad131882023-06-28 16:42:20 +020086 }
87 partTable, err := gpt.New(targetDev)
88 if err != nil {
89 return fmt.Errorf("target device has invalid geometry: %w", err)
90 }
91 esp := gpt.Partition{
92 Type: gpt.PartitionTypeEFISystem,
93 Name: "MetropolisInstaller",
94 }
95 fatOpts := fat32.Options{Label: "METRO_INST"}
96 // TODO(#254): Build and use dynamically-grown block devices
97 var espSize int64 = 512 * 1024 * 1024
98 if err := partTable.AddPartition(&esp, espSize); err != nil {
99 return fmt.Errorf("unable to create partition layout: %w", err)
100 }
101 if esp.BlockSize() > math.MaxUint16 {
102 return fmt.Errorf("block size (%d) too large for FAT32", esp.BlockSize())
103 }
104 fatOpts.BlockSize = uint16(esp.BlockSize())
105 fatOpts.BlockCount = uint32(esp.BlockCount())
106 if err := fat32.WriteFS(blockdev.NewRWS(esp), espRoot, fatOpts); err != nil {
107 return fmt.Errorf("failed to write FAT32: %w", err)
108 }
109 if err := partTable.Write(); err != nil {
110 return fmt.Errorf("unable to write partition table: %w", err)
Lorenz Brun57479bb2021-10-26 14:01:06 +0200111 }
112 return nil
113}