github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/overlord/devicestate/systems.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 devicestate 21 22 import ( 23 "fmt" 24 "path/filepath" 25 "strings" 26 27 "github.com/snapcore/snapd/asserts" 28 "github.com/snapcore/snapd/boot" 29 "github.com/snapcore/snapd/dirs" 30 "github.com/snapcore/snapd/logger" 31 "github.com/snapcore/snapd/osutil" 32 "github.com/snapcore/snapd/overlord/snapstate" 33 "github.com/snapcore/snapd/overlord/state" 34 "github.com/snapcore/snapd/seed" 35 "github.com/snapcore/snapd/seed/seedwriter" 36 "github.com/snapcore/snapd/snap" 37 "github.com/snapcore/snapd/strutil" 38 ) 39 40 func checkSystemRequestConflict(st *state.State, systemLabel string) error { 41 st.Lock() 42 defer st.Unlock() 43 44 var seeded bool 45 if err := st.Get("seeded", &seeded); err != nil && err != state.ErrNoState { 46 return err 47 } 48 if seeded { 49 // the system is fully seeded already 50 return nil 51 } 52 53 // inspect the current system which is stored in modeenv, note we are 54 // holding the state lock so there is no race against mark-seeded 55 // clearing recovery system; recovery system is not cleared when seeding 56 // fails 57 modeEnv, err := maybeReadModeenv() 58 if err != nil { 59 return err 60 } 61 if modeEnv == nil { 62 // non UC20 systems do not support actions, no conflict can 63 // happen 64 return nil 65 } 66 67 // not yet fully seeded, hold off requests for the system that is being 68 // seeded, but allow requests for other systems 69 if modeEnv.RecoverySystem == systemLabel { 70 return &snapstate.ChangeConflictError{ 71 ChangeKind: "seed", 72 Message: "cannot request system action, system is seeding", 73 } 74 } 75 return nil 76 } 77 78 func systemFromSeed(label string, current *currentSystem) (*System, error) { 79 s, err := seed.Open(dirs.SnapSeedDir, label) 80 if err != nil { 81 return nil, fmt.Errorf("cannot open: %v", err) 82 } 83 if err := s.LoadAssertions(nil, nil); err != nil { 84 return nil, fmt.Errorf("cannot load assertions: %v", err) 85 } 86 // get the model 87 model := s.Model() 88 brand, err := s.Brand() 89 if err != nil { 90 return nil, fmt.Errorf("cannot obtain brand: %v", err) 91 } 92 system := &System{ 93 Current: false, 94 Label: label, 95 Model: model, 96 Brand: brand, 97 Actions: defaultSystemActions, 98 } 99 if current.sameAs(system) { 100 system.Current = true 101 system.Actions = current.actions 102 } 103 return system, nil 104 } 105 106 type currentSystem struct { 107 *seededSystem 108 actions []SystemAction 109 } 110 111 func (c *currentSystem) sameAs(other *System) bool { 112 return c != nil && 113 c.System == other.Label && 114 c.Model == other.Model.Model() && 115 c.BrandID == other.Brand.AccountID() 116 } 117 118 func currentSystemForMode(st *state.State, mode string) (*currentSystem, error) { 119 var system *seededSystem 120 var actions []SystemAction 121 var err error 122 123 switch mode { 124 case "run": 125 actions = currentSystemActions 126 system, err = currentSeededSystem(st) 127 case "install": 128 // there is no current system for install mode 129 return nil, nil 130 case "recover": 131 actions = recoverSystemActions 132 // recover mode uses modeenv for reference 133 system, err = seededSystemFromModeenv() 134 default: 135 return nil, fmt.Errorf("internal error: cannot identify current system for unsupported mode %q", mode) 136 } 137 if err != nil { 138 return nil, err 139 } 140 currentSys := ¤tSystem{ 141 seededSystem: system, 142 actions: actions, 143 } 144 return currentSys, nil 145 } 146 147 func currentSeededSystem(st *state.State) (*seededSystem, error) { 148 st.Lock() 149 defer st.Unlock() 150 151 var whatseeded []seededSystem 152 if err := st.Get("seeded-systems", &whatseeded); err != nil { 153 return nil, err 154 } 155 if len(whatseeded) == 0 { 156 // unexpected 157 return nil, state.ErrNoState 158 } 159 // seeded systems are prepended to the list, so the most recently seeded 160 // one comes first 161 return &whatseeded[0], nil 162 } 163 164 func seededSystemFromModeenv() (*seededSystem, error) { 165 modeEnv, err := maybeReadModeenv() 166 if err != nil { 167 return nil, err 168 } 169 if modeEnv == nil { 170 return nil, fmt.Errorf("internal error: modeenv does not exist") 171 } 172 if modeEnv.RecoverySystem == "" { 173 return nil, fmt.Errorf("internal error: recovery system is unset") 174 } 175 176 system, err := systemFromSeed(modeEnv.RecoverySystem, nil) 177 if err != nil { 178 return nil, err 179 } 180 seededSys := &seededSystem{ 181 System: modeEnv.RecoverySystem, 182 Model: system.Model.Model(), 183 BrandID: system.Model.BrandID(), 184 Revision: system.Model.Revision(), 185 Timestamp: system.Model.Timestamp(), 186 // SeedTime is intentionally left unset 187 } 188 return seededSys, nil 189 } 190 191 // getInfoFunc is expected to return for a given snap name a snap.Info for that 192 // snap and whether the snap is present is present. The second bit is relevant 193 // for non-essential snaps mentioned in the model, which if present and having 194 // an 'optional' presence in the model, will be added to the recovery system. 195 type getSnapInfoFunc func(name string) (info *snap.Info, snapIsPresent bool, err error) 196 197 // snapWriteObserveFunc is called with the recovery system directory and the 198 // path to a snap file being written. The snap file may be written to a location 199 // under the common snaps directory. 200 type snapWriteObserveFunc func(systemDir, where string) error 201 202 // createSystemForModelFromValidatedSnaps creates a new recovery system for the 203 // specified model with the specified label using the snaps in the database and 204 // the getInfo function. 205 // 206 // The function returns the directory of the new recovery system as well as the 207 // set of absolute file paths to the new snap files that were written for the 208 // recovery system - some snaps may be in the recovery system directory while 209 // others may be in the common snaps directory shared between multiple recovery 210 // systems on ubuntu-seed. 211 func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, db asserts.RODatabase, getInfo getSnapInfoFunc, observeWrite snapWriteObserveFunc) (dir string, err error) { 212 if model.Grade() == asserts.ModelGradeUnset { 213 return "", fmt.Errorf("cannot create a system for non UC20 model") 214 } 215 216 logger.Noticef("creating recovery system with label %q for %q", label, model.Model()) 217 218 // TODO: should that path provided by boot package instead? 219 recoverySystemDirInRootDir := filepath.Join("/systems", label) 220 assertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps") 221 recoverySystemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir) 222 223 wOpts := &seedwriter.Options{ 224 // RW mount of ubuntu-seed 225 SeedDir: boot.InitramfsUbuntuSeedDir, 226 Label: label, 227 } 228 w, err := seedwriter.New(model, wOpts) 229 if err != nil { 230 return "", err 231 } 232 233 optsSnaps := make([]*seedwriter.OptionsSnap, 0, len(model.RequiredWithEssentialSnaps())) 234 // collect all snaps that are present 235 modelSnaps := make(map[string]*snap.Info) 236 237 getModelSnap := func(name string, essential bool, nonEssentialPresence string) error { 238 kind := "essential" 239 if !essential { 240 kind = "non-essential" 241 if nonEssentialPresence != "" { 242 kind = fmt.Sprintf("non-essential but %v", nonEssentialPresence) 243 } 244 } 245 info, present, err := getInfo(name) 246 if err != nil { 247 return fmt.Errorf("cannot obtain %v snap information: %v", kind, err) 248 } 249 if !essential && !present && nonEssentialPresence == "optional" { 250 // non-essential snap which is declared as optionally 251 // present in the model 252 return nil 253 } 254 // grab those 255 logger.Debugf("%v snap: %v", kind, name) 256 if !present { 257 return fmt.Errorf("internal error: %v snap %q not present", kind, name) 258 } 259 if _, ok := modelSnaps[info.MountFile()]; ok { 260 // we've already seen this snap 261 return nil 262 } 263 // present locally 264 // TODO: for grade dangerous we could have a channel here which is not 265 // the model channel, handle that here 266 optsSnaps = append(optsSnaps, &seedwriter.OptionsSnap{ 267 Path: info.MountFile(), 268 }) 269 modelSnaps[info.MountFile()] = info 270 return nil 271 } 272 273 for _, sn := range model.EssentialSnaps() { 274 const essential = true 275 if err := getModelSnap(sn.SnapName(), essential, ""); err != nil { 276 return "", err 277 } 278 } 279 // snapd is implicitly needed 280 const snapdIsEssential = true 281 if err := getModelSnap("snapd", snapdIsEssential, ""); err != nil { 282 return "", err 283 } 284 for _, sn := range model.SnapsWithoutEssential() { 285 const essential = false 286 if err := getModelSnap(sn.SnapName(), essential, sn.Presence); err != nil { 287 return "", err 288 } 289 } 290 if err := w.SetOptionsSnaps(optsSnaps); err != nil { 291 return "", err 292 } 293 294 newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher { 295 fromDB := func(ref *asserts.Ref) (asserts.Assertion, error) { 296 return ref.Resolve(db.Find) 297 } 298 return asserts.NewFetcher(db, fromDB, save) 299 } 300 f, err := w.Start(db, newFetcher) 301 if err != nil { 302 return "", err 303 } 304 // past this point the system directory is present 305 306 localSnaps, err := w.LocalSnaps() 307 if err != nil { 308 return recoverySystemDir, err 309 } 310 311 for _, sn := range localSnaps { 312 info, ok := modelSnaps[sn.Path] 313 if !ok { 314 return recoverySystemDir, fmt.Errorf("internal error: no snap info for %q", sn.Path) 315 } 316 // TODO: the side info derived here can be different from what 317 // we have in snap.Info, but getting it this way can be 318 // expensive as we need to compute the hash, try to find a 319 // better way 320 _, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db) 321 if err != nil { 322 if !asserts.IsNotFound(err) { 323 return recoverySystemDir, err 324 } else if info.SnapID != "" { 325 // snap info from state must have come 326 // from the store, so it is unexpected 327 // if no assertions for it were found 328 return recoverySystemDir, fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID) 329 } 330 } 331 if err := w.SetInfo(sn, info); err != nil { 332 return recoverySystemDir, err 333 } 334 sn.ARefs = aRefs 335 } 336 337 if err := w.InfoDerived(); err != nil { 338 return recoverySystemDir, err 339 } 340 341 for { 342 // get the list of snaps we need in this iteration 343 toDownload, err := w.SnapsToDownload() 344 if err != nil { 345 return recoverySystemDir, err 346 } 347 // which should be empty as all snaps should be accounted for 348 // already 349 if len(toDownload) > 0 { 350 which := make([]string, 0, len(toDownload)) 351 for _, sn := range toDownload { 352 which = append(which, sn.SnapName()) 353 } 354 return recoverySystemDir, fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", ")) 355 } 356 357 complete, err := w.Downloaded() 358 if err != nil { 359 return recoverySystemDir, err 360 } 361 if complete { 362 logger.Debugf("snap processing for creating %q complete", label) 363 break 364 } 365 } 366 367 for _, warn := range w.Warnings() { 368 logger.Noticef("WARNING creating system %q: %s", label, warn) 369 } 370 371 unassertedSnaps, err := w.UnassertedSnaps() 372 if err != nil { 373 return recoverySystemDir, err 374 } 375 if len(unassertedSnaps) > 0 { 376 locals := make([]string, len(unassertedSnaps)) 377 for i, sn := range unassertedSnaps { 378 locals[i] = sn.SnapName() 379 } 380 logger.Noticef("system %q contains unasserted snaps %s", label, strutil.Quoted(locals)) 381 } 382 383 copySnap := func(name, src, dst string) error { 384 // if the destination snap is in the asserted snaps dir and already 385 // exists, we don't need to copy it since asserted snaps are shared 386 if strings.HasPrefix(dst, assertedSnapsDir+"/") && osutil.FileExists(dst) { 387 return nil 388 } 389 // otherwise, unasserted snaps are not shared, so even if the 390 // destination already exists if it is not in the asserted snaps we 391 // should copy it 392 logger.Noticef("copying new seed snap %q from %v to %v", name, src, dst) 393 if observeWrite != nil { 394 if err := observeWrite(recoverySystemDir, dst); err != nil { 395 return err 396 } 397 } 398 return osutil.CopyFile(src, dst, 0) 399 } 400 if err := w.SeedSnaps(copySnap); err != nil { 401 return recoverySystemDir, err 402 } 403 if err := w.WriteMeta(); err != nil { 404 return recoverySystemDir, err 405 } 406 407 bootSnaps, err := w.BootSnaps() 408 if err != nil { 409 return recoverySystemDir, err 410 } 411 bootWith := &boot.RecoverySystemBootableSet{} 412 for _, sn := range bootSnaps { 413 switch sn.Info.Type() { 414 case snap.TypeKernel: 415 bootWith.Kernel = sn.Info 416 bootWith.KernelPath = sn.Path 417 case snap.TypeGadget: 418 bootWith.GadgetSnapOrDir = sn.Path 419 } 420 } 421 if err := boot.MakeRecoverySystemBootable(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir, bootWith); err != nil { 422 return recoverySystemDir, fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err) 423 } 424 logger.Noticef("created recovery system %q", label) 425 426 return recoverySystemDir, nil 427 }