blob: 0dbc485cd0f1443edb08a043dd6277ecdda1db03 [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 (
7 "archive/zip"
8 "bytes"
9 "context"
Lorenz Brund79881d2023-11-30 19:02:06 +010010 "crypto/sha256"
Lorenz Brund14be0e2023-07-31 16:46:14 +020011 "debug/pe"
Lorenz Brund79881d2023-11-30 19:02:06 +010012 _ "embed"
Lorenz Brun35fcf032023-06-29 04:15:58 +020013 "errors"
14 "fmt"
15 "io"
16 "net/http"
17 "os"
18 "path/filepath"
19 "regexp"
20 "strconv"
Lorenz Brund14be0e2023-07-31 16:46:14 +020021 "strings"
Lorenz Brun35fcf032023-06-29 04:15:58 +020022
23 "github.com/cenkalti/backoff/v4"
Lorenz Brund14be0e2023-07-31 16:46:14 +020024 "golang.org/x/sys/unix"
Lorenz Brun35fcf032023-06-29 04:15:58 +020025 "google.golang.org/grpc/codes"
26 "google.golang.org/grpc/status"
Lorenz Brun54a5a052023-10-02 16:40:11 +020027 "google.golang.org/protobuf/proto"
Lorenz Brun35fcf032023-06-29 04:15:58 +020028
Serge Bazanski3c5d0632024-09-12 10:49:12 +000029 "source.monogon.dev/go/logging"
Lorenz Brun54a5a052023-10-02 16:40:11 +020030 abloaderpb "source.monogon.dev/metropolis/node/core/abloader/spec"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020031 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020032 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020033 "source.monogon.dev/osbase/efivarfs"
34 "source.monogon.dev/osbase/gpt"
35 "source.monogon.dev/osbase/kexec"
Lorenz Brun35fcf032023-06-29 04:15:58 +020036)
37
38// Service contains data and functionality to perform A/B updates on a
39// Metropolis node.
40type Service struct {
41 // Path to the mount point of the EFI System Partition (ESP).
42 ESPPath string
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000043 // gpt.Partition of the ESP System Partition.
44 ESPPart *gpt.Partition
Lorenz Brun35fcf032023-06-29 04:15:58 +020045 // Partition number (1-based) of the ESP in the GPT partitions array.
46 ESPPartNumber uint32
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000047
Lorenz Brun35fcf032023-06-29 04:15:58 +020048 // Logger service for the update service.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000049 Logger logging.Leveled
Lorenz Brun35fcf032023-06-29 04:15:58 +020050}
51
52type Slot int
53
54const (
55 SlotInvalid Slot = 0
56 SlotA Slot = 1
57 SlotB Slot = 2
58)
59
60// Other returns the "other" slot, i.e. returns slot A for B and B for A.
61// It returns SlotInvalid for any s which is not SlotA or SlotB.
62func (s Slot) Other() Slot {
63 switch s {
64 case SlotA:
65 return SlotB
66 case SlotB:
67 return SlotA
68 default:
69 return SlotInvalid
70 }
71}
72
73func (s Slot) String() string {
74 switch s {
75 case SlotA:
76 return "A"
77 case SlotB:
78 return "B"
79 default:
80 return "<invalid slot>"
81 }
82}
83
84func (s Slot) EFIBootPath() string {
85 switch s {
86 case SlotA:
87 return osimage.EFIBootAPath
88 case SlotB:
89 return osimage.EFIBootBPath
90 default:
91 return ""
92 }
93}
94
95var slotRegexp = regexp.MustCompile(`PARTLABEL=METROPOLIS-SYSTEM-([AB])`)
96
97// ProvideESP is a convenience function for providing information about the
98// ESP after the update service has been instantiated.
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000099func (s *Service) ProvideESP(path string, partNum uint32, part *gpt.Partition) {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200100 s.ESPPath = path
101 s.ESPPartNumber = partNum
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000102 s.ESPPart = part
Lorenz Brun35fcf032023-06-29 04:15:58 +0200103}
104
105// CurrentlyRunningSlot returns the slot the current system is booted from.
106func (s *Service) CurrentlyRunningSlot() Slot {
107 cmdline, err := os.ReadFile("/proc/cmdline")
108 if err != nil {
109 return SlotInvalid
110 }
111 slotMatches := slotRegexp.FindStringSubmatch(string(cmdline))
112 if len(slotMatches) != 2 {
113 return SlotInvalid
114 }
115 switch slotMatches[1] {
116 case "A":
117 return SlotA
118 case "B":
119 return SlotB
120 default:
121 panic("unreachable")
122 }
123}
124
125var bootVarRegexp = regexp.MustCompile(`^Boot([0-9A-Fa-f]{4})$`)
126
Lorenz Brun35fcf032023-06-29 04:15:58 +0200127// MarkBootSuccessful must be called after each boot if some implementation-
128// defined criteria for a successful boot are met. If an update has been
129// installed and booted and this function is called, the updated version is
130// marked as default. If an issue occurs during boot and so this function is
131// not called the old version will be started again on next boot.
132func (s *Service) MarkBootSuccessful() error {
133 if s.ESPPath == "" {
134 return errors.New("no ESP information provided to update service, cannot continue")
135 }
Lorenz Brund79881d2023-11-30 19:02:06 +0100136 if err := s.fixupEFI(); err != nil {
137 s.Logger.Errorf("Error when checking boot entry configuration: %v", err)
138 }
139 if err := s.fixupPreloader(); err != nil {
140 s.Logger.Errorf("Error when fixing A/B preloader: %v", err)
141 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200142 activeSlot := s.CurrentlyRunningSlot()
Lorenz Brun54a5a052023-10-02 16:40:11 +0200143 abState, err := s.getABState()
Lorenz Brun35fcf032023-06-29 04:15:58 +0200144 if err != nil {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200145 s.Logger.Warningf("Error while getting A/B loader state, recreating: %v", err)
146 abState = &abloaderpb.ABLoaderData{
147 ActiveSlot: abloaderpb.Slot(activeSlot),
Lorenz Brun35fcf032023-06-29 04:15:58 +0200148 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200149 err := s.setABState(abState)
150 if err != nil {
151 return fmt.Errorf("while recreating A/B loader state: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200152 }
153 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200154 if Slot(abState.ActiveSlot) != activeSlot {
155 err := s.setABState(&abloaderpb.ABLoaderData{
156 ActiveSlot: abloaderpb.Slot(activeSlot),
157 })
158 if err != nil {
159 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200160 }
161 s.Logger.Infof("Permanently activated slot %v", activeSlot)
162 } else {
163 s.Logger.Infof("Normal boot from slot %v", activeSlot)
164 }
165
166 return nil
167}
168
Lorenz Brunca6da6a2024-09-09 17:55:15 +0200169// Rollback sets the currently-inactive slot as the next boot slot. This is
170// intended to recover from scenarios where roll-forward fixing is difficult.
171// Only the next boot slot is set to make sure that the node is not
172// made unbootable accidentally. On successful bootup that code can switch the
173// active slot to itself.
174func (s *Service) Rollback() error {
175 if s.ESPPath == "" {
176 return errors.New("no ESP information provided to update service, cannot continue")
177 }
178 activeSlot := s.CurrentlyRunningSlot()
179 abState, err := s.getABState()
180 if err != nil {
181 return fmt.Errorf("no valid A/B loader state, cannot rollback: %w", err)
182 }
183 nextSlot := activeSlot.Other()
184 err = s.setABState(&abloaderpb.ABLoaderData{
185 ActiveSlot: abState.ActiveSlot,
186 NextSlot: abloaderpb.Slot(nextSlot),
187 })
188 if err != nil {
189 return fmt.Errorf("while setting next A/B slot: %w", err)
190 }
191 s.Logger.Warningf("Rollback requested, NextSlot set to %v", nextSlot)
192 return nil
193}
194
Lorenz Brun1640c282024-09-09 17:50:48 +0200195// KexecLoadNext loads the slot to be booted next into the kexec staging area.
196// The next slot can then be launched by executing kexec via the reboot
197// syscall. Calling this function counts as a next boot for the purposes of
198// A/B state tracking, so it should not be called without kexecing afterwards.
199func (s *Service) KexecLoadNext() error {
200 state, err := s.getABState()
201 if err != nil {
202 return fmt.Errorf("bad A/B state: %w", err)
203 }
204 slotToLoad := Slot(state.ActiveSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100205 if state.NextSlot != abloaderpb.Slot_SLOT_NONE {
Lorenz Brun1640c282024-09-09 17:50:48 +0200206 slotToLoad = Slot(state.NextSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100207 state.NextSlot = abloaderpb.Slot_SLOT_NONE
Lorenz Brun1640c282024-09-09 17:50:48 +0200208 err = s.setABState(state)
209 if err != nil {
210 return fmt.Errorf("while updating A/B state: %w", err)
211 }
212 }
213 boot, err := os.Open(filepath.Join(s.ESPPath, slotToLoad.EFIBootPath()))
214 if err != nil {
215 return fmt.Errorf("failed to open boot file for slot %v: %w", slotToLoad, err)
216 }
217 defer boot.Close()
218 if err := s.stageKexec(boot, slotToLoad); err != nil {
219 return fmt.Errorf("failed to stage next slot for kexec: %w", err)
220 }
221 return nil
222}
223
Lorenz Brun35fcf032023-06-29 04:15:58 +0200224func openSystemSlot(slot Slot) (*blockdev.Device, error) {
225 switch slot {
226 case SlotA:
227 return blockdev.Open("/dev/system-a")
228 case SlotB:
229 return blockdev.Open("/dev/system-b")
230 default:
231 return nil, errors.New("invalid slot identifier given")
232 }
233}
234
Lorenz Brun54a5a052023-10-02 16:40:11 +0200235func (s *Service) getABState() (*abloaderpb.ABLoaderData, error) {
236 abDataRaw, err := os.ReadFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"))
237 if err != nil {
238 return nil, err
239 }
240 var abData abloaderpb.ABLoaderData
241 if err := proto.Unmarshal(abDataRaw, &abData); err != nil {
242 return nil, err
243 }
244 return &abData, nil
245}
246
247func (s *Service) setABState(d *abloaderpb.ABLoaderData) error {
248 abDataRaw, err := proto.Marshal(d)
249 if err != nil {
250 return fmt.Errorf("while marshaling: %w", err)
251 }
252 if err := os.WriteFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"), abDataRaw, 0666); err != nil {
253 return err
254 }
255 return nil
256}
257
Lorenz Brun35fcf032023-06-29 04:15:58 +0200258// InstallBundle installs the bundle at the given HTTP(S) URL into the currently
259// inactive slot and sets that slot to boot next. If it doesn't return an error,
260// a reboot boots into the new slot.
Lorenz Brund14be0e2023-07-31 16:46:14 +0200261func (s *Service) InstallBundle(ctx context.Context, bundleURL string, withKexec bool) error {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200262 if s.ESPPath == "" {
263 return errors.New("no ESP information provided to update service, cannot continue")
264 }
265 // Download into a buffer as ZIP files cannot efficiently be read from
266 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
267 // thus a ton of small range requests would need to be used, causing
268 // a huge latency penalty as well as costing a lot of money on typical
269 // object storages. This should go away when we switch to a better bundle
270 // format which can be streamed.
271 var bundleRaw bytes.Buffer
272 b := backoff.NewExponentialBackOff()
273 err := backoff.Retry(func() error {
274 return s.tryDownloadBundle(ctx, bundleURL, &bundleRaw)
275 }, backoff.WithContext(b, ctx))
276 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200277 return fmt.Errorf("error downloading Metropolis bundle: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200278 }
279 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
280 if err != nil {
281 return fmt.Errorf("failed to open node bundle: %w", err)
282 }
283 efiPayload, err := bundle.Open("kernel_efi.efi")
284 if err != nil {
285 return fmt.Errorf("invalid bundle: %w", err)
286 }
287 defer efiPayload.Close()
288 systemImage, err := bundle.Open("verity_rootfs.img")
289 if err != nil {
290 return fmt.Errorf("invalid bundle: %w", err)
291 }
292 defer systemImage.Close()
293 activeSlot := s.CurrentlyRunningSlot()
294 if activeSlot == SlotInvalid {
295 return errors.New("unable to determine active slot, cannot continue")
296 }
297 targetSlot := activeSlot.Other()
298
Lorenz Brun35fcf032023-06-29 04:15:58 +0200299 systemPart, err := openSystemSlot(targetSlot)
300 if err != nil {
301 return status.Errorf(codes.Internal, "Inactive system slot unavailable: %v", err)
302 }
303 defer systemPart.Close()
304 if _, err := io.Copy(blockdev.NewRWS(systemPart), systemImage); err != nil {
305 return status.Errorf(codes.Unavailable, "Failed to copy system image: %v", err)
306 }
307
308 bootFile, err := os.Create(filepath.Join(s.ESPPath, targetSlot.EFIBootPath()))
309 if err != nil {
310 return fmt.Errorf("failed to open boot file: %w", err)
311 }
312 defer bootFile.Close()
313 if _, err := io.Copy(bootFile, efiPayload); err != nil {
314 return fmt.Errorf("failed to write boot file: %w", err)
315 }
316
Lorenz Brund14be0e2023-07-31 16:46:14 +0200317 if withKexec {
318 if err := s.stageKexec(bootFile, targetSlot); err != nil {
319 return fmt.Errorf("while kexec staging: %w", err)
320 }
321 } else {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200322 err := s.setABState(&abloaderpb.ABLoaderData{
323 ActiveSlot: abloaderpb.Slot(activeSlot),
324 NextSlot: abloaderpb.Slot(targetSlot),
325 })
326 if err != nil {
327 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brund14be0e2023-07-31 16:46:14 +0200328 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200329 }
330
331 return nil
332}
333
334func (*Service) tryDownloadBundle(ctx context.Context, bundleURL string, bundleRaw *bytes.Buffer) error {
335 bundleReq, err := http.NewRequestWithContext(ctx, "GET", bundleURL, nil)
Tim Windelschmidt096654a2024-04-18 23:10:19 +0200336 if err != nil {
337 return fmt.Errorf("failed to create request: %w", err)
338 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200339 bundleRes, err := http.DefaultClient.Do(bundleReq)
340 if err != nil {
341 return fmt.Errorf("HTTP request failed: %w", err)
342 }
343 defer bundleRes.Body.Close()
344 switch bundleRes.StatusCode {
345 case http.StatusTooEarly, http.StatusTooManyRequests,
346 http.StatusInternalServerError, http.StatusBadGateway,
347 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
348 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
349 default:
350 // Non-standard code range used for proxy-related issue by various
351 // vendors. Treat as non-permanent error.
352 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
353 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
354 }
355 if bundleRes.StatusCode != 200 {
356 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
357 }
358 }
359 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
360 bundleRaw.Reset()
361 return err
362 }
363 return nil
364}
Lorenz Brund14be0e2023-07-31 16:46:14 +0200365
366// newMemfile creates a new file which is not located on a specific filesystem,
367// but is instead backed by anonymous memory.
368func newMemfile(name string, flags int) (*os.File, error) {
369 fd, err := unix.MemfdCreate(name, flags)
370 if err != nil {
371 return nil, fmt.Errorf("memfd_create: %w", err)
372 }
373 return os.NewFile(uintptr(fd), name), nil
374}
375
376// stageKexec stages the kernel, command line and initramfs if available for
377// a future kexec. It extracts the relevant data from the EFI boot executable.
378func (s *Service) stageKexec(bootFile io.ReaderAt, targetSlot Slot) error {
379 bootPE, err := pe.NewFile(bootFile)
380 if err != nil {
381 return fmt.Errorf("unable to open bootFile as PE: %w", err)
382 }
383 var cmdlineRaw []byte
384 cmdlineSection := bootPE.Section(".cmdline")
385 if cmdlineSection == nil {
386 return fmt.Errorf("no .cmdline section in boot PE")
387 }
388 cmdlineRaw, err = cmdlineSection.Data()
389 if err != nil {
390 return fmt.Errorf("while reading .cmdline PE section: %w", err)
391 }
392 cmdline := string(bytes.TrimRight(cmdlineRaw, "\x00"))
393 cmdline = strings.ReplaceAll(cmdline, "METROPOLIS-SYSTEM-X", fmt.Sprintf("METROPOLIS-SYSTEM-%s", targetSlot))
394 kernelFile, err := newMemfile("kernel", 0)
395 if err != nil {
396 return fmt.Errorf("failed to create kernel memfile: %w", err)
397 }
398 defer kernelFile.Close()
399 kernelSection := bootPE.Section(".linux")
400 if kernelSection == nil {
401 return fmt.Errorf("no .linux section in boot PE")
402 }
403 if _, err := io.Copy(kernelFile, kernelSection.Open()); err != nil {
404 return fmt.Errorf("while copying .linux PE section: %w", err)
405 }
406
407 initramfsSection := bootPE.Section(".initrd")
408 var initramfsFile *os.File
409 if initramfsSection != nil && initramfsSection.Size > 0 {
410 initramfsFile, err = newMemfile("initramfs", 0)
411 if err != nil {
412 return fmt.Errorf("failed to create initramfs memfile: %w", err)
413 }
414 defer initramfsFile.Close()
415 if _, err := io.Copy(initramfsFile, initramfsSection.Open()); err != nil {
416 return fmt.Errorf("while copying .initrd PE section: %w", err)
417 }
418 }
419 if err := kexec.FileLoad(kernelFile, initramfsFile, cmdline); err != nil {
420 return fmt.Errorf("while staging new kexec kernel: %w", err)
421 }
422 return nil
423}
Lorenz Brund79881d2023-11-30 19:02:06 +0100424
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200425//go:embed metropolis/node/core/abloader/abloader.efi
Lorenz Brund79881d2023-11-30 19:02:06 +0100426var abloader []byte
427
428func (s *Service) fixupPreloader() error {
429 abLoaderFile, err := os.Open(filepath.Join(s.ESPPath, osimage.EFIPayloadPath))
430 if err != nil {
431 s.Logger.Warningf("A/B preloader not available, attempting to restore: %v", err)
432 } else {
433 expectedSum := sha256.Sum256(abloader)
434 h := sha256.New()
435 _, err := io.Copy(h, abLoaderFile)
436 abLoaderFile.Close()
437 if err == nil {
438 if bytes.Equal(h.Sum(nil), expectedSum[:]) {
439 // A/B Preloader is present and has correct hash
440 return nil
441 } else {
442 s.Logger.Infof("Replacing A/B preloader with current version: %x %x", h.Sum(nil), expectedSum[:])
443 }
444 } else {
445 s.Logger.Warningf("Error while reading A/B preloader, restoring: %v", err)
446 }
447 }
448 preloader, err := os.Create(filepath.Join(s.ESPPath, "preloader.swp"))
449 if err != nil {
450 return fmt.Errorf("while creating preloader swap file: %w", err)
451 }
452 if _, err := preloader.Write(abloader); err != nil {
453 return fmt.Errorf("while writing preloader swap file: %w", err)
454 }
455 if err := preloader.Sync(); err != nil {
456 return fmt.Errorf("while sync'ing preloader swap file: %w", err)
457 }
458 preloader.Close()
459 if err := os.Rename(filepath.Join(s.ESPPath, "preloader.swp"), filepath.Join(s.ESPPath, osimage.EFIPayloadPath)); err != nil {
460 return fmt.Errorf("while swapping preloader: %w", err)
461 }
462 s.Logger.Info("Successfully wrote current preloader")
463 return nil
464}
465
466// fixupEFI checks for the existence and correctness of the EFI boot entry
467// repairs/recreates it if needed.
468func (s *Service) fixupEFI() error {
469 varNames, err := efivarfs.List(efivarfs.ScopeGlobal)
470 if err != nil {
471 return fmt.Errorf("failed to list EFI variables: %w", err)
472 }
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200473 var validBootEntryIdx = -1
Lorenz Brund79881d2023-11-30 19:02:06 +0100474 for _, varName := range varNames {
475 m := bootVarRegexp.FindStringSubmatch(varName)
476 if m == nil {
477 continue
478 }
479 idx, err := strconv.ParseUint(m[1], 16, 16)
480 if err != nil {
481 // This cannot be hit as all regexp matches are parseable.
482 panic(err)
483 }
484 e, err := efivarfs.GetBootEntry(int(idx))
485 if err != nil {
486 s.Logger.Warningf("Unable to get boot entry %d, skipping: %v", idx, err)
487 continue
488 }
489 if len(e.FilePath) != 2 {
490 // Not our entry, ours always have two parts
491 continue
492 }
493 switch p := e.FilePath[0].(type) {
494 case *efivarfs.HardDrivePath:
495 gptMatch, ok := p.PartitionMatch.(*efivarfs.PartitionGPT)
496 if ok && gptMatch.PartitionUUID != s.ESPPart.ID {
497 // Not related to our ESP
498 continue
499 }
500 default:
501 continue
502 }
503 switch p := e.FilePath[1].(type) {
504 case efivarfs.FilePath:
505 if string(p) == osimage.EFIPayloadPath {
506 if validBootEntryIdx == -1 {
507 validBootEntryIdx = int(idx)
508 } else {
509 // Another valid boot entry already exists, delete this one
510 err := efivarfs.DeleteBootEntry(int(idx))
511 if err == nil {
512 s.Logger.Infof("Deleted duplicate boot entry %q", e.Description)
513 } else {
514 s.Logger.Warningf("Error while deleting duplicate boot entry %q: %v", e.Description, err)
515 }
516 }
517 } else if strings.Contains(e.Description, "Metropolis") {
518 err := efivarfs.DeleteBootEntry(int(idx))
519 if err == nil {
520 s.Logger.Infof("Deleted orphaned boot entry %q", e.Description)
521 } else {
522 s.Logger.Warningf("Error while deleting orphaned boot entry %q: %v", e.Description, err)
523 }
524 }
525 default:
526 continue
527 }
528 }
529 if validBootEntryIdx == -1 {
530 validBootEntryIdx, err = efivarfs.AddBootEntry(&efivarfs.LoadOption{
531 Description: "Metropolis",
532 FilePath: efivarfs.DevicePath{
533 &efivarfs.HardDrivePath{
534 PartitionNumber: 1,
535 PartitionStartBlock: s.ESPPart.FirstBlock,
536 PartitionSizeBlocks: s.ESPPart.SizeBlocks(),
537 PartitionMatch: efivarfs.PartitionGPT{
538 PartitionUUID: s.ESPPart.ID,
539 },
540 },
541 efivarfs.FilePath(osimage.EFIPayloadPath),
542 },
543 })
544 if err == nil {
545 s.Logger.Infof("Restored missing EFI boot entry for Metropolis")
546 } else {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200547 return fmt.Errorf("while restoring missing EFI boot entry for Metropolis: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100548 }
549 }
550 bootOrder, err := efivarfs.GetBootOrder()
551 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200552 return fmt.Errorf("failed to get EFI boot order: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100553 }
554 for _, bentry := range bootOrder {
555 if bentry == uint16(validBootEntryIdx) {
556 // Our boot entry is in the boot order, everything's ok
557 return nil
558 }
559 }
560 newBootOrder := append(efivarfs.BootOrder{uint16(validBootEntryIdx)}, bootOrder...)
561 if err := efivarfs.SetBootOrder(newBootOrder); err != nil {
562 return fmt.Errorf("while setting EFI boot order: %w", err)
563 }
564 return nil
565}