blob: 48f778d8d3752435490d92ea2604b7e2b6276b58 [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är62cecde2025-04-16 15:24:04 +000029 mversion "source.monogon.dev/metropolis/version"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020030 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020031 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020032 "source.monogon.dev/osbase/efivarfs"
33 "source.monogon.dev/osbase/gpt"
34 "source.monogon.dev/osbase/kexec"
Jan Schär62cecde2025-04-16 15:24:04 +000035 ociosimage "source.monogon.dev/osbase/oci/osimage"
36 "source.monogon.dev/osbase/oci/registry"
37 "source.monogon.dev/version"
38
39 abloaderpb "source.monogon.dev/metropolis/node/core/abloader/spec"
40 apb "source.monogon.dev/metropolis/proto/api"
Lorenz Brun35fcf032023-06-29 04:15:58 +020041)
42
43// Service contains data and functionality to perform A/B updates on a
44// Metropolis node.
45type Service struct {
46 // Path to the mount point of the EFI System Partition (ESP).
47 ESPPath string
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000048 // gpt.Partition of the ESP System Partition.
49 ESPPart *gpt.Partition
Lorenz Brun35fcf032023-06-29 04:15:58 +020050 // Partition number (1-based) of the ESP in the GPT partitions array.
51 ESPPartNumber uint32
Tim Windelschmidt8e87a062023-07-31 01:33:10 +000052
Lorenz Brun35fcf032023-06-29 04:15:58 +020053 // Logger service for the update service.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000054 Logger logging.Leveled
Lorenz Brun35fcf032023-06-29 04:15:58 +020055}
56
57type Slot int
58
59const (
60 SlotInvalid Slot = 0
61 SlotA Slot = 1
62 SlotB Slot = 2
63)
64
65// Other returns the "other" slot, i.e. returns slot A for B and B for A.
66// It returns SlotInvalid for any s which is not SlotA or SlotB.
67func (s Slot) Other() Slot {
68 switch s {
69 case SlotA:
70 return SlotB
71 case SlotB:
72 return SlotA
73 default:
74 return SlotInvalid
75 }
76}
77
78func (s Slot) String() string {
79 switch s {
80 case SlotA:
81 return "A"
82 case SlotB:
83 return "B"
84 default:
85 return "<invalid slot>"
86 }
87}
88
89func (s Slot) EFIBootPath() string {
90 switch s {
91 case SlotA:
92 return osimage.EFIBootAPath
93 case SlotB:
94 return osimage.EFIBootBPath
95 default:
96 return ""
97 }
98}
99
100var slotRegexp = regexp.MustCompile(`PARTLABEL=METROPOLIS-SYSTEM-([AB])`)
101
102// ProvideESP is a convenience function for providing information about the
103// ESP after the update service has been instantiated.
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000104func (s *Service) ProvideESP(path string, partNum uint32, part *gpt.Partition) {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200105 s.ESPPath = path
106 s.ESPPartNumber = partNum
Tim Windelschmidt8e87a062023-07-31 01:33:10 +0000107 s.ESPPart = part
Lorenz Brun35fcf032023-06-29 04:15:58 +0200108}
109
110// CurrentlyRunningSlot returns the slot the current system is booted from.
111func (s *Service) CurrentlyRunningSlot() Slot {
112 cmdline, err := os.ReadFile("/proc/cmdline")
113 if err != nil {
114 return SlotInvalid
115 }
116 slotMatches := slotRegexp.FindStringSubmatch(string(cmdline))
117 if len(slotMatches) != 2 {
118 return SlotInvalid
119 }
120 switch slotMatches[1] {
121 case "A":
122 return SlotA
123 case "B":
124 return SlotB
125 default:
126 panic("unreachable")
127 }
128}
129
130var bootVarRegexp = regexp.MustCompile(`^Boot([0-9A-Fa-f]{4})$`)
131
Lorenz Brun35fcf032023-06-29 04:15:58 +0200132// MarkBootSuccessful must be called after each boot if some implementation-
133// defined criteria for a successful boot are met. If an update has been
134// installed and booted and this function is called, the updated version is
135// marked as default. If an issue occurs during boot and so this function is
136// not called the old version will be started again on next boot.
137func (s *Service) MarkBootSuccessful() error {
138 if s.ESPPath == "" {
139 return errors.New("no ESP information provided to update service, cannot continue")
140 }
Lorenz Brund79881d2023-11-30 19:02:06 +0100141 if err := s.fixupEFI(); err != nil {
142 s.Logger.Errorf("Error when checking boot entry configuration: %v", err)
143 }
144 if err := s.fixupPreloader(); err != nil {
145 s.Logger.Errorf("Error when fixing A/B preloader: %v", err)
146 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200147 activeSlot := s.CurrentlyRunningSlot()
Lorenz Brun54a5a052023-10-02 16:40:11 +0200148 abState, err := s.getABState()
Lorenz Brun35fcf032023-06-29 04:15:58 +0200149 if err != nil {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200150 s.Logger.Warningf("Error while getting A/B loader state, recreating: %v", err)
151 abState = &abloaderpb.ABLoaderData{
152 ActiveSlot: abloaderpb.Slot(activeSlot),
Lorenz Brun35fcf032023-06-29 04:15:58 +0200153 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200154 err := s.setABState(abState)
155 if err != nil {
156 return fmt.Errorf("while recreating A/B loader state: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200157 }
158 }
Lorenz Brun54a5a052023-10-02 16:40:11 +0200159 if Slot(abState.ActiveSlot) != activeSlot {
160 err := s.setABState(&abloaderpb.ABLoaderData{
161 ActiveSlot: abloaderpb.Slot(activeSlot),
162 })
163 if err != nil {
164 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200165 }
166 s.Logger.Infof("Permanently activated slot %v", activeSlot)
167 } else {
168 s.Logger.Infof("Normal boot from slot %v", activeSlot)
169 }
170
171 return nil
172}
173
Lorenz Brunca6da6a2024-09-09 17:55:15 +0200174// Rollback sets the currently-inactive slot as the next boot slot. This is
175// intended to recover from scenarios where roll-forward fixing is difficult.
176// Only the next boot slot is set to make sure that the node is not
177// made unbootable accidentally. On successful bootup that code can switch the
178// active slot to itself.
179func (s *Service) Rollback() error {
180 if s.ESPPath == "" {
181 return errors.New("no ESP information provided to update service, cannot continue")
182 }
183 activeSlot := s.CurrentlyRunningSlot()
184 abState, err := s.getABState()
185 if err != nil {
186 return fmt.Errorf("no valid A/B loader state, cannot rollback: %w", err)
187 }
188 nextSlot := activeSlot.Other()
189 err = s.setABState(&abloaderpb.ABLoaderData{
190 ActiveSlot: abState.ActiveSlot,
191 NextSlot: abloaderpb.Slot(nextSlot),
192 })
193 if err != nil {
194 return fmt.Errorf("while setting next A/B slot: %w", err)
195 }
196 s.Logger.Warningf("Rollback requested, NextSlot set to %v", nextSlot)
197 return nil
198}
199
Lorenz Brun1640c282024-09-09 17:50:48 +0200200// KexecLoadNext loads the slot to be booted next into the kexec staging area.
201// The next slot can then be launched by executing kexec via the reboot
202// syscall. Calling this function counts as a next boot for the purposes of
203// A/B state tracking, so it should not be called without kexecing afterwards.
204func (s *Service) KexecLoadNext() error {
205 state, err := s.getABState()
206 if err != nil {
207 return fmt.Errorf("bad A/B state: %w", err)
208 }
209 slotToLoad := Slot(state.ActiveSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100210 if state.NextSlot != abloaderpb.Slot_SLOT_NONE {
Lorenz Brun1640c282024-09-09 17:50:48 +0200211 slotToLoad = Slot(state.NextSlot)
Tim Windelschmidta10d0cb2025-01-13 14:44:15 +0100212 state.NextSlot = abloaderpb.Slot_SLOT_NONE
Lorenz Brun1640c282024-09-09 17:50:48 +0200213 err = s.setABState(state)
214 if err != nil {
215 return fmt.Errorf("while updating A/B state: %w", err)
216 }
217 }
218 boot, err := os.Open(filepath.Join(s.ESPPath, slotToLoad.EFIBootPath()))
219 if err != nil {
220 return fmt.Errorf("failed to open boot file for slot %v: %w", slotToLoad, err)
221 }
222 defer boot.Close()
223 if err := s.stageKexec(boot, slotToLoad); err != nil {
224 return fmt.Errorf("failed to stage next slot for kexec: %w", err)
225 }
226 return nil
227}
228
Lorenz Brun35fcf032023-06-29 04:15:58 +0200229func openSystemSlot(slot Slot) (*blockdev.Device, error) {
230 switch slot {
231 case SlotA:
232 return blockdev.Open("/dev/system-a")
233 case SlotB:
234 return blockdev.Open("/dev/system-b")
235 default:
236 return nil, errors.New("invalid slot identifier given")
237 }
238}
239
Lorenz Brun54a5a052023-10-02 16:40:11 +0200240func (s *Service) getABState() (*abloaderpb.ABLoaderData, error) {
241 abDataRaw, err := os.ReadFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"))
242 if err != nil {
243 return nil, err
244 }
245 var abData abloaderpb.ABLoaderData
246 if err := proto.Unmarshal(abDataRaw, &abData); err != nil {
247 return nil, err
248 }
249 return &abData, nil
250}
251
252func (s *Service) setABState(d *abloaderpb.ABLoaderData) error {
253 abDataRaw, err := proto.Marshal(d)
254 if err != nil {
255 return fmt.Errorf("while marshaling: %w", err)
256 }
257 if err := os.WriteFile(filepath.Join(s.ESPPath, "EFI/metropolis/loader_state.pb"), abDataRaw, 0666); err != nil {
258 return err
259 }
260 return nil
261}
262
Jan Schär62cecde2025-04-16 15:24:04 +0000263// InstallImage fetches the given image, installs it into the currently inactive
264// slot and sets that slot to boot next. If it doesn't return an error, a reboot
265// boots into the new slot.
266func (s *Service) InstallImage(ctx context.Context, imageRef *apb.OSImageRef, withKexec bool) error {
267 if imageRef == nil {
268 return fmt.Errorf("missing OS image in OS installation request")
269 }
270 if imageRef.Digest == "" {
271 return fmt.Errorf("missing digest in OS installation request")
272 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200273 if s.ESPPath == "" {
274 return errors.New("no ESP information provided to update service, cannot continue")
275 }
Jan Schär62cecde2025-04-16 15:24:04 +0000276
277 downloadCtx, cancel := context.WithTimeout(ctx, 15*time.Minute)
278 defer cancel()
279
280 client := &registry.Client{
281 GetBackOff: func() backoff.BackOff {
282 return backoff.NewExponentialBackOff()
283 },
284 RetryNotify: func(err error, d time.Duration) {
285 s.Logger.Warningf("Error while fetching OS image, retrying in %v: %v", d, err)
286 },
287 UserAgent: "MonogonOS/" + strings.TrimPrefix(version.Semver(mversion.Version), "v"),
288 Scheme: imageRef.Scheme,
289 Host: imageRef.Host,
290 Repository: imageRef.Repository,
Lorenz Brun35fcf032023-06-29 04:15:58 +0200291 }
Jan Schär62cecde2025-04-16 15:24:04 +0000292
293 image, err := client.Read(downloadCtx, imageRef.Tag, imageRef.Digest)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200294 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000295 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200296 }
Jan Schär62cecde2025-04-16 15:24:04 +0000297
298 osImage, err := ociosimage.Read(image)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200299 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000300 return fmt.Errorf("failed to fetch OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200301 }
Jan Schär62cecde2025-04-16 15:24:04 +0000302
303 efiPayload, err := osImage.Payload("kernel.efi")
Lorenz Brun35fcf032023-06-29 04:15:58 +0200304 if err != nil {
Jan Schär62cecde2025-04-16 15:24:04 +0000305 return fmt.Errorf("cannot open EFI payload in OS image: %w", err)
Lorenz Brun35fcf032023-06-29 04:15:58 +0200306 }
Jan Schär62cecde2025-04-16 15:24:04 +0000307 systemImage, err := osImage.Payload("system")
308 if err != nil {
309 return fmt.Errorf("cannot open system image in OS image: %w", err)
310 }
311
Lorenz Brun35fcf032023-06-29 04:15:58 +0200312 activeSlot := s.CurrentlyRunningSlot()
313 if activeSlot == SlotInvalid {
314 return errors.New("unable to determine active slot, cannot continue")
315 }
316 targetSlot := activeSlot.Other()
317
Lorenz Brun35fcf032023-06-29 04:15:58 +0200318 systemPart, err := openSystemSlot(targetSlot)
319 if err != nil {
320 return status.Errorf(codes.Internal, "Inactive system slot unavailable: %v", err)
321 }
Jan Schär62cecde2025-04-16 15:24:04 +0000322 systemImageContent, err := systemImage.Open()
323 if err != nil {
324 systemPart.Close()
325 return fmt.Errorf("failed to open system image: %w", err)
326 }
327 _, err = io.Copy(blockdev.NewRWS(systemPart), systemImageContent)
328 systemImageContent.Close()
329 closeErr := systemPart.Close()
330 if err == nil {
331 err = closeErr
332 }
333 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200334 return status.Errorf(codes.Unavailable, "Failed to copy system image: %v", err)
335 }
336
337 bootFile, err := os.Create(filepath.Join(s.ESPPath, targetSlot.EFIBootPath()))
338 if err != nil {
339 return fmt.Errorf("failed to open boot file: %w", err)
340 }
341 defer bootFile.Close()
Jan Schär62cecde2025-04-16 15:24:04 +0000342 efiPayloadContent, err := efiPayload.Open()
343 if err != nil {
344 return fmt.Errorf("failed to open EFI payload: %w", err)
345 }
346 _, err = io.Copy(bootFile, efiPayloadContent)
347 efiPayloadContent.Close()
348 if err != nil {
Lorenz Brun35fcf032023-06-29 04:15:58 +0200349 return fmt.Errorf("failed to write boot file: %w", err)
350 }
351
Lorenz Brund14be0e2023-07-31 16:46:14 +0200352 if withKexec {
353 if err := s.stageKexec(bootFile, targetSlot); err != nil {
354 return fmt.Errorf("while kexec staging: %w", err)
355 }
356 } else {
Lorenz Brun54a5a052023-10-02 16:40:11 +0200357 err := s.setABState(&abloaderpb.ABLoaderData{
358 ActiveSlot: abloaderpb.Slot(activeSlot),
359 NextSlot: abloaderpb.Slot(targetSlot),
360 })
361 if err != nil {
362 return fmt.Errorf("while setting next A/B slot: %w", err)
Lorenz Brund14be0e2023-07-31 16:46:14 +0200363 }
Lorenz Brun35fcf032023-06-29 04:15:58 +0200364 }
365
366 return nil
367}
368
Lorenz Brund14be0e2023-07-31 16:46:14 +0200369// newMemfile creates a new file which is not located on a specific filesystem,
370// but is instead backed by anonymous memory.
371func newMemfile(name string, flags int) (*os.File, error) {
372 fd, err := unix.MemfdCreate(name, flags)
373 if err != nil {
374 return nil, fmt.Errorf("memfd_create: %w", err)
375 }
376 return os.NewFile(uintptr(fd), name), nil
377}
378
379// stageKexec stages the kernel, command line and initramfs if available for
380// a future kexec. It extracts the relevant data from the EFI boot executable.
381func (s *Service) stageKexec(bootFile io.ReaderAt, targetSlot Slot) error {
382 bootPE, err := pe.NewFile(bootFile)
383 if err != nil {
384 return fmt.Errorf("unable to open bootFile as PE: %w", err)
385 }
386 var cmdlineRaw []byte
387 cmdlineSection := bootPE.Section(".cmdline")
388 if cmdlineSection == nil {
389 return fmt.Errorf("no .cmdline section in boot PE")
390 }
391 cmdlineRaw, err = cmdlineSection.Data()
392 if err != nil {
393 return fmt.Errorf("while reading .cmdline PE section: %w", err)
394 }
395 cmdline := string(bytes.TrimRight(cmdlineRaw, "\x00"))
396 cmdline = strings.ReplaceAll(cmdline, "METROPOLIS-SYSTEM-X", fmt.Sprintf("METROPOLIS-SYSTEM-%s", targetSlot))
397 kernelFile, err := newMemfile("kernel", 0)
398 if err != nil {
399 return fmt.Errorf("failed to create kernel memfile: %w", err)
400 }
401 defer kernelFile.Close()
402 kernelSection := bootPE.Section(".linux")
403 if kernelSection == nil {
404 return fmt.Errorf("no .linux section in boot PE")
405 }
406 if _, err := io.Copy(kernelFile, kernelSection.Open()); err != nil {
407 return fmt.Errorf("while copying .linux PE section: %w", err)
408 }
409
410 initramfsSection := bootPE.Section(".initrd")
411 var initramfsFile *os.File
412 if initramfsSection != nil && initramfsSection.Size > 0 {
413 initramfsFile, err = newMemfile("initramfs", 0)
414 if err != nil {
415 return fmt.Errorf("failed to create initramfs memfile: %w", err)
416 }
417 defer initramfsFile.Close()
418 if _, err := io.Copy(initramfsFile, initramfsSection.Open()); err != nil {
419 return fmt.Errorf("while copying .initrd PE section: %w", err)
420 }
421 }
422 if err := kexec.FileLoad(kernelFile, initramfsFile, cmdline); err != nil {
423 return fmt.Errorf("while staging new kexec kernel: %w", err)
424 }
425 return nil
426}
Lorenz Brund79881d2023-11-30 19:02:06 +0100427
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +0200428//go:embed metropolis/node/core/abloader/abloader.efi
Lorenz Brund79881d2023-11-30 19:02:06 +0100429var abloader []byte
430
431func (s *Service) fixupPreloader() error {
432 abLoaderFile, err := os.Open(filepath.Join(s.ESPPath, osimage.EFIPayloadPath))
433 if err != nil {
434 s.Logger.Warningf("A/B preloader not available, attempting to restore: %v", err)
435 } else {
436 expectedSum := sha256.Sum256(abloader)
437 h := sha256.New()
438 _, err := io.Copy(h, abLoaderFile)
439 abLoaderFile.Close()
440 if err == nil {
441 if bytes.Equal(h.Sum(nil), expectedSum[:]) {
442 // A/B Preloader is present and has correct hash
443 return nil
444 } else {
445 s.Logger.Infof("Replacing A/B preloader with current version: %x %x", h.Sum(nil), expectedSum[:])
446 }
447 } else {
448 s.Logger.Warningf("Error while reading A/B preloader, restoring: %v", err)
449 }
450 }
451 preloader, err := os.Create(filepath.Join(s.ESPPath, "preloader.swp"))
452 if err != nil {
453 return fmt.Errorf("while creating preloader swap file: %w", err)
454 }
455 if _, err := preloader.Write(abloader); err != nil {
456 return fmt.Errorf("while writing preloader swap file: %w", err)
457 }
458 if err := preloader.Sync(); err != nil {
459 return fmt.Errorf("while sync'ing preloader swap file: %w", err)
460 }
461 preloader.Close()
462 if err := os.Rename(filepath.Join(s.ESPPath, "preloader.swp"), filepath.Join(s.ESPPath, osimage.EFIPayloadPath)); err != nil {
463 return fmt.Errorf("while swapping preloader: %w", err)
464 }
465 s.Logger.Info("Successfully wrote current preloader")
466 return nil
467}
468
469// fixupEFI checks for the existence and correctness of the EFI boot entry
470// repairs/recreates it if needed.
471func (s *Service) fixupEFI() error {
472 varNames, err := efivarfs.List(efivarfs.ScopeGlobal)
473 if err != nil {
474 return fmt.Errorf("failed to list EFI variables: %w", err)
475 }
Tim Windelschmidt5e460a92024-04-11 01:33:09 +0200476 var validBootEntryIdx = -1
Lorenz Brund79881d2023-11-30 19:02:06 +0100477 for _, varName := range varNames {
478 m := bootVarRegexp.FindStringSubmatch(varName)
479 if m == nil {
480 continue
481 }
482 idx, err := strconv.ParseUint(m[1], 16, 16)
483 if err != nil {
484 // This cannot be hit as all regexp matches are parseable.
485 panic(err)
486 }
487 e, err := efivarfs.GetBootEntry(int(idx))
488 if err != nil {
489 s.Logger.Warningf("Unable to get boot entry %d, skipping: %v", idx, err)
490 continue
491 }
492 if len(e.FilePath) != 2 {
493 // Not our entry, ours always have two parts
494 continue
495 }
496 switch p := e.FilePath[0].(type) {
497 case *efivarfs.HardDrivePath:
498 gptMatch, ok := p.PartitionMatch.(*efivarfs.PartitionGPT)
499 if ok && gptMatch.PartitionUUID != s.ESPPart.ID {
500 // Not related to our ESP
501 continue
502 }
503 default:
504 continue
505 }
506 switch p := e.FilePath[1].(type) {
507 case efivarfs.FilePath:
508 if string(p) == osimage.EFIPayloadPath {
509 if validBootEntryIdx == -1 {
510 validBootEntryIdx = int(idx)
511 } else {
512 // Another valid boot entry already exists, delete this one
513 err := efivarfs.DeleteBootEntry(int(idx))
514 if err == nil {
515 s.Logger.Infof("Deleted duplicate boot entry %q", e.Description)
516 } else {
517 s.Logger.Warningf("Error while deleting duplicate boot entry %q: %v", e.Description, err)
518 }
519 }
520 } else if strings.Contains(e.Description, "Metropolis") {
521 err := efivarfs.DeleteBootEntry(int(idx))
522 if err == nil {
523 s.Logger.Infof("Deleted orphaned boot entry %q", e.Description)
524 } else {
525 s.Logger.Warningf("Error while deleting orphaned boot entry %q: %v", e.Description, err)
526 }
527 }
528 default:
529 continue
530 }
531 }
532 if validBootEntryIdx == -1 {
533 validBootEntryIdx, err = efivarfs.AddBootEntry(&efivarfs.LoadOption{
534 Description: "Metropolis",
535 FilePath: efivarfs.DevicePath{
536 &efivarfs.HardDrivePath{
537 PartitionNumber: 1,
538 PartitionStartBlock: s.ESPPart.FirstBlock,
539 PartitionSizeBlocks: s.ESPPart.SizeBlocks(),
540 PartitionMatch: efivarfs.PartitionGPT{
541 PartitionUUID: s.ESPPart.ID,
542 },
543 },
544 efivarfs.FilePath(osimage.EFIPayloadPath),
545 },
546 })
547 if err == nil {
548 s.Logger.Infof("Restored missing EFI boot entry for Metropolis")
549 } else {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200550 return fmt.Errorf("while restoring missing EFI boot entry for Metropolis: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100551 }
552 }
553 bootOrder, err := efivarfs.GetBootOrder()
554 if err != nil {
Tim Windelschmidt5f1a7de2024-09-19 02:00:14 +0200555 return fmt.Errorf("failed to get EFI boot order: %w", err)
Lorenz Brund79881d2023-11-30 19:02:06 +0100556 }
557 for _, bentry := range bootOrder {
558 if bentry == uint16(validBootEntryIdx) {
559 // Our boot entry is in the boot order, everything's ok
560 return nil
561 }
562 }
563 newBootOrder := append(efivarfs.BootOrder{uint16(validBootEntryIdx)}, bootOrder...)
564 if err := efivarfs.SetBootOrder(newBootOrder); err != nil {
565 return fmt.Errorf("while setting EFI boot order: %w", err)
566 }
567 return nil
568}