github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 return &whatseeded[0], nil 160 } 161 162 func seededSystemFromModeenv() (*seededSystem, error) { 163 modeEnv, err := maybeReadModeenv() 164 if err != nil { 165 return nil, err 166 } 167 if modeEnv == nil { 168 return nil, fmt.Errorf("internal error: modeenv does not exist") 169 } 170 if modeEnv.RecoverySystem == "" { 171 return nil, fmt.Errorf("internal error: recovery system is unset") 172 } 173 174 system, err := systemFromSeed(modeEnv.RecoverySystem, nil) 175 if err != nil { 176 return nil, err 177 } 178 seededSys := &seededSystem{ 179 System: modeEnv.RecoverySystem, 180 Model: system.Model.Model(), 181 BrandID: system.Model.BrandID(), 182 Revision: system.Model.Revision(), 183 Timestamp: system.Model.Timestamp(), 184 // SeedTime is intentionally left unset 185 } 186 return seededSys, nil 187 } 188 189 // getInfoFunc is expected to return for a given snap name a snap.Info for that 190 // snap and whether the snap is present is present. The second bit is relevant 191 // for non-essential snaps mentioned in the model, which if present and having 192 // an 'optional' presence in the model, will be added to the recovery system. 193 type getSnapInfoFunc func(name string) (info *snap.Info, snapIsPresent bool, err error) 194 195 // snapWriteObserveFunc is called with the recovery system directory and the 196 // path to a snap file being written. The snap file may be written to a location 197 // under the common snaps directory. 198 type snapWriteObserveFunc func(systemDir, where string) error 199 200 // createSystemForModelFromValidatedSnaps creates a new recovery system for the 201 // specified model with the specified label using the snaps in the database and 202 // the getInfo function. 203 // 204 // The function returns the directory of the new recovery system as well as the 205 // set of absolute file paths to the new snap files that were written for the 206 // recovery system - some snaps may be in the recovery system directory while 207 // others may be in the common snaps directory shared between multiple recovery 208 // systems on ubuntu-seed. 209 func createSystemForModelFromValidatedSnaps(model *asserts.Model, label string, db asserts.RODatabase, getInfo getSnapInfoFunc, observeWrite snapWriteObserveFunc) (dir string, err error) { 210 if model.Grade() == asserts.ModelGradeUnset { 211 return "", fmt.Errorf("cannot create a system for non UC20 model") 212 } 213 214 logger.Noticef("creating recovery system with label %q for %q", label, model.Model()) 215 216 // TODO: should that path provided by boot package instead? 217 recoverySystemDirInRootDir := filepath.Join("/systems", label) 218 assertedSnapsDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps") 219 recoverySystemDir := filepath.Join(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir) 220 221 wOpts := &seedwriter.Options{ 222 // RW mount of ubuntu-seed 223 SeedDir: boot.InitramfsUbuntuSeedDir, 224 Label: label, 225 } 226 w, err := seedwriter.New(model, wOpts) 227 if err != nil { 228 return "", err 229 } 230 231 optsSnaps := make([]*seedwriter.OptionsSnap, 0, len(model.RequiredWithEssentialSnaps())) 232 // collect all snaps that are present 233 modelSnaps := make(map[string]*snap.Info) 234 235 getModelSnap := func(name string, essential bool, nonEssentialPresence string) error { 236 kind := "essential" 237 if !essential { 238 kind = "non-essential" 239 if nonEssentialPresence != "" { 240 kind = fmt.Sprintf("non-essential but %v", nonEssentialPresence) 241 } 242 } 243 info, present, err := getInfo(name) 244 if err != nil { 245 return fmt.Errorf("cannot obtain %v snap information: %v", kind, err) 246 } 247 if !essential && !present && nonEssentialPresence == "optional" { 248 // non-essential snap which is declared as optionally 249 // present in the model 250 return nil 251 } 252 // grab those 253 logger.Debugf("%v snap: %v", kind, name) 254 if !present { 255 return fmt.Errorf("internal error: %v snap %q not present", kind, name) 256 } 257 if _, ok := modelSnaps[info.MountFile()]; ok { 258 // we've already seen this snap 259 return nil 260 } 261 // present locally 262 // TODO: for grade dangerous we could have a channel here which is not 263 // the model channel, handle that here 264 optsSnaps = append(optsSnaps, &seedwriter.OptionsSnap{ 265 Path: info.MountFile(), 266 }) 267 modelSnaps[info.MountFile()] = info 268 return nil 269 } 270 271 for _, sn := range model.EssentialSnaps() { 272 const essential = true 273 if err := getModelSnap(sn.SnapName(), essential, ""); err != nil { 274 return "", err 275 } 276 } 277 // snapd is implicitly needed 278 const snapdIsEssential = true 279 if err := getModelSnap("snapd", snapdIsEssential, ""); err != nil { 280 return "", err 281 } 282 for _, sn := range model.SnapsWithoutEssential() { 283 const essential = false 284 if err := getModelSnap(sn.SnapName(), essential, sn.Presence); err != nil { 285 return "", err 286 } 287 } 288 if err := w.SetOptionsSnaps(optsSnaps); err != nil { 289 return "", err 290 } 291 292 newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher { 293 fromDB := func(ref *asserts.Ref) (asserts.Assertion, error) { 294 return ref.Resolve(db.Find) 295 } 296 return asserts.NewFetcher(db, fromDB, save) 297 } 298 f, err := w.Start(db, newFetcher) 299 if err != nil { 300 return "", err 301 } 302 // past this point the system directory is present 303 304 localSnaps, err := w.LocalSnaps() 305 if err != nil { 306 return recoverySystemDir, err 307 } 308 309 for _, sn := range localSnaps { 310 info, ok := modelSnaps[sn.Path] 311 if !ok { 312 return recoverySystemDir, fmt.Errorf("internal error: no snap info for %q", sn.Path) 313 } 314 // TODO: the side info derived here can be different from what 315 // we have in snap.Info, but getting it this way can be 316 // expensive as we need to compute the hash, try to find a 317 // better way 318 _, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db) 319 if err != nil { 320 if !asserts.IsNotFound(err) { 321 return recoverySystemDir, err 322 } else if info.SnapID != "" { 323 // snap info from state must have come 324 // from the store, so it is unexpected 325 // if no assertions for it were found 326 return recoverySystemDir, fmt.Errorf("internal error: no assertions for asserted snap with ID: %v", info.SnapID) 327 } 328 } 329 if err := w.SetInfo(sn, info); err != nil { 330 return recoverySystemDir, err 331 } 332 sn.ARefs = aRefs 333 } 334 335 if err := w.InfoDerived(); err != nil { 336 return recoverySystemDir, err 337 } 338 339 for { 340 // get the list of snaps we need in this iteration 341 toDownload, err := w.SnapsToDownload() 342 if err != nil { 343 return recoverySystemDir, err 344 } 345 // which should be empty as all snaps should be accounted for 346 // already 347 if len(toDownload) > 0 { 348 which := make([]string, 0, len(toDownload)) 349 for _, sn := range toDownload { 350 which = append(which, sn.SnapName()) 351 } 352 return recoverySystemDir, fmt.Errorf("internal error: need to download snaps: %v", strings.Join(which, ", ")) 353 } 354 355 complete, err := w.Downloaded() 356 if err != nil { 357 return recoverySystemDir, err 358 } 359 if complete { 360 logger.Debugf("snap processing for creating %q complete", label) 361 break 362 } 363 } 364 365 for _, warn := range w.Warnings() { 366 logger.Noticef("WARNING creating system %q: %s", label, warn) 367 } 368 369 unassertedSnaps, err := w.UnassertedSnaps() 370 if err != nil { 371 return recoverySystemDir, err 372 } 373 if len(unassertedSnaps) > 0 { 374 locals := make([]string, len(unassertedSnaps)) 375 for i, sn := range unassertedSnaps { 376 locals[i] = sn.SnapName() 377 } 378 logger.Noticef("system %q contains unasserted snaps %s", label, strutil.Quoted(locals)) 379 } 380 381 copySnap := func(name, src, dst string) error { 382 // if the destination snap is in the asserted snaps dir and already 383 // exists, we don't need to copy it since asserted snaps are shared 384 if strings.HasPrefix(dst, assertedSnapsDir+"/") && osutil.FileExists(dst) { 385 return nil 386 } 387 // otherwise, unasserted snaps are not shared, so even if the 388 // destination already exists if it is not in the asserted snaps we 389 // should copy it 390 logger.Noticef("copying new seed snap %q from %v to %v", name, src, dst) 391 if observeWrite != nil { 392 if err := observeWrite(recoverySystemDir, dst); err != nil { 393 return err 394 } 395 } 396 return osutil.CopyFile(src, dst, 0) 397 } 398 if err := w.SeedSnaps(copySnap); err != nil { 399 return recoverySystemDir, err 400 } 401 if err := w.WriteMeta(); err != nil { 402 return recoverySystemDir, err 403 } 404 405 bootSnaps, err := w.BootSnaps() 406 if err != nil { 407 return recoverySystemDir, err 408 } 409 bootWith := &boot.RecoverySystemBootableSet{} 410 for _, sn := range bootSnaps { 411 switch sn.Info.Type() { 412 case snap.TypeKernel: 413 bootWith.Kernel = sn.Info 414 bootWith.KernelPath = sn.Path 415 case snap.TypeGadget: 416 bootWith.GadgetSnapOrDir = sn.Path 417 } 418 } 419 if err := boot.MakeRecoverySystemBootable(boot.InitramfsUbuntuSeedDir, recoverySystemDirInRootDir, bootWith); err != nil { 420 return recoverySystemDir, fmt.Errorf("cannot make candidate recovery system %q bootable: %v", label, err) 421 } 422 logger.Noticef("created recovery system %q", label) 423 424 return recoverySystemDir, nil 425 }