gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/overlord/devicestate/handlers_systems.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 /* 3 * Copyright (C) 2021 Canonical Ltd 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License version 3 as 7 * published by the Free Software Foundation. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 * 17 */ 18 19 package devicestate 20 21 import ( 22 "bufio" 23 "bytes" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "strings" 29 30 "gopkg.in/tomb.v2" 31 32 "github.com/snapcore/snapd/asserts" 33 "github.com/snapcore/snapd/boot" 34 "github.com/snapcore/snapd/logger" 35 "github.com/snapcore/snapd/osutil" 36 "github.com/snapcore/snapd/overlord/assertstate" 37 "github.com/snapcore/snapd/overlord/snapstate" 38 "github.com/snapcore/snapd/overlord/state" 39 "github.com/snapcore/snapd/release" 40 "github.com/snapcore/snapd/snap" 41 "github.com/snapcore/snapd/snap/snapfile" 42 "github.com/snapcore/snapd/strutil" 43 ) 44 45 func taskRecoverySystemSetup(t *state.Task) (*recoverySystemSetup, error) { 46 var setup recoverySystemSetup 47 48 err := t.Get("recovery-system-setup", &setup) 49 if err == nil { 50 return &setup, nil 51 } 52 if err != state.ErrNoState { 53 return nil, err 54 } 55 // find the task which holds the data 56 var id string 57 if err := t.Get("recovery-system-setup-task", &id); err != nil { 58 return nil, err 59 } 60 ts := t.State().Task(id) 61 if ts == nil { 62 return nil, fmt.Errorf("internal error: cannot find referenced task %v", id) 63 } 64 if err := ts.Get("recovery-system-setup", &setup); err != nil { 65 return nil, err 66 } 67 return &setup, nil 68 } 69 70 func setTaskRecoverySystemSetup(t *state.Task, setup *recoverySystemSetup) error { 71 if t.Has("recovery-system-setup") { 72 t.Set("recovery-system-setup", setup) 73 return nil 74 } 75 return fmt.Errorf("internal error: cannot indirectly set recovery-system-setup") 76 } 77 78 func logNewSystemSnapFile(logfile, fileName string) error { 79 if !strings.HasPrefix(filepath.Dir(fileName), boot.InitramfsUbuntuSeedDir+"/") { 80 return fmt.Errorf("internal error: unexpected recovery system snap location %q", fileName) 81 } 82 currentLog, err := ioutil.ReadFile(logfile) 83 if err != nil && !os.IsNotExist(err) { 84 return err 85 } 86 modifiedLog := bytes.NewBuffer(currentLog) 87 fmt.Fprintln(modifiedLog, fileName) 88 return osutil.AtomicWriteFile(logfile, modifiedLog.Bytes(), 0644, 0) 89 } 90 91 func purgeNewSystemSnapFiles(logfile string) error { 92 f, err := os.Open(logfile) 93 if err != nil { 94 if os.IsNotExist(err) { 95 return nil 96 } 97 return err 98 } 99 defer f.Close() 100 s := bufio.NewScanner(f) 101 for { 102 if !s.Scan() { 103 break 104 } 105 // one file per line 106 fileName := strings.TrimSpace(s.Text()) 107 if fileName == "" { 108 continue 109 } 110 if !strings.HasPrefix(fileName, boot.InitramfsUbuntuSeedDir) { 111 logger.Noticef("while removing new seed snap %q: unexpected recovery system snap location", fileName) 112 continue 113 } 114 if err := os.Remove(fileName); err != nil && !os.IsNotExist(err) { 115 logger.Noticef("while removing new seed snap %q: %v", fileName, err) 116 } 117 } 118 return s.Err() 119 } 120 121 func (m *DeviceManager) doCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) (err error) { 122 if release.OnClassic { 123 // TODO: this may need to be lifted in the future 124 return fmt.Errorf("cannot create recovery systems on a classic system") 125 } 126 127 st := t.State() 128 st.Lock() 129 defer st.Unlock() 130 131 remodelCtx, err := DeviceCtx(st, t, nil) 132 if err != nil { 133 return err 134 } 135 model := remodelCtx.Model() 136 isRemodel := remodelCtx.ForRemodeling() 137 138 setup, err := taskRecoverySystemSetup(t) 139 if err != nil { 140 return fmt.Errorf("internal error: cannot obtain recovery system setup information") 141 } 142 label := setup.Label 143 systemDirectory := setup.Directory 144 145 // get all infos 146 infoGetter := func(name string) (info *snap.Info, present bool, err error) { 147 // snap may be present in the system in which case info comes 148 // from snapstate 149 info, err = snapstate.CurrentInfo(st, name) 150 if err == nil { 151 hash, _, err := asserts.SnapFileSHA3_384(info.MountFile()) 152 if err != nil { 153 return nil, true, fmt.Errorf("cannot compute SHA3 of snap file: %v", err) 154 } 155 info.Sha3_384 = hash 156 return info, true, nil 157 } 158 if _, ok := err.(*snap.NotInstalledError); !ok { 159 return nil, false, err 160 } 161 logger.Debugf("requested info for not yet installed snap %q", name) 162 163 if !isRemodel { 164 // when not in remodel, a recovery system can only be 165 // created from snaps that are already installed 166 return nil, false, nil 167 } 168 169 // in a remodel scenario, the snaps may need to be fetched, and 170 // thus we can pull the relevant information from the tasks 171 // carrying snap-setup 172 173 for _, tskID := range setup.SnapSetupTasks { 174 taskWithSnapSetup := st.Task(tskID) 175 snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup) 176 if err != nil { 177 return nil, false, err 178 } 179 if snapsup.SnapName() != name { 180 continue 181 } 182 // by the time this task runs, the file has already been 183 // downloaded and validated 184 snapFile, err := snapfile.Open(snapsup.MountFile()) 185 if err != nil { 186 return nil, false, err 187 } 188 info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo) 189 if err != nil { 190 return nil, false, err 191 } 192 193 return info, true, nil 194 } 195 return nil, false, nil 196 } 197 198 observeSnapFileWrite := func(recoverySystemDir, where string) error { 199 if recoverySystemDir != systemDirectory { 200 return fmt.Errorf("internal error: unexpected recovery system path %q", recoverySystemDir) 201 } 202 // track all the files, both asserted shared snaps and private 203 // ones 204 return logNewSystemSnapFile(filepath.Join(recoverySystemDir, "snapd-new-file-log"), where) 205 } 206 207 var db asserts.RODatabase 208 if isRemodel { 209 // during remodel, the model assertion is not yet present in the 210 // assertstate database, hence we need to use a temporary one to 211 // which we explicitly add the new model assertion, as 212 // createSystemForModelFromValidatedSnaps expects all relevant 213 // assertions to be present in the passed db 214 tempDB := assertstate.TemporaryDB(st) 215 if err := tempDB.Add(model); err != nil { 216 return fmt.Errorf("cannot create a temporary database with model: %v", err) 217 } 218 db = tempDB 219 } else { 220 db = assertstate.DB(st) 221 } 222 defer func() { 223 if err == nil { 224 return 225 } 226 if err := purgeNewSystemSnapFiles(filepath.Join(systemDirectory, "snapd-new-file-log")); err != nil { 227 logger.Noticef("when removing seed files: %v", err) 228 } 229 // this is ok, as before the change with this task was created, 230 // we checked that the system directory did not exist; it may 231 // exist now if one of the post-create steps failed, or the the 232 // task is being re-run after a reboot and creating a system 233 // failed 234 if err := os.RemoveAll(systemDirectory); err != nil && !os.IsNotExist(err) { 235 logger.Noticef("when removing recovery system %q: %v", label, err) 236 } 237 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 238 logger.Noticef("when dropping the recovery system %q: %v", label, err) 239 } 240 // we could have reentered the task after a reboot, but the 241 // state was set up sufficiently such that the system was 242 // actually tried and ended up in the tried systems list, which 243 // we should reset now 244 st.Set("tried-systems", nil) 245 }() 246 // 1. prepare recovery system from remodel snaps (or current snaps) 247 // TODO: this fails when there is a partially complete system seed which 248 // creation could have been interrupted by an unexpected reboot; 249 // consider clearing the recovery system directory and restarting from 250 // scratch 251 _, err = createSystemForModelFromValidatedSnaps(model, label, db, infoGetter, observeSnapFileWrite) 252 if err != nil { 253 return fmt.Errorf("cannot create a recovery system with label %q for %v: %v", label, model.Model(), err) 254 } 255 logger.Debugf("recovery system dir: %v", systemDirectory) 256 257 // 2. keep track of the system in task state 258 if err := setTaskRecoverySystemSetup(t, setup); err != nil { 259 return fmt.Errorf("cannot record recovery system setup state: %v", err) 260 } 261 // 3. set up boot variables for tracking the tried system state 262 if err := boot.SetTryRecoverySystem(remodelCtx, label); err != nil { 263 // rollback? 264 return fmt.Errorf("cannot attempt booting into recovery system %q: %v", label, err) 265 } 266 // 4. and set up the next boot that that system 267 if err := boot.SetRecoveryBootSystemAndMode(remodelCtx, label, "recover"); err != nil { 268 return fmt.Errorf("cannot set device to boot into candidate system %q: %v", label, err) 269 } 270 271 // this task is done, further processing happens in finalize 272 t.SetStatus(state.DoneStatus) 273 274 logger.Noticef("restarting into candidate system %q", label) 275 m.state.RequestRestart(state.RestartSystemNow) 276 return nil 277 } 278 279 func (m *DeviceManager) undoCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 280 if release.OnClassic { 281 // TODO: this may need to be lifted in the future 282 return fmt.Errorf("internal error: cannot create recovery systems on a classic system") 283 } 284 285 st := t.State() 286 st.Lock() 287 defer st.Unlock() 288 289 remodelCtx, err := DeviceCtx(st, t, nil) 290 if err != nil { 291 return err 292 } 293 294 setup, err := taskRecoverySystemSetup(t) 295 if err != nil { 296 return fmt.Errorf("internal error: cannot obtain recovery system setup information") 297 } 298 label := setup.Label 299 300 var undoErr error 301 302 if err := purgeNewSystemSnapFiles(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil { 303 t.Logf("when removing seed files: %v", err) 304 } 305 if err := os.RemoveAll(setup.Directory); err != nil && !os.IsNotExist(err) { 306 t.Logf("when removing recovery system %q: %v", label, err) 307 undoErr = err 308 } else { 309 t.Logf("removed recovery system directory %v", setup.Directory) 310 } 311 312 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 313 return fmt.Errorf("cannot drop a current recovery system %q: %v", label, err) 314 } 315 316 return undoErr 317 } 318 319 func (m *DeviceManager) doFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 320 if release.OnClassic { 321 // TODO: this may need to be lifted in the future 322 return fmt.Errorf("internal error: cannot finalize recovery systems on a classic system") 323 } 324 325 st := t.State() 326 st.Lock() 327 defer st.Unlock() 328 329 if ok, _ := st.Restarting(); ok { 330 // don't continue until we are in the restarted snapd 331 t.Logf("Waiting for system reboot...") 332 return &state.Retry{} 333 } 334 335 remodelCtx, err := DeviceCtx(st, t, nil) 336 if err != nil { 337 return err 338 } 339 isRemodel := remodelCtx.ForRemodeling() 340 341 var triedSystems []string 342 // after rebooting to the recovery system and back, the system got moved 343 // to the tried-systems list in the state 344 if err := st.Get("tried-systems", &triedSystems); err != nil { 345 return fmt.Errorf("cannot obtain tried recovery systems: %v", err) 346 } 347 348 setup, err := taskRecoverySystemSetup(t) 349 if err != nil { 350 return err 351 } 352 label := setup.Label 353 354 logger.Debugf("finalize recovery system with label %q", label) 355 356 if isRemodel { 357 // so far so good, a recovery system created during remodel was 358 // tested successfully 359 if !strutil.ListContains(triedSystems, label) { 360 // system failed, trigger undoing of everything we did so far 361 return fmt.Errorf("tried recovery system %q failed", label) 362 } 363 364 // XXX: candidate system is promoted to the list of good ones once we 365 // complete the whole remodel change 366 logger.Debugf("recovery system created during remodel will be promoted later") 367 } else { 368 if err := boot.PromoteTriedRecoverySystem(remodelCtx, label, triedSystems); err != nil { 369 return fmt.Errorf("cannot promote recovery system %q: %v", label, err) 370 } 371 372 // tried systems should be a one item list, we can clear it now 373 st.Set("tried-systems", nil) 374 } 375 376 // we are done 377 t.SetStatus(state.DoneStatus) 378 379 return nil 380 } 381 382 func (m *DeviceManager) undoFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 383 st := t.State() 384 st.Lock() 385 defer st.Unlock() 386 387 remodelCtx, err := DeviceCtx(st, t, nil) 388 if err != nil { 389 return err 390 } 391 392 setup, err := taskRecoverySystemSetup(t) 393 if err != nil { 394 return err 395 } 396 label := setup.Label 397 398 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 399 return fmt.Errorf("cannot drop a good recovery system %q: %v", label, err) 400 } 401 402 return nil 403 } 404 405 func (m *DeviceManager) cleanupRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 406 st := t.State() 407 st.Lock() 408 defer st.Unlock() 409 410 setup, err := taskRecoverySystemSetup(t) 411 if err != nil { 412 return err 413 } 414 if os.Remove(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil && !os.IsNotExist(err) { 415 return err 416 } 417 return nil 418 }