blob: d37bb385407916339ee1fd351eaebd67733ff6cc [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 Brun35fcf032023-06-29 04:15:58 +02004package update
5
6import (
Lorenz Brun35fcf032023-06-29 04:15:58 +02007 "bytes"
8 "context"
Lorenz Brund79881d2023-11-30 19:02:06 +01009 "crypto/sha256"
Lorenz Brund14be0e2023-07-31 16:46:14 +020010 "debug/pe"
Lorenz Brund79881d2023-11-30 19:02:06 +010011 _ "embed"
Lorenz Brun35fcf032023-06-29 04:15:58 +020012 "errors"
13 "fmt"
14 "io"
Lorenz Brun35fcf032023-06-29 04:15:58 +020015 "os"
16 "path/filepath"
17 "regexp"
18 "strconv"
Lorenz Brund14be0e2023-07-31 16:46:14 +020019 "strings"
Jan Schär62cecde2025-04-16 15:24:04 +000020 "time"
Lorenz Brun35fcf032023-06-29 04:15:58 +020021
22 "github.com/cenkalti/backoff/v4"
Lorenz Brund14be0e2023-07-31 16:46:14 +020023 "golang.org/x/sys/unix"
Lorenz Brun35fcf032023-06-29 04:15:58 +020024 "google.golang.org/grpc/codes"
25 "google.golang.org/grpc/status"
Lorenz Brun54a5a052023-10-02 16:40:11 +020026 "google.golang.org/protobuf/proto"
Lorenz Brun35fcf032023-06-29 04:15:58 +020027
Serge Bazanski3c5d0632024-09-12 10:49:12 +000028 "source.monogon.dev/go/logging"
Jan Schäre19d2792025-06-23 12:37:58 +000029 "source.monogon.dev/metropolis/installer/install"
Jan Schärb86917b2025-05-14 16:31:08 +000030 "source.monogon.dev/metropolis/node/core/productinfo"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020031 "source.monogon.dev/osbase/blockdev"
32 "source.monogon.dev/osbase/efivarfs"
33 "source.monogon.dev/osbase/gpt"
34 "source.monogon.dev/osbase/kexec"
Jan Schäre19d2792025-06-23 12:37:58 +000035 "source.monogon.dev/osbase/oci/osimage"
Jan Schär62cecde2025-04-16 15:24:04 +000036 "source.monogon.dev/osbase/oci/registry"
Jan Schär62cecde2025-04-16 15:24:04 +000037
Jan Schär69b76872025-05-14 16:39:47 +000038 abloaderpb "source.monogon.dev/metropolis/node/abloader/spec"
Jan Schär62cecde2025-04-16 15:24:04 +000039 apb "source.monogon.dev/metropolis/proto/api"
Lorenz Brun35fcf032023-06-29 04:15:58 +020040)
41
42// Service contains data and functionality to perform A/B updates on a
43// Metropolis node.
44type Service struct {
45 // Path to the mount point of the EFI System Partition (ESP).
46 ESPPath string
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000047 // gpt.Partition of the ESP System Partition.
48 ESPPart *gpt.Partition
Lorenz Brun35fcf032023-06-29 04:15:58 +020049 // Partition number (1-based) of the ESP in the GPT partitions array.
50 ESPPartNumber uint32
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000051
Lorenz Brun35fcf032023-06-29 04:15:58 +020052 // Logger service for the update service.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000053 Logger logging.Leveled
Lorenz Brun35fcf032023-06-29 04:15:58 +020054}
55
56type Slot int
57
58const (
59 SlotInvalid Slot = 0
60 SlotA Slot = 1
61 SlotB Slot = 2
62)
63
64// Other returns the "other" slot, i.e. returns slot A for B and B for A.
65// It returns SlotInvalid for any s which is not SlotA or SlotB.
66func (s Slot) Other() Slot {
67 switch s {
68 case SlotA:
69 return SlotB
70 case SlotB:
71 return SlotA
72 default:
73 return SlotInvalid
74 }
75}
76
77func (s Slot) String() string {
78 switch s {
79 case SlotA:
80 return "A"
81 case SlotB:
82 return "B"
83 default:
84 return "<invalid slot>"
85 }
86}
87
88func (s Slot) EFIBootPath() string {
89 switch s {
90 case SlotA:
Jan Schäre19d2792025-06-23 12:37:58 +000091 return install.EFIBootAPath
Lorenz Brun35fcf032023-06-29 04:15:58 +020092 case SlotB:
Jan Schäre19d2792025-06-23 12:37:58 +000093 return install.EFIBootBPath
Lorenz Brun35fcf032023-06-29 04:15:58 +020094 default:
95 return ""
96 }
97}
98
99var slotRegexp = regexp.MustCompile(`PARTLABEL=METROPOLIS-SYSTEM-([AB])`)
100
101// ProvideESP is a convenience function for providing information about the
102// ESP after the update service has been instantiated.
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000103func (s *Service) ProvideESP(path string, partNum uint32, part *gpt.Partition) {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200104 s.ESPPath = path
105 s.ESPPartNumber = partNum
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000106 s.ESPPart = part
Lorenz Brun35fcf032023-06-29 04:15:58 +0200107}
108
109// CurrentlyRunningSlot returns the slot the current system is booted from.
110func (s *Service) CurrentlyRunningSlot() Slot {
111 cmdline, err := os.ReadFile("/proc/cmdline")
112 if err != nil {
113 return SlotInvalid
114 }
115 slotMatches := slotRegexp.FindStringSubmatch(string(cmdline))
116 if len(slotMatches) != 2 {
117 return SlotInvalid
118 }
119 switch slotMatches[1] {
120 case "A":
121 return SlotA
122 case "B":
123 return SlotB
124 default:
125 panic("unreachable")
126 }
127}
128
129var bootVarRegexp = regexp.MustCompile(`^Boot([0-9A-Fa-f]{4})$`)
130
Lorenz Brun35fcf032023-06-29 04:15:58 +0200131// MarkBootSuccessful must be called after each boot if some implementation-
132// defined criteria for a successful boot are met. If an update has been
133// installed and booted and this function is called, the updated version is
134// marked as default. If an issue occurs during boot and so this function is
135// not called the old version will be started again on next boot.
136func (s *Service) MarkBootSuccessful() error {
137 if s.ESPPath == "" {
138 return errors.New("no ESP information provided to update service, cannot continue")
139 }
Lorenz Brund79881d2023-11-30 19:02:06 +0100140 if err := s.fixupEFI(); err != nil {
141 s.Logger.Errorf("Error when checking boot entry configuration: %v", err)
142 }
143 if err := s.fixupPreloader(); err != nil {
144 s.Logger.Errorf("Error when fixing A/B preloader: %v", err)
145 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200146 activeSlot := s.CurrentlyRunningSlot()
Lorenz Brun54a5a052023-10-02 16:40:11 +0200147 abState, err := s.getABState()
Lorenz Brun35fcf032023-06-29 04:15:58 +0200148 if err != nil {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200149 s.Logger.Warningf("Error while getting A/B loader state, recreating: %v", err)
150 abState = &abloaderpb.ABLoaderData{
151 ActiveSlot: abloaderpb.Slot(activeSlot),
Lorenz Brun35fcf032023-06-29 04:15:58 +0200152 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200153 err := s.setABState(abState)
154 if err != nil {
155 return fmt.Errorf("while recreating A/B loader state: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200156 }
157 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200158 if Slot(abState.ActiveSlot) != activeSlot {
159 err := s.setABState(&abloaderpb.ABLoaderData{
160 ActiveSlot: abloaderpb.Slot(activeSlot),
161 })
162 if err != nil {
163 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200164 }
165 s.Logger.Infof("Permanently activated slot %v", activeSlot)
166 } else {
167 s.Logger.Infof("Normal boot from slot %v", activeSlot)
168 }
169
170 return nil
171}
172
Lorenz Brunca6da6a2024-09-09 17:55:15 +0200173// Rollback sets the currently-inactive slot as the next boot slot. This is
174// intended to recover from scenarios where roll-forward fixing is difficult.
175// Only the next boot slot is set to make sure that the node is not
176// made unbootable accidentally. On successful bootup that code can switch the
177// active slot to itself.
178func (s *Service) Rollback() error {
179 if s.ESPPath == "" {
180 return errors.New("no ESP information provided to update service, cannot continue")
181 }
182 activeSlot := s.CurrentlyRunningSlot()
183 abState, err := s.getABState()
184 if err != nil {
185 return fmt.Errorf("no valid A/B loader state, cannot rollback: %w", err)
186 }
187 nextSlot := activeSlot.Other()
188 err = s.setABState(&abloaderpb.ABLoaderData{
189 ActiveSlot: abState.ActiveSlot,
190 NextSlot: abloaderpb.Slot(nextSlot),
191 })
192 if err != nil {
193 return fmt.Errorf("while setting next A/B slot: %w", err)
194 }
195 s.Logger.Warningf("Rollback requested, NextSlot set to %v", nextSlot)
196 return nil
197}
198
Lorenz Brun1640c282024-09-09 17:50:48 +0200199// KexecLoadNext loads the slot to be booted next into the kexec staging area.
200// The next slot can then be launched by executing kexec via the reboot
201// syscall. Calling this function counts as a next boot for the purposes of
202// A/B state tracking, so it should not be called without kexecing afterwards.
203func (s *Service) KexecLoadNext() error {
204 state, err := s.getABState()
205 if err != nil {
206 return fmt.Errorf("bad A/B state: %w", err)
207 }
208 slotToLoad := Slot(state.ActiveSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100209 if state.NextSlot != abloaderpb.Slot_SLOT_NONE {
Lorenz Brun1640c282024-09-09 17:50:48 +0200210 slotToLoad = Slot(state.NextSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100211 state.NextSlot = abloaderpb.Slot_SLOT_NONE
Lorenz Brun1640c282024-09-09 17:50:48 +0200212 err = s.setABState(state)
213 if err != nil {
214 return fmt.Errorf("while updating A/B state: %w", err)
215 }
216 }
217 boot, err := os.Open(filepath.Join(s.ESPPath, slotToLoad.EFIBootPath()))
218 if err != nil {
219 return fmt.Errorf("failed to open boot file for slot %v: %w", slotToLoad, err)
220 }
221 defer boot.Close()
222 if err := s.stageKexec(boot, slotToLoad); err != nil {
223 return fmt.Errorf("failed to stage next slot for kexec: %w", err)
224 }
225 return nil
226}
227
Lorenz Brun35fcf032023-06-29 04:15:58 +0200228func openSystemSlot(slot Slot) (*blockdev.Device, error) {
229 switch slot {
230 case SlotA:
231 return blockdev.Open("/dev/system-a")
232 case SlotB:
233 return blockdev.Open("/dev/system-b")
234 default:
235 return nil, errors.New("invalid slot identifier given")
236 }
237}
238
Lorenz Brun54a5a052023-10-02 16:40:11 +0200239func (s *Service) getABState() (*abloaderpb.ABLoaderData, error) {
240 abDataRaw, err := os.ReadFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"))
241 if err != nil {
242 return nil, err
243 }
244 var abData abloaderpb.ABLoaderData
245 if err := proto.Unmarshal(abDataRaw, &abData); err != nil {
246 return nil, err
247 }
248 return &abData, nil
249}
250
251func (s *Service) setABState(d *abloaderpb.ABLoaderData) error {
252 abDataRaw, err := proto.Marshal(d)
253 if err != nil {
254 return fmt.Errorf("while marshaling: %w", err)
255 }
256 if err := os.WriteFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"), abDataRaw, 0666); err != nil {
257 return err
258 }
259 return nil
260}
261
Jan Schär62cecde2025-04-16 15:24:04 +0000262// InstallImage fetches the given image, installs it into the currently inactive
263// slot and sets that slot to boot next. If it doesn't return an error, a reboot
264// boots into the new slot.
265func (s *Service) InstallImage(ctx context.Context, imageRef *apb.OSImageRef, withKexec bool) error {
266 if imageRef == nil {
267 return fmt.Errorf("missing OS image in OS installation request")
268 }
269 if imageRef.Digest == "" {
270 return fmt.Errorf("missing digest in OS installation request")
271 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200272 if s.ESPPath == "" {
273 return errors.New("no ESP information provided to update service, cannot continue")
274 }
Jan Schär62cecde2025-04-16 15:24:04 +0000275
276 downloadCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
277 defer cancel()
278
279 client := &registry.Client{
280 GetBackOff: func() backoff.BackOff {
281 return backoff.NewExponentialBackOff()
282 },
283 RetryNotify: func(err error, d time.Duration) {
284 s.Logger.Warningf("Error while fetching OS image, retrying in %v: %v", d, err)
285 },
Jan Schärb86917b2025-05-14 16:31:08 +0000286 UserAgent: "MonogonOS/" + productinfo.Get().VersionString,
Jan Schär62cecde2025-04-16 15:24:04 +0000287 Scheme: imageRef.Scheme,
288 Host: imageRef.Host,
289 Repository: imageRef.Repository,
Lorenz Brun35fcf032023-06-29 04:15:58 +0200290 }
Jan Schär62cecde2025-04-16 15:24:04 +0000291
292 image, err := client.Read(downloadCtx, imageRef.Tag, imageRef.Digest)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200293 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000294 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200295 }
Jan Schär62cecde2025-04-16 15:24:04 +0000296
Jan Schäre19d2792025-06-23 12:37:58 +0000297 osImage, err := osimage.Read(image)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200298 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000299 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200300 }
Jan Schär62cecde2025-04-16 15:24:04 +0000301
302 efiPayload, err := osImage.Payload("kernel.efi")
Lorenz Brun35fcf032023-06-29 04:15:58 +0200303 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000304 return fmt.Errorf("cannot open EFI payload in OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200305 }
Jan Schär62cecde2025-04-16 15:24:04 +0000306 systemImage, err := osImage.Payload("system")
307 if err != nil {
308 return fmt.Errorf("cannot open system image in OS image: %w", err)
309 }
310
Lorenz Brun35fcf032023-06-29 04:15:58 +0200311 activeSlot := s.CurrentlyRunningSlot()
312 if activeSlot == SlotInvalid {
313 return errors.New("unable to determine active slot, cannot continue")
314 }
315 targetSlot := activeSlot.Other()
316
Lorenz Brun35fcf032023-06-29 04:15:58 +0200317 systemPart, err := openSystemSlot(targetSlot)
318 if err != nil {
319 return status.Errorf(codes.Internal, "Inactive system slot unavailable: %v", err)
320 }
Jan Schär62cecde2025-04-16 15:24:04 +0000321 systemImageContent, err := systemImage.Open()
322 if err != nil {
323 systemPart.Close()
324 return fmt.Errorf("failed to open system image: %w", err)
325 }
326 _, err = io.Copy(blockdev.NewRWS(systemPart), systemImageContent)
327 systemImageContent.Close()
328 closeErr := systemPart.Close()
329 if err == nil {
330 err = closeErr
331 }
332 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200333 return status.Errorf(codes.Unavailable, "Failed to copy system image: %v", err)
334 }
335
336 bootFile, err := os.Create(filepath.Join(s.ESPPath, targetSlot.EFIBootPath()))
337 if err != nil {
338 return fmt.Errorf("failed to open boot file: %w", err)
339 }
340 defer bootFile.Close()
Jan Schär62cecde2025-04-16 15:24:04 +0000341 efiPayloadContent, err := efiPayload.Open()
342 if err != nil {
343 return fmt.Errorf("failed to open EFI payload: %w", err)
344 }
345 _, err = io.Copy(bootFile, efiPayloadContent)
346 efiPayloadContent.Close()
347 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200348 return fmt.Errorf("failed to write boot file: %w", err)
349 }
350
Lorenz Brund14be0e2023-07-31 16:46:14 +0200351 if withKexec {
352 if err := s.stageKexec(bootFile, targetSlot); err != nil {
353 return fmt.Errorf("while kexec staging: %w", err)
354 }
355 } else {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200356 err := s.setABState(&abloaderpb.ABLoaderData{
357 ActiveSlot: abloaderpb.Slot(activeSlot),
358 NextSlot: abloaderpb.Slot(targetSlot),
359 })
360 if err != nil {
361 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brund14be0e2023-07-31 16:46:14 +0200362 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200363 }
364
365 return nil
366}
367
Lorenz Brund14be0e2023-07-31 16:46:14 +0200368// newMemfile creates a new file which is not located on a specific filesystem,
369// but is instead backed by anonymous memory.
370func newMemfile(name string, flags int) (*os.File, error) {
371 fd, err := unix.MemfdCreate(name, flags)
372 if err != nil {
373 return nil, fmt.Errorf("memfd_create: %w", err)
374 }
375 return os.NewFile(uintptr(fd), name), nil
376}
377
378// stageKexec stages the kernel, command line and initramfs if available for
379// a future kexec. It extracts the relevant data from the EFI boot executable.
380func (s *Service) stageKexec(bootFile io.ReaderAt, targetSlot Slot) error {
381 bootPE, err := pe.NewFile(bootFile)
382 if err != nil {
383 return fmt.Errorf("unable to open bootFile as PE: %w", err)
384 }
385 var cmdlineRaw []byte
386 cmdlineSection := bootPE.Section(".cmdline")
387 if cmdlineSection == nil {
388 return fmt.Errorf("no .cmdline section in boot PE")
389 }
390 cmdlineRaw, err = cmdlineSection.Data()
391 if err != nil {
392 return fmt.Errorf("while reading .cmdline PE section: %w", err)
393 }
394 cmdline := string(bytes.TrimRight(cmdlineRaw, "\x00"))
395 cmdline = strings.ReplaceAll(cmdline, "METROPOLIS-SYSTEM-X", fmt.Sprintf("METROPOLIS-SYSTEM-%s", targetSlot))
396 kernelFile, err := newMemfile("kernel", 0)
397 if err != nil {
398 return fmt.Errorf("failed to create kernel memfile: %w", err)
399 }
400 defer kernelFile.Close()
401 kernelSection := bootPE.Section(".linux")
402 if kernelSection == nil {
403 return fmt.Errorf("no .linux section in boot PE")
404 }
405 if _, err := io.Copy(kernelFile, kernelSection.Open()); err != nil {
406 return fmt.Errorf("while copying .linux PE section: %w", err)
407 }
408
409 initramfsSection := bootPE.Section(".initrd")
410 var initramfsFile *os.File
411 if initramfsSection != nil && initramfsSection.Size > 0 {
412 initramfsFile, err = newMemfile("initramfs", 0)
413 if err != nil {
414 return fmt.Errorf("failed to create initramfs memfile: %w", err)
415 }
416 defer initramfsFile.Close()
417 if _, err := io.Copy(initramfsFile, initramfsSection.Open()); err != nil {
418 return fmt.Errorf("while copying .initrd PE section: %w", err)
419 }
420 }
421 if err := kexec.FileLoad(kernelFile, initramfsFile, cmdline); err != nil {
422 return fmt.Errorf("while staging new kexec kernel: %w", err)
423 }
424 return nil
425}
Lorenz Brund79881d2023-11-30 19:02:06 +0100426
Jan Schär69b76872025-05-14 16:39:47 +0000427//go:embed metropolis/node/abloader/abloader_bin.efi
Lorenz Brund79881d2023-11-30 19:02:06 +0100428var abloader []byte
429
430func (s *Service) fixupPreloader() error {
Jan Schäre19d2792025-06-23 12:37:58 +0000431 efiBootPath, err := install.EFIBootPath(productinfo.Get().Info.Architecture())
Jan Schär4b888262025-05-13 09:12:03 +0000432 if err != nil {
433 return err
434 }
435 efiBootFilePath := filepath.Join(s.ESPPath, efiBootPath)
436 abLoaderFile, err := os.Open(efiBootFilePath)
Lorenz Brund79881d2023-11-30 19:02:06 +0100437 if err != nil {
438 s.Logger.Warningf("A/B preloader not available, attempting to restore: %v", err)
439 } else {
440 expectedSum := sha256.Sum256(abloader)
441 h := sha256.New()
442 _, err := io.Copy(h, abLoaderFile)
443 abLoaderFile.Close()
444 if err == nil {
445 if bytes.Equal(h.Sum(nil), expectedSum[:]) {
446 // A/B Preloader is present and has correct hash
447 return nil
448 } else {
449 s.Logger.Infof("Replacing A/B preloader with current version: %x %x", h.Sum(nil), expectedSum[:])
450 }
451 } else {
452 s.Logger.Warningf("Error while reading A/B preloader, restoring: %v", err)
453 }
454 }
455 preloader, err := os.Create(filepath.Join(s.ESPPath, "preloader.swp"))
456 if err != nil {
457 return fmt.Errorf("while creating preloader swap file: %w", err)
458 }
459 if _, err := preloader.Write(abloader); err != nil {
460 return fmt.Errorf("while writing preloader swap file: %w", err)
461 }
462 if err := preloader.Sync(); err != nil {
463 return fmt.Errorf("while sync'ing preloader swap file: %w", err)
464 }
465 preloader.Close()
Jan Schär4b888262025-05-13 09:12:03 +0000466 if err := os.Rename(filepath.Join(s.ESPPath, "preloader.swp"), efiBootFilePath); err != nil {
Lorenz Brund79881d2023-11-30 19:02:06 +0100467 return fmt.Errorf("while swapping preloader: %w", err)
468 }
469 s.Logger.Info("Successfully wrote current preloader")
470 return nil
471}
472
473// fixupEFI checks for the existence and correctness of the EFI boot entry
474// repairs/recreates it if needed.
475func (s *Service) fixupEFI() error {
Jan Schäre19d2792025-06-23 12:37:58 +0000476 efiBootPath, err := install.EFIBootPath(productinfo.Get().Info.Architecture())
Jan Schär4b888262025-05-13 09:12:03 +0000477 if err != nil {
478 return err
479 }
480 efiBootVarPath := "/" + efiBootPath
Lorenz Brund79881d2023-11-30 19:02:06 +0100481 varNames, err := efivarfs.List(efivarfs.ScopeGlobal)
482 if err != nil {
483 return fmt.Errorf("failed to list EFI variables: %w", err)
484 }
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200485 var validBootEntryIdx = -1
Lorenz Brund79881d2023-11-30 19:02:06 +0100486 for _, varName := range varNames {
487 m := bootVarRegexp.FindStringSubmatch(varName)
488 if m == nil {
489 continue
490 }
491 idx, err := strconv.ParseUint(m[1], 16, 16)
492 if err != nil {
493 // This cannot be hit as all regexp matches are parseable.
494 panic(err)
495 }
496 e, err := efivarfs.GetBootEntry(int(idx))
497 if err != nil {
498 s.Logger.Warningf("Unable to get boot entry %d, skipping: %v", idx, err)
499 continue
500 }
501 if len(e.FilePath) != 2 {
502 // Not our entry, ours always have two parts
503 continue
504 }
505 switch p := e.FilePath[0].(type) {
506 case *efivarfs.HardDrivePath:
507 gptMatch, ok := p.PartitionMatch.(*efivarfs.PartitionGPT)
508 if ok && gptMatch.PartitionUUID != s.ESPPart.ID {
509 // Not related to our ESP
510 continue
511 }
512 default:
513 continue
514 }
515 switch p := e.FilePath[1].(type) {
516 case efivarfs.FilePath:
Jan Schär4b888262025-05-13 09:12:03 +0000517 if string(p) == efiBootVarPath {
Lorenz Brund79881d2023-11-30 19:02:06 +0100518 if validBootEntryIdx == -1 {
519 validBootEntryIdx = int(idx)
520 } else {
521 // Another valid boot entry already exists, delete this one
522 err := efivarfs.DeleteBootEntry(int(idx))
523 if err == nil {
524 s.Logger.Infof("Deleted duplicate boot entry %q", e.Description)
525 } else {
526 s.Logger.Warningf("Error while deleting duplicate boot entry %q: %v", e.Description, err)
527 }
528 }
529 } else if strings.Contains(e.Description, "Metropolis") {
530 err := efivarfs.DeleteBootEntry(int(idx))
531 if err == nil {
532 s.Logger.Infof("Deleted orphaned boot entry %q", e.Description)
533 } else {
534 s.Logger.Warningf("Error while deleting orphaned boot entry %q: %v", e.Description, err)
535 }
536 }
537 default:
538 continue
539 }
540 }
541 if validBootEntryIdx == -1 {
542 validBootEntryIdx, err = efivarfs.AddBootEntry(&efivarfs.LoadOption{
543 Description: "Metropolis",
544 FilePath: efivarfs.DevicePath{
545 &efivarfs.HardDrivePath{
546 PartitionNumber: 1,
547 PartitionStartBlock: s.ESPPart.FirstBlock,
548 PartitionSizeBlocks: s.ESPPart.SizeBlocks(),
549 PartitionMatch: efivarfs.PartitionGPT{
550 PartitionUUID: s.ESPPart.ID,
551 },
552 },
Jan Schär4b888262025-05-13 09:12:03 +0000553 efivarfs.FilePath(efiBootVarPath),
Lorenz Brund79881d2023-11-30 19:02:06 +0100554 },
555 })
556 if err == nil {
557 s.Logger.Infof("Restored missing EFI boot entry for Metropolis")
558 } else {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200559 return fmt.Errorf("while restoring missing EFI boot entry for Metropolis: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100560 }
561 }
562 bootOrder, err := efivarfs.GetBootOrder()
563 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200564 return fmt.Errorf("failed to get EFI boot order: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100565 }
566 for _, bentry := range bootOrder {
567 if bentry == uint16(validBootEntryIdx) {
568 // Our boot entry is in the boot order, everything's ok
569 return nil
570 }
571 }
572 newBootOrder := append(efivarfs.BootOrder{uint16(validBootEntryIdx)}, bootOrder...)
573 if err := efivarfs.SetBootOrder(newBootOrder); err != nil {
574 return fmt.Errorf("while setting EFI boot order: %w", err)
575 }
576 return nil
577}