github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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 db := assertstate.DB(st) 208 defer func() { 209 if err == nil { 210 return 211 } 212 if err := purgeNewSystemSnapFiles(filepath.Join(systemDirectory, "snapd-new-file-log")); err != nil { 213 logger.Noticef("when removing seed files: %v", err) 214 } 215 // this is ok, as before the change with this task was created, 216 // we checked that the system directory did not exist; it may 217 // exist now if one of the post-create steps failed, or the the 218 // task is being re-run after a reboot and creating a system 219 // failed 220 if err := os.RemoveAll(systemDirectory); err != nil && !os.IsNotExist(err) { 221 logger.Noticef("when removing recovery system %q: %v", label, err) 222 } 223 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 224 logger.Noticef("when dropping the recovery system %q: %v", label, err) 225 } 226 // we could have reentered the task after a reboot, but the 227 // state was set up sufficiently such that the system was 228 // actually tried and ended up in the tried systems list, which 229 // we should reset now 230 st.Set("tried-systems", nil) 231 }() 232 // 1. prepare recovery system from remodel snaps (or current snaps) 233 // TODO: this fails when there is a partially complete system seed which 234 // creation could have been interrupted by an unexpected reboot; 235 // consider clearing the recovery system directory and restarting from 236 // scratch 237 _, err = createSystemForModelFromValidatedSnaps(model, label, db, infoGetter, observeSnapFileWrite) 238 if err != nil { 239 return fmt.Errorf("cannot create a recovery system with label %q for %v: %v", label, model.Model(), err) 240 } 241 logger.Debugf("recovery system dir: %v", systemDirectory) 242 243 // 2. keep track of the system in task state 244 if err := setTaskRecoverySystemSetup(t, setup); err != nil { 245 return fmt.Errorf("cannot record recovery system setup state: %v", err) 246 } 247 // 3. set up boot variables for tracking the tried system state 248 if err := boot.SetTryRecoverySystem(remodelCtx, label); err != nil { 249 // rollback? 250 return fmt.Errorf("cannot attempt booting into recovery system %q: %v", label, err) 251 } 252 // 4. and set up the next boot that that system 253 if err := boot.SetRecoveryBootSystemAndMode(remodelCtx, label, "recover"); err != nil { 254 return fmt.Errorf("cannot set device to boot into candidate system %q: %v", label, err) 255 } 256 257 // this task is done, further processing happens in finalize 258 t.SetStatus(state.DoneStatus) 259 260 logger.Noticef("restarting into candidate system %q", label) 261 m.state.RequestRestart(state.RestartSystemNow) 262 return nil 263 } 264 265 func (m *DeviceManager) undoCreateRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 266 if release.OnClassic { 267 // TODO: this may need to be lifted in the future 268 return fmt.Errorf("internal error: cannot create recovery systems on a classic system") 269 } 270 271 st := t.State() 272 st.Lock() 273 defer st.Unlock() 274 275 remodelCtx, err := DeviceCtx(st, t, nil) 276 if err != nil { 277 return err 278 } 279 280 setup, err := taskRecoverySystemSetup(t) 281 if err != nil { 282 return fmt.Errorf("internal error: cannot obtain recovery system setup information") 283 } 284 label := setup.Label 285 286 var undoErr error 287 288 if err := purgeNewSystemSnapFiles(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil { 289 t.Logf("when removing seed files: %v", err) 290 } 291 if err := os.RemoveAll(setup.Directory); err != nil && !os.IsNotExist(err) { 292 t.Logf("when removing recovery system %q: %v", label, err) 293 undoErr = err 294 } else { 295 t.Logf("removed recovery system directory %v", setup.Directory) 296 } 297 298 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 299 return fmt.Errorf("cannot drop a current recovery system %q: %v", label, err) 300 } 301 302 return undoErr 303 } 304 305 func (m *DeviceManager) doFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 306 if release.OnClassic { 307 // TODO: this may need to be lifted in the future 308 return fmt.Errorf("internal error: cannot finalize recovery systems on a classic system") 309 } 310 311 st := t.State() 312 st.Lock() 313 defer st.Unlock() 314 315 if ok, _ := st.Restarting(); ok { 316 // don't continue until we are in the restarted snapd 317 t.Logf("Waiting for system reboot...") 318 return &state.Retry{} 319 } 320 321 remodelCtx, err := DeviceCtx(st, t, nil) 322 if err != nil { 323 return err 324 } 325 isRemodel := remodelCtx.ForRemodeling() 326 327 var triedSystems []string 328 // after rebooting to the recovery system and back, the system got moved 329 // to the tried-systems list in the state 330 if err := st.Get("tried-systems", &triedSystems); err != nil { 331 return fmt.Errorf("cannot obtain tried recovery systems: %v", err) 332 } 333 334 setup, err := taskRecoverySystemSetup(t) 335 if err != nil { 336 return err 337 } 338 label := setup.Label 339 340 logger.Debugf("finalize recovery system with label %q", label) 341 342 if isRemodel { 343 // so far so good, a recovery system created during remodel was 344 // tested successfully 345 if !strutil.ListContains(triedSystems, label) { 346 // system failed, trigger undoing of everything we did so far 347 return fmt.Errorf("tried recovery system %q failed", label) 348 } 349 350 // XXX: candidate system is promoted to the list of good ones once we 351 // complete the whole remodel change 352 logger.Debugf("recovery system created during remodel will be promoted later") 353 } else { 354 if err := boot.PromoteTriedRecoverySystem(remodelCtx, label, triedSystems); err != nil { 355 return fmt.Errorf("cannot promote recovery system %q: %v", label, err) 356 } 357 358 // tried systems should be a one item list, we can clear it now 359 st.Set("tried-systems", nil) 360 } 361 362 // we are done 363 t.SetStatus(state.DoneStatus) 364 365 return nil 366 } 367 368 func (m *DeviceManager) undoFinalizeTriedRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 369 st := t.State() 370 st.Lock() 371 defer st.Unlock() 372 373 remodelCtx, err := DeviceCtx(st, t, nil) 374 if err != nil { 375 return err 376 } 377 378 setup, err := taskRecoverySystemSetup(t) 379 if err != nil { 380 return err 381 } 382 label := setup.Label 383 384 if err := boot.DropRecoverySystem(remodelCtx, label); err != nil { 385 return fmt.Errorf("cannot drop a good recovery system %q: %v", label, err) 386 } 387 388 return nil 389 } 390 391 func (m *DeviceManager) cleanupRecoverySystem(t *state.Task, _ *tomb.Tomb) error { 392 st := t.State() 393 st.Lock() 394 defer st.Unlock() 395 396 setup, err := taskRecoverySystemSetup(t) 397 if err != nil { 398 return err 399 } 400 if os.Remove(filepath.Join(setup.Directory, "snapd-new-file-log")); err != nil && !os.IsNotExist(err) { 401 return err 402 } 403 return nil 404 }