github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/overlord/devicestate/devicestate.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016-2019 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 implements the manager and state aspects responsible 21 // for the device identity and policies. 22 package devicestate 23 24 import ( 25 "context" 26 "fmt" 27 "path/filepath" 28 "sync" 29 30 "github.com/snapcore/snapd/asserts" 31 "github.com/snapcore/snapd/boot" 32 "github.com/snapcore/snapd/gadget" 33 "github.com/snapcore/snapd/i18n" 34 "github.com/snapcore/snapd/logger" 35 "github.com/snapcore/snapd/netutil" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/overlord/assertstate" 38 "github.com/snapcore/snapd/overlord/auth" 39 "github.com/snapcore/snapd/overlord/configstate/config" 40 "github.com/snapcore/snapd/overlord/devicestate/internal" 41 "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" 42 "github.com/snapcore/snapd/overlord/snapstate" 43 "github.com/snapcore/snapd/overlord/state" 44 "github.com/snapcore/snapd/release" 45 "github.com/snapcore/snapd/snap" 46 "github.com/snapcore/snapd/snap/naming" 47 ) 48 49 var ( 50 snapstateInstallWithDeviceContext = snapstate.InstallWithDeviceContext 51 snapstateUpdateWithDeviceContext = snapstate.UpdateWithDeviceContext 52 ) 53 54 // findModel returns the device model assertion. 55 func findModel(st *state.State) (*asserts.Model, error) { 56 device, err := internal.Device(st) 57 if err != nil { 58 return nil, err 59 } 60 61 if device.Brand == "" || device.Model == "" { 62 return nil, state.ErrNoState 63 } 64 65 a, err := assertstate.DB(st).Find(asserts.ModelType, map[string]string{ 66 "series": release.Series, 67 "brand-id": device.Brand, 68 "model": device.Model, 69 }) 70 if asserts.IsNotFound(err) { 71 return nil, state.ErrNoState 72 } 73 if err != nil { 74 return nil, err 75 } 76 77 return a.(*asserts.Model), nil 78 } 79 80 // findSerial returns the device serial assertion. device is optional and used instead of the global state if provided. 81 func findSerial(st *state.State, device *auth.DeviceState) (*asserts.Serial, error) { 82 if device == nil { 83 var err error 84 device, err = internal.Device(st) 85 if err != nil { 86 return nil, err 87 } 88 } 89 90 if device.Serial == "" { 91 return nil, state.ErrNoState 92 } 93 94 a, err := assertstate.DB(st).Find(asserts.SerialType, map[string]string{ 95 "brand-id": device.Brand, 96 "model": device.Model, 97 "serial": device.Serial, 98 }) 99 if asserts.IsNotFound(err) { 100 return nil, state.ErrNoState 101 } 102 if err != nil { 103 return nil, err 104 } 105 106 return a.(*asserts.Serial), nil 107 } 108 109 // auto-refresh 110 func canAutoRefresh(st *state.State) (bool, error) { 111 // we need to be seeded first 112 var seeded bool 113 st.Get("seeded", &seeded) 114 if !seeded { 115 return false, nil 116 } 117 118 // Either we have a serial or we try anyway if we attempted 119 // for a while to get a serial, this would allow us to at 120 // least upgrade core if that can help. 121 if ensureOperationalAttempts(st) >= 3 { 122 return true, nil 123 } 124 125 // Check model exists, for sanity. We always have a model, either 126 // seeded or a generic one that ships with snapd. 127 _, err := findModel(st) 128 if err == state.ErrNoState { 129 return false, nil 130 } 131 if err != nil { 132 return false, err 133 } 134 135 _, err = findSerial(st, nil) 136 if err == state.ErrNoState { 137 return false, nil 138 } 139 if err != nil { 140 return false, err 141 } 142 143 return true, nil 144 } 145 146 func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, _ snap.Container, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) error { 147 kind := "" 148 var snapType snap.Type 149 var getName func(*asserts.Model) string 150 switch snapInfo.Type() { 151 case snap.TypeGadget: 152 kind = "gadget" 153 snapType = snap.TypeGadget 154 getName = (*asserts.Model).Gadget 155 case snap.TypeKernel: 156 if release.OnClassic { 157 return fmt.Errorf("cannot install a kernel snap on classic") 158 } 159 160 kind = "kernel" 161 snapType = snap.TypeKernel 162 getName = (*asserts.Model).Kernel 163 default: 164 // not a relevant check 165 return nil 166 } 167 168 model := deviceCtx.Model() 169 170 if snapInfo.SnapID != "" { 171 snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID) 172 if err != nil { 173 return fmt.Errorf("internal error: cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err) 174 } 175 publisher := snapDecl.PublisherID() 176 if publisher != "canonical" && publisher != model.BrandID() { 177 return fmt.Errorf("cannot install %s %q published by %q for model by %q", kind, snapInfo.InstanceName(), publisher, model.BrandID()) 178 } 179 } else { 180 logger.Noticef("installing unasserted %s %q", kind, snapInfo.InstanceName()) 181 } 182 183 found, err := snapstate.HasSnapOfType(st, snapType) 184 if err != nil { 185 return fmt.Errorf("cannot detect original %s snap: %v", kind, err) 186 } 187 if found { 188 // already installed, snapstate takes care 189 return nil 190 } 191 // first installation of a gadget/kernel 192 193 expectedName := getName(model) 194 if expectedName == "" { // can happen only on classic 195 return fmt.Errorf("cannot install %s snap on classic if not requested by the model", kind) 196 } 197 198 if snapInfo.InstanceName() != snapInfo.SnapName() { 199 return fmt.Errorf("cannot install %q, parallel installation of kernel or gadget snaps is not supported", snapInfo.InstanceName()) 200 } 201 202 if snapInfo.InstanceName() != expectedName { 203 return fmt.Errorf("cannot install %s %q, model assertion requests %q", kind, snapInfo.InstanceName(), expectedName) 204 } 205 206 return nil 207 } 208 209 func checkGadgetValid(st *state.State, snapInfo, _ *snap.Info, snapf snap.Container, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) error { 210 if snapInfo.Type() != snap.TypeGadget { 211 // not a gadget, nothing to do 212 return nil 213 } 214 if deviceCtx.ForRemodeling() { 215 // in this case the gadget is checked by 216 // checkGadgetRemodelCompatible 217 return nil 218 } 219 220 // do basic validity checks on the gadget against its model constraints 221 _, err := gadget.ReadInfoFromSnapFile(snapf, deviceCtx.Model()) 222 return err 223 } 224 225 var once sync.Once 226 227 func delayedCrossMgrInit() { 228 once.Do(func() { 229 snapstate.AddCheckSnapCallback(checkGadgetOrKernel) 230 snapstate.AddCheckSnapCallback(checkGadgetValid) 231 snapstate.AddCheckSnapCallback(checkGadgetRemodelCompatible) 232 }) 233 snapstate.CanAutoRefresh = canAutoRefresh 234 snapstate.CanManageRefreshes = CanManageRefreshes 235 snapstate.IsOnMeteredConnection = netutil.IsOnMeteredConnection 236 snapstate.DeviceCtx = DeviceCtx 237 snapstate.Remodeling = Remodeling 238 } 239 240 // proxyStore returns the store assertion for the proxy store if one is set. 241 func proxyStore(st *state.State, tr *config.Transaction) (*asserts.Store, error) { 242 var proxyStore string 243 err := tr.GetMaybe("core", "proxy.store", &proxyStore) 244 if err != nil { 245 return nil, err 246 } 247 if proxyStore == "" { 248 return nil, state.ErrNoState 249 } 250 251 a, err := assertstate.DB(st).Find(asserts.StoreType, map[string]string{ 252 "store": proxyStore, 253 }) 254 if asserts.IsNotFound(err) { 255 return nil, state.ErrNoState 256 } 257 if err != nil { 258 return nil, err 259 } 260 261 return a.(*asserts.Store), nil 262 } 263 264 // interfaceConnected returns true if the given snap/interface names 265 // are connected 266 func interfaceConnected(st *state.State, snapName, ifName string) bool { 267 conns, err := ifacerepo.Get(st).Connected(snapName, ifName) 268 return err == nil && len(conns) > 0 269 } 270 271 // CanManageRefreshes returns true if the device can be 272 // switched to the "core.refresh.schedule=managed" mode. 273 // 274 // TODO: 275 // - Move the CanManageRefreshes code into the ifstate 276 // - Look at the connections and find the connection for snapd-control 277 // with the managed attribute 278 // - Take the snap from this connection and look at the snapstate to see 279 // if that snap has a snap declaration (to ensure it comes from the store) 280 func CanManageRefreshes(st *state.State) bool { 281 snapStates, err := snapstate.All(st) 282 if err != nil { 283 return false 284 } 285 for _, snapst := range snapStates { 286 // Always get the current info even if the snap is currently 287 // being operated on or if its disabled. 288 info, err := snapst.CurrentInfo() 289 if err != nil { 290 continue 291 } 292 if info.Broken != "" { 293 continue 294 } 295 // The snap must have a snap declaration (implies that 296 // its from the store) 297 if _, err := assertstate.SnapDeclaration(st, info.SideInfo.SnapID); err != nil { 298 continue 299 } 300 301 for _, plugInfo := range info.Plugs { 302 if plugInfo.Interface == "snapd-control" && plugInfo.Attrs["refresh-schedule"] == "managed" { 303 snapName := info.InstanceName() 304 plugName := plugInfo.Name 305 if interfaceConnected(st, snapName, plugName) { 306 return true 307 } 308 } 309 } 310 } 311 312 return false 313 } 314 315 func getAllRequiredSnapsForModel(model *asserts.Model) *naming.SnapSet { 316 reqSnaps := model.RequiredWithEssentialSnaps() 317 return naming.NewSnapSet(reqSnaps) 318 } 319 320 // extractDownloadInstallEdgesFromTs extracts the first, last download 321 // phase and install phase tasks from a TaskSet 322 func extractDownloadInstallEdgesFromTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) { 323 edgeTask, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge) 324 if err != nil { 325 return nil, nil, nil, nil, err 326 } 327 tasks := ts.Tasks() 328 // we know we always start with downloads 329 firstDl = tasks[0] 330 // and always end with installs 331 lastInst = tasks[len(tasks)-1] 332 333 var edgeTaskIndex int 334 for i, task := range tasks { 335 if task == edgeTask { 336 edgeTaskIndex = i 337 break 338 } 339 } 340 return firstDl, tasks[edgeTaskIndex], tasks[edgeTaskIndex+1], lastInst, nil 341 } 342 343 func notInstalled(st *state.State, name string) (bool, error) { 344 _, err := snapstate.CurrentInfo(st, name) 345 _, isNotInstalled := err.(*snap.NotInstalledError) 346 if isNotInstalled { 347 return true, nil 348 } 349 return false, err 350 } 351 352 func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext, fromChange string) ([]*state.TaskSet, error) { 353 userID := 0 354 var tss []*state.TaskSet 355 356 // kernel 357 if current.Kernel() == new.Kernel() && current.KernelTrack() != new.KernelTrack() { 358 ts, err := snapstateUpdateWithDeviceContext(st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange) 359 if err != nil { 360 return nil, err 361 } 362 tss = append(tss, ts) 363 } 364 365 var ts *state.TaskSet 366 if current.Kernel() != new.Kernel() { 367 needsInstall, err := notInstalled(st, new.Kernel()) 368 if err != nil { 369 return nil, err 370 } 371 if needsInstall { 372 ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange) 373 } else { 374 ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base()) 375 } 376 if err != nil { 377 return nil, err 378 } 379 tss = append(tss, ts) 380 } 381 if current.Base() != new.Base() { 382 needsInstall, err := notInstalled(st, new.Base()) 383 if err != nil { 384 return nil, err 385 } 386 if needsInstall { 387 ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Base(), nil, userID, snapstate.Flags{}, deviceCtx, fromChange) 388 } else { 389 ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base()) 390 } 391 if err != nil { 392 return nil, err 393 } 394 tss = append(tss, ts) 395 } 396 // gadget 397 if current.Gadget() == new.Gadget() && current.GadgetTrack() != new.GadgetTrack() { 398 ts, err := snapstateUpdateWithDeviceContext(st, new.Gadget(), &snapstate.RevisionOptions{Channel: new.GadgetTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange) 399 if err != nil { 400 return nil, err 401 } 402 tss = append(tss, ts) 403 } 404 if current.Gadget() != new.Gadget() { 405 ts, err := snapstateInstallWithDeviceContext(ctx, st, new.Gadget(), &snapstate.RevisionOptions{Channel: new.GadgetTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange) 406 if err != nil { 407 return nil, err 408 } 409 tss = append(tss, ts) 410 } 411 412 // add new required-snaps, no longer required snaps will be cleaned 413 // in "set-model" 414 for _, snapRef := range new.RequiredNoEssentialSnaps() { 415 // TODO|XXX: have methods that take refs directly 416 // to respect the snap ids 417 needsInstall, err := notInstalled(st, snapRef.SnapName()) 418 if err != nil { 419 return nil, err 420 } 421 if needsInstall { 422 // If the snap is not installed we need to install it now. 423 ts, err := snapstateInstallWithDeviceContext(ctx, st, snapRef.SnapName(), nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange) 424 if err != nil { 425 return nil, err 426 } 427 tss = append(tss, ts) 428 } 429 } 430 // TODO: Validate that all bases and default-providers are part 431 // of the install tasksets and error if not. If the 432 // prereq task handler check starts adding installs into 433 // our remodel change our carefully constructed wait chain 434 // breaks down. 435 436 // Keep track of downloads tasks carrying snap-setup which is needed for 437 // recovery system tasks 438 var snapSetupTasks []string 439 440 // Ensure all download/check tasks are run *before* the install 441 // tasks. During a remodel the network may not be available so 442 // we need to ensure we have everything local. 443 var lastDownloadInChain, firstInstallInChain *state.Task 444 var prevDownload, prevInstall *state.Task 445 for _, ts := range tss { 446 // make sure all things happen sequentially 447 // Terminology 448 // A <- B means B waits for A 449 // "download,verify" are part of the "Download" phase 450 // "link,start" is part of "Install" phase 451 // 452 // - all tasks inside ts{Download,Install} already wait for 453 // each other so the chains look something like this: 454 // download1 <- verify1 <- install1 455 // download2 <- verify2 <- install2 456 // download3 <- verify3 <- install3 457 // - add wait of each first ts{Download,Install} task for 458 // the last previous ts{Download,Install} task 459 // Our chains now looks like: 460 // download1 <- verify1 <- install1 (as before) 461 // download2 <- verify2 <- install2 (as before) 462 // download3 <- verify3 <- install3 (as before) 463 // verify1 <- download2 (added) 464 // verify2 <- download3 (added) 465 // install1 <- install2 (added) 466 // install2 <- install3 (added) 467 downloadStart, downloadLast, installFirst, installLast, err := extractDownloadInstallEdgesFromTs(ts) 468 if err != nil { 469 return nil, fmt.Errorf("cannot remodel: %v", err) 470 } 471 if prevDownload != nil { 472 // XXX: we don't strictly need to serialize the download 473 downloadStart.WaitFor(prevDownload) 474 } 475 if prevInstall != nil { 476 installFirst.WaitFor(prevInstall) 477 } 478 prevDownload = downloadLast 479 prevInstall = installLast 480 // update global state 481 lastDownloadInChain = downloadLast 482 if firstInstallInChain == nil { 483 firstInstallInChain = installFirst 484 } 485 // download is always a first task of the 'download' phase 486 snapSetupTasks = append(snapSetupTasks, downloadStart.ID()) 487 } 488 // Make sure the first install waits for the recovery system (only in 489 // UC20) which waits for the last download. With this our (simplified) 490 // wait chain looks like this: 491 // 492 // download1 493 // ^- verify1 494 // ^- download2 495 // ^- verify2 496 // ^- download3 497 // ^- verify3 498 // ^- recovery (UC20) 499 // ^- install1 500 // ^- install2 501 // ^- install3 502 if firstInstallInChain != nil && lastDownloadInChain != nil { 503 firstInstallInChain.WaitFor(lastDownloadInChain) 504 } 505 506 recoverySetupTaskID := "" 507 if new.Grade() != asserts.ModelGradeUnset { 508 // create a recovery when remodeling to a UC20 system, actual 509 // policy for possible remodels has already been verified by the 510 // caller 511 label := timeNow().Format("20060102") 512 createRecoveryTasks, err := createRecoverySystemTasks(st, label, snapSetupTasks) 513 if err != nil { 514 return nil, err 515 } 516 if lastDownloadInChain != nil { 517 // wait for all snaps that need to be downloaded 518 createRecoveryTasks.WaitFor(lastDownloadInChain) 519 } 520 if firstInstallInChain != nil { 521 // when any snap installations need to happen, they 522 // should also wait for recovery system to be created 523 firstInstallInChain.WaitAll(createRecoveryTasks) 524 } 525 tss = append(tss, createRecoveryTasks) 526 recoverySetupTaskID = createRecoveryTasks.Tasks()[0].ID() 527 } 528 529 // Set the new model assertion - this *must* be the last thing done 530 // by the change. 531 setModel := st.NewTask("set-model", i18n.G("Set new model assertion")) 532 for _, tsPrev := range tss { 533 setModel.WaitAll(tsPrev) 534 } 535 if recoverySetupTaskID != "" { 536 // set model needs to access information about the recovery 537 // system 538 setModel.Set("recovery-system-setup-task", recoverySetupTaskID) 539 } 540 tss = append(tss, state.NewTaskSet(setModel)) 541 542 return tss, nil 543 } 544 545 var allowUC20RemodelTesting = false 546 547 // AllowUC20RemodelTesting is a temporary helper to allow testing remodeling of 548 // UC20 before the implementation is complete and the policy for this settled. 549 // It will be removed once remodel is fully implemented and made available for 550 // general use. 551 func AllowUC20RemodelTesting(allow bool) (restore func()) { 552 osutil.MustBeTestBinary("uc20 remodel testing only can be mocked in tests") 553 oldAllowUC20RemodelTesting := allowUC20RemodelTesting 554 allowUC20RemodelTesting = allow 555 return func() { 556 allowUC20RemodelTesting = oldAllowUC20RemodelTesting 557 } 558 } 559 560 // Remodel takes a new model assertion and generates a change that 561 // takes the device from the old to the new model or an error if the 562 // transition is not possible. 563 // 564 // TODO: 565 // - Check estimated disk size delta 566 // - Check all relevant snaps exist in new store 567 // (need to check that even unchanged snaps are accessible) 568 // - Make sure this works with Core 20 as well, in the Core 20 case 569 // we must enforce the default-channels from the model as well 570 func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) { 571 var seeded bool 572 err := st.Get("seeded", &seeded) 573 if err != nil && err != state.ErrNoState { 574 return nil, err 575 } 576 if !seeded { 577 return nil, fmt.Errorf("cannot remodel until fully seeded") 578 } 579 580 current, err := findModel(st) 581 if err != nil { 582 return nil, err 583 } 584 585 if _, err := findSerial(st, nil); err != nil { 586 if err == state.ErrNoState { 587 return nil, fmt.Errorf("cannot remodel without a serial") 588 } 589 return nil, err 590 } 591 592 if current.Series() != new.Series() { 593 return nil, fmt.Errorf("cannot remodel to different series yet") 594 } 595 596 // TODO:UC20: support remodel, also ensure we never remodel to a lower 597 // grade 598 if !allowUC20RemodelTesting { 599 if current.Grade() != asserts.ModelGradeUnset { 600 return nil, fmt.Errorf("cannot remodel Ubuntu Core 20 models yet") 601 } 602 if new.Grade() != asserts.ModelGradeUnset { 603 return nil, fmt.Errorf("cannot remodel to Ubuntu Core 20 models yet") 604 } 605 } else { 606 // also disallows remodel from non-UC20 (grade unset) to UC20 607 if current.Grade() != new.Grade() { 608 return nil, fmt.Errorf("cannot remodel from grade %v to grade %v", current.Grade(), new.Grade()) 609 } 610 } 611 612 // TODO: we need dedicated assertion language to permit for 613 // model transitions before we allow cross vault 614 // transitions. 615 616 remodelKind := ClassifyRemodel(current, new) 617 618 // TODO: should we restrict remodel from one arch to another? 619 // There are valid use-cases here though, i.e. amd64 machine that 620 // remodels itself to/from i386 (if the HW can do both 32/64 bit) 621 if current.Architecture() != new.Architecture() { 622 return nil, fmt.Errorf("cannot remodel to different architectures yet") 623 } 624 625 // calculate snap differences between the two models 626 // FIXME: this needs work to switch from core->bases 627 if current.Base() == "" && new.Base() != "" { 628 return nil, fmt.Errorf("cannot remodel from core to bases yet") 629 } 630 631 // Do we do this only for the more complicated cases (anything 632 // more than adding required-snaps really)? 633 if err := snapstate.CheckChangeConflictRunExclusively(st, "remodel"); err != nil { 634 return nil, err 635 } 636 637 remodCtx, err := remodelCtx(st, current, new) 638 if err != nil { 639 return nil, err 640 } 641 642 var tss []*state.TaskSet 643 switch remodelKind { 644 case ReregRemodel: 645 requestSerial := st.NewTask("request-serial", i18n.G("Request new device serial")) 646 647 prepare := st.NewTask("prepare-remodeling", i18n.G("Prepare remodeling")) 648 prepare.WaitFor(requestSerial) 649 ts := state.NewTaskSet(requestSerial, prepare) 650 tss = []*state.TaskSet{ts} 651 case StoreSwitchRemodel: 652 sto := remodCtx.Store() 653 if sto == nil { 654 return nil, fmt.Errorf("internal error: a store switch remodeling should have built a store") 655 } 656 // ensure a new session accounting for the new brand store 657 st.Unlock() 658 _, err := sto.EnsureDeviceSession() 659 st.Lock() 660 if err != nil { 661 return nil, fmt.Errorf("cannot get a store session based on the new model assertion: %v", err) 662 } 663 fallthrough 664 case UpdateRemodel: 665 var err error 666 tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "") 667 if err != nil { 668 return nil, err 669 } 670 } 671 672 // we potentially released the lock a couple of times here: 673 // make sure the current model is essentially the same as when 674 // we started 675 current1, err := findModel(st) 676 if err != nil { 677 return nil, err 678 } 679 if current.BrandID() != current1.BrandID() || current.Model() != current1.Model() || current.Revision() != current1.Revision() { 680 return nil, &snapstate.ChangeConflictError{Message: fmt.Sprintf("cannot start remodel, clashing with concurrent remodel to %v/%v (%v)", current1.BrandID(), current1.Model(), current1.Revision())} 681 } 682 // make sure another unfinished remodel wasn't already setup either 683 if Remodeling(st) { 684 return nil, &snapstate.ChangeConflictError{Message: "cannot start remodel, clashing with concurrent one"} 685 } 686 687 var msg string 688 if current.BrandID() == new.BrandID() && current.Model() == new.Model() { 689 msg = fmt.Sprintf(i18n.G("Refresh model assertion from revision %v to %v"), current.Revision(), new.Revision()) 690 } else { 691 msg = fmt.Sprintf(i18n.G("Remodel device to %v/%v (%v)"), new.BrandID(), new.Model(), new.Revision()) 692 } 693 694 chg := st.NewChange("remodel", msg) 695 remodCtx.Init(chg) 696 for _, ts := range tss { 697 chg.AddAll(ts) 698 } 699 700 return chg, nil 701 } 702 703 // Remodeling returns true whether there's a remodeling in progress 704 func Remodeling(st *state.State) bool { 705 for _, chg := range st.Changes() { 706 if !chg.IsReady() && chg.Kind() == "remodel" { 707 return true 708 } 709 } 710 return false 711 } 712 713 type recoverySystemSetup struct { 714 // Label of the recovery system, selected when tasks are created 715 Label string `json:"label"` 716 // Directory inside the seed filesystem where the recovery system files 717 // are kept, typically /run/mnt/ubuntu-seed/systems/<label>, set when 718 // tasks are created 719 Directory string `json:"directory"` 720 // SnapSetupTasks is a list of task IDs that carry snap setup 721 // information, relevant only during remodel, set when tasks are created 722 SnapSetupTasks []string `json:"snap-setup-tasks"` 723 } 724 725 func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []string) (*state.TaskSet, error) { 726 // sanity check, the directory should not exist yet 727 // TODO: we should have a common helper to derive this path 728 systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", label) 729 exists, _, err := osutil.DirExists(systemDirectory) 730 if err != nil { 731 return nil, err 732 } 733 if exists { 734 return nil, fmt.Errorf("recovery system %q already exists", label) 735 } 736 737 create := st.NewTask("create-recovery-system", fmt.Sprintf("Create recovery system with label %q", label)) 738 // the label we want 739 create.Set("recovery-system-setup", &recoverySystemSetup{ 740 Label: label, 741 Directory: systemDirectory, 742 // IDs of the tasks carrying snap-setup 743 SnapSetupTasks: snapSetupTasks, 744 }) 745 746 finalize := st.NewTask("finalize-recovery-system", fmt.Sprintf("Finalize recovery system with label %q", label)) 747 finalize.WaitFor(create) 748 // finalize needs to know the label too 749 finalize.Set("recovery-system-setup-task", create.ID()) 750 return state.NewTaskSet(create, finalize), nil 751 } 752 753 func CreateRecoverySystem(st *state.State, label string) (*state.Change, error) { 754 var seeded bool 755 err := st.Get("seeded", &seeded) 756 if err != nil && err != state.ErrNoState { 757 return nil, err 758 } 759 if !seeded { 760 return nil, fmt.Errorf("cannot create new recovery systems until fully seeded") 761 } 762 chg := st.NewChange("create-recovery-system", fmt.Sprintf("Create new recovery system with label %q", label)) 763 ts, err := createRecoverySystemTasks(st, label, nil) 764 if err != nil { 765 return nil, err 766 } 767 chg.AddAll(ts) 768 return chg, nil 769 }