gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/boot/boot.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package boot 21 22 import ( 23 "errors" 24 "fmt" 25 26 "github.com/snapcore/snapd/asserts" 27 "github.com/snapcore/snapd/bootloader" 28 "github.com/snapcore/snapd/snap" 29 ) 30 31 const ( 32 // DefaultStatus is the value of a status boot variable when nothing is 33 // being tried 34 DefaultStatus = "" 35 // TryStatus is the value of a status boot variable when something is about 36 // to be tried 37 TryStatus = "try" 38 // TryingStatus is the value of a status boot variable after we have 39 // attempted a boot with a try snap - this status is only set in the early 40 // boot sequence (bootloader, initramfs, etc.) 41 TryingStatus = "trying" 42 ) 43 44 // A BootParticipant handles the boot process details for a snap involved in it. 45 type BootParticipant interface { 46 // SetNextBoot will schedule the snap to be used in the next boot. For 47 // base snaps it is up to the caller to select the right bootable base 48 // (from the model assertion). It is a noop for not relevant snaps. 49 // Otherwise it returns whether a reboot is required. 50 SetNextBoot() (rebootRequired bool, err error) 51 52 // Is this a trivial implementation of the interface? 53 IsTrivial() bool 54 } 55 56 // A BootKernel handles the bootloader setup of a kernel. 57 type BootKernel interface { 58 // RemoveKernelAssets removes the unpacked kernel/initrd for the given 59 // kernel snap. 60 RemoveKernelAssets() error 61 // ExtractKernelAssets extracts kernel/initrd/dtb data from the given 62 // kernel snap, if required, to a versioned bootloader directory so 63 // that the bootloader can use it. 64 ExtractKernelAssets(snap.Container) error 65 // Is this a trivial implementation of the interface? 66 IsTrivial() bool 67 } 68 69 type trivial struct{} 70 71 func (trivial) SetNextBoot() (bool, error) { return false, nil } 72 func (trivial) IsTrivial() bool { return true } 73 func (trivial) RemoveKernelAssets() error { return nil } 74 func (trivial) ExtractKernelAssets(snap.Container) error { return nil } 75 76 // ensure trivial is a BootParticipant 77 var _ BootParticipant = trivial{} 78 79 // ensure trivial is a Kernel 80 var _ BootKernel = trivial{} 81 82 // Device carries information about the device model and mode that is 83 // relevant to boot. Note snapstate.DeviceContext implements this, and that's 84 // the expected use case. 85 type Device interface { 86 RunMode() bool 87 Classic() bool 88 89 Kernel() string 90 Base() string 91 92 HasModeenv() bool 93 94 Model() *asserts.Model 95 } 96 97 // Participant figures out what the BootParticipant is for the given 98 // arguments, and returns it. If the snap does _not_ participate in 99 // the boot process, the returned object will be a NOP, so it's safe 100 // to call anything on it always. 101 // 102 // Currently, on classic, nothing is a boot participant (returned will 103 // always be NOP). 104 func Participant(s snap.PlaceInfo, t snap.Type, dev Device) BootParticipant { 105 if applicable(s, t, dev) { 106 bs, err := bootStateFor(t, dev) 107 if err != nil { 108 // all internal errors at this point 109 panic(err) 110 } 111 return &coreBootParticipant{s: s, bs: bs} 112 } 113 return trivial{} 114 } 115 116 // bootloaderOptionsForDeviceKernel returns a set of bootloader options that 117 // enable correct kernel extraction and removal for given device 118 func bootloaderOptionsForDeviceKernel(dev Device) *bootloader.Options { 119 if !dev.HasModeenv() { 120 return nil 121 } 122 // find the run-mode bootloader with its kernel support for UC20 123 return &bootloader.Options{ 124 Role: bootloader.RoleRunMode, 125 } 126 } 127 128 // Kernel checks that the given arguments refer to a kernel snap 129 // that participates in the boot process, and returns the associated 130 // BootKernel, or a trivial implementation otherwise. 131 func Kernel(s snap.PlaceInfo, t snap.Type, dev Device) BootKernel { 132 if t == snap.TypeKernel && applicable(s, t, dev) { 133 return &coreKernel{s: s, bopts: bootloaderOptionsForDeviceKernel(dev)} 134 } 135 return trivial{} 136 } 137 138 func applicable(s snap.PlaceInfo, t snap.Type, dev Device) bool { 139 if dev.Classic() { 140 return false 141 } 142 // In ephemeral modes we never need to care about updating the boot 143 // config. This will be done via boot.MakeBootable(). 144 if !dev.RunMode() { 145 return false 146 } 147 148 if t != snap.TypeOS && t != snap.TypeKernel && t != snap.TypeBase { 149 // note we don't currently have anything useful to do with gadgets 150 return false 151 } 152 153 switch t { 154 case snap.TypeKernel: 155 if s.InstanceName() != dev.Kernel() { 156 // a remodel might leave you in this state 157 return false 158 } 159 case snap.TypeBase, snap.TypeOS: 160 base := dev.Base() 161 if base == "" { 162 base = "core" 163 } 164 if s.InstanceName() != base { 165 return false 166 } 167 } 168 169 return true 170 } 171 172 // bootState exposes the boot state for a type of boot snap during 173 // normal running state, i.e. after the pivot_root and after the initramfs. 174 type bootState interface { 175 // revisions retrieves the revisions of the current snap and 176 // the try snap (only the latter might not be set), and 177 // the status of the trying snap. 178 // Note that the error could be only specific to the try snap, in which case 179 // curSnap may still be non-nil and valid. Callers concerned with robustness 180 // should always inspect a non-nil error with isTrySnapError, and use 181 // curSnap instead if the error is only for the trySnap or tryingStatus. 182 revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) 183 184 // setNext lazily implements setting the next boot target for 185 // the type's boot snap. actually committing the update 186 // is done via the returned bootStateUpdate's commit method. 187 setNext(s snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) 188 189 // markSuccessful lazily implements marking the boot 190 // successful for the type's boot snap. The actual committing 191 // of the update is done via bootStateUpdate's commit, that 192 // way different markSuccessful can be folded together. 193 markSuccessful(bootStateUpdate) (bootStateUpdate, error) 194 } 195 196 // successfulBootState exposes the state of resources requiring bookkeeping on a 197 // successful boot. 198 type successfulBootState interface { 199 // markSuccessful lazily implements marking the boot 200 // successful for the given type of resource. 201 markSuccessful(bootStateUpdate) (bootStateUpdate, error) 202 } 203 204 // bootStateFor finds the right bootState implementation of the given 205 // snap type and Device, if applicable. 206 func bootStateFor(typ snap.Type, dev Device) (s bootState, err error) { 207 if !dev.RunMode() { 208 return nil, fmt.Errorf("internal error: no boot state handling for ephemeral modes") 209 } 210 newBootState := newBootState16 211 if dev.HasModeenv() { 212 newBootState = newBootState20 213 } 214 switch typ { 215 case snap.TypeOS, snap.TypeBase: 216 return newBootState(snap.TypeBase, dev), nil 217 case snap.TypeKernel: 218 return newBootState(snap.TypeKernel, dev), nil 219 default: 220 return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ) 221 } 222 } 223 224 // InUseFunc is a function to check if the snap is in use or not. 225 type InUseFunc func(name string, rev snap.Revision) bool 226 227 func fixedInUse(inUse bool) InUseFunc { 228 return func(string, snap.Revision) bool { 229 return inUse 230 } 231 } 232 233 // InUse returns a checker for whether a given name/revision is used in the 234 // boot environment for snaps of the relevant snap type. 235 func InUse(typ snap.Type, dev Device) (InUseFunc, error) { 236 if dev.Classic() { 237 // no boot state on classic 238 return fixedInUse(false), nil 239 } 240 if !dev.RunMode() { 241 // ephemeral mode, block manipulations for now 242 return fixedInUse(true), nil 243 } 244 switch typ { 245 case snap.TypeKernel, snap.TypeBase, snap.TypeOS: 246 break 247 default: 248 return fixedInUse(false), nil 249 } 250 cands := make([]snap.PlaceInfo, 0, 2) 251 s, err := bootStateFor(typ, dev) 252 if err != nil { 253 return nil, err 254 } 255 cand, tryCand, _, err := s.revisions() 256 if err != nil { 257 return nil, err 258 } 259 cands = append(cands, cand) 260 if tryCand != nil { 261 cands = append(cands, tryCand) 262 } 263 264 return func(name string, rev snap.Revision) bool { 265 for _, cand := range cands { 266 if cand.SnapName() == name && cand.SnapRevision() == rev { 267 return true 268 } 269 } 270 return false 271 }, nil 272 } 273 274 var ( 275 // ErrBootNameAndRevisionNotReady is returned when the boot revision is not 276 // established yet. 277 ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established") 278 ) 279 280 // GetCurrentBoot returns the currently set name and revision for boot for the given 281 // type of snap, which can be snap.TypeBase (or snap.TypeOS), or snap.TypeKernel. 282 // Returns ErrBootNameAndRevisionNotReady if the values are temporarily not established. 283 func GetCurrentBoot(t snap.Type, dev Device) (snap.PlaceInfo, error) { 284 s, err := bootStateFor(t, dev) 285 if err != nil { 286 return nil, err 287 } 288 289 snap, _, status, err := s.revisions() 290 if err != nil { 291 return nil, err 292 } 293 294 if status == TryingStatus { 295 return nil, ErrBootNameAndRevisionNotReady 296 } 297 298 return snap, nil 299 } 300 301 // bootStateUpdate carries the state for an on-going boot state update. 302 // At the end it can be used to commit it. 303 type bootStateUpdate interface { 304 commit() error 305 } 306 307 // MarkBootSuccessful marks the current boot as successful. This means 308 // that snappy will consider this combination of kernel/os a valid 309 // target for rollback. 310 // 311 // The states that a boot goes through for UC16/18 are the following: 312 // - By default snap_mode is "" in which case the bootloader loads 313 // two squashfs'es denoted by variables snap_core and snap_kernel. 314 // - On a refresh of core/kernel snapd will set snap_mode=try and 315 // will also set snap_try_{core,kernel} to the core/kernel that 316 // will be tried next. 317 // - On reboot the bootloader will inspect the snap_mode and if the 318 // mode is set to "try" it will set "snap_mode=trying" and then 319 // try to boot the snap_try_{core,kernel}". 320 // - On a successful boot snapd resets snap_mode to "" and copies 321 // snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_* 322 // values are cleared afterwards. 323 // - On a failing boot the bootloader will see snap_mode=trying which 324 // means snapd did not start successfully. In this case the bootloader 325 // will set snap_mode="" and the system will boot with the known good 326 // values from snap_{core,kernel} 327 func MarkBootSuccessful(dev Device) error { 328 const errPrefix = "cannot mark boot successful: %s" 329 330 var u bootStateUpdate 331 for _, t := range []snap.Type{snap.TypeBase, snap.TypeKernel} { 332 s, err := bootStateFor(t, dev) 333 if err != nil { 334 return err 335 } 336 u, err = s.markSuccessful(u) 337 if err != nil { 338 return fmt.Errorf(errPrefix, err) 339 } 340 } 341 342 if dev.HasModeenv() { 343 for _, bs := range []successfulBootState{ 344 trustedAssetsBootState(dev), 345 trustedCommandLineBootState(dev), 346 recoverySystemsBootState(dev), 347 modelBootState(dev), 348 } { 349 var err error 350 u, err = bs.markSuccessful(u) 351 if err != nil { 352 return fmt.Errorf(errPrefix, err) 353 } 354 } 355 } 356 357 if u != nil { 358 if err := u.commit(); err != nil { 359 return fmt.Errorf(errPrefix, err) 360 } 361 } 362 return nil 363 } 364 365 var ErrUnsupportedSystemMode = errors.New("system mode is unsupported") 366 367 // SetRecoveryBootSystemAndMode configures the recovery bootloader to boot into 368 // the given recovery system in a particular mode. Returns 369 // ErrUnsupportedSystemMode when booting into a recovery system is not supported 370 // by the device. 371 func SetRecoveryBootSystemAndMode(dev Device, systemLabel, mode string) error { 372 if !dev.HasModeenv() { 373 // only UC20 devices are supported 374 return ErrUnsupportedSystemMode 375 } 376 if systemLabel == "" { 377 return fmt.Errorf("internal error: system label is unset") 378 } 379 if mode == "" { 380 return fmt.Errorf("internal error: system mode is unset") 381 } 382 383 opts := &bootloader.Options{ 384 // setup the recovery bootloader 385 Role: bootloader.RoleRecovery, 386 } 387 // TODO:UC20: should the recovery partition stay around as RW during run 388 // mode all the time? 389 bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) 390 if err != nil { 391 return err 392 } 393 394 m := map[string]string{ 395 "snapd_recovery_system": systemLabel, 396 "snapd_recovery_mode": mode, 397 } 398 return bl.SetBootVars(m) 399 } 400 401 // UpdateManagedBootConfigs updates managed boot config assets if those are 402 // present for the ubuntu-boot bootloader. Returns true when an update was 403 // carried out. 404 func UpdateManagedBootConfigs(dev Device, gadgetSnapOrDir string) (updated bool, err error) { 405 if !dev.HasModeenv() { 406 // only UC20 devices use managed boot config 407 return false, nil 408 } 409 if !dev.RunMode() { 410 return false, fmt.Errorf("internal error: boot config can only be updated in run mode") 411 } 412 return updateManagedBootConfigForBootloader(dev, ModeRun, gadgetSnapOrDir) 413 } 414 415 func updateManagedBootConfigForBootloader(dev Device, mode, gadgetSnapOrDir string) (updated bool, err error) { 416 if mode != ModeRun { 417 return false, fmt.Errorf("internal error: updating boot config of recovery bootloader is not supported yet") 418 } 419 420 opts := &bootloader.Options{ 421 Role: bootloader.RoleRunMode, 422 NoSlashBoot: true, 423 } 424 tbl, err := getBootloaderManagingItsAssets(InitramfsUbuntuBootDir, opts) 425 if err != nil { 426 if err == errBootConfigNotManaged { 427 // we're not managing this bootloader's boot config 428 return false, nil 429 } 430 return false, err 431 } 432 // boot config update can lead to a change of kernel command line 433 _, err = observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonSnapd, gadgetSnapOrDir) 434 if err != nil { 435 return false, err 436 } 437 return tbl.UpdateBootConfig() 438 } 439 440 // UpdateCommandLineForGadgetComponent handles the update of a gadget that 441 // contributes to the kernel command line of the run system. Returns true when a 442 // change in command line has been observed and a reboot is needed. The reboot, 443 // if needed, should be requested at the the earliest possible occasion. 444 func UpdateCommandLineForGadgetComponent(dev Device, gadgetSnapOrDir string) (needsReboot bool, err error) { 445 if !dev.HasModeenv() { 446 // only UC20 devices are supported 447 return false, fmt.Errorf("internal error: command line component cannot be updated on non UC20 devices") 448 } 449 opts := &bootloader.Options{ 450 Role: bootloader.RoleRunMode, 451 } 452 // TODO: add support for bootloaders that that do not have any managed 453 // assets 454 tbl, err := getBootloaderManagingItsAssets("", opts) 455 if err != nil { 456 if err == errBootConfigNotManaged { 457 // we're not managing this bootloader's boot config 458 return false, nil 459 } 460 return false, err 461 } 462 // gadget update can lead to a change of kernel command line 463 cmdlineChange, err := observeCommandLineUpdate(dev.Model(), commandLineUpdateReasonGadget, gadgetSnapOrDir) 464 if err != nil { 465 return false, err 466 } 467 if !cmdlineChange { 468 return false, nil 469 } 470 // update the bootloader environment, maybe clearing the relevant 471 // variables 472 cmdlineVars, err := bootVarsForTrustedCommandLineFromGadget(gadgetSnapOrDir) 473 if err != nil { 474 return false, fmt.Errorf("cannot prepare bootloader variables for kernel command line: %v", err) 475 } 476 if err := tbl.SetBootVars(cmdlineVars); err != nil { 477 return false, fmt.Errorf("cannot set run system kernel command line arguments: %v", err) 478 } 479 return cmdlineChange, nil 480 }