github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/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 // Ensure all download/check tasks are run *before* the install 437 // tasks. During a remodel the network may not be available so 438 // we need to ensure we have everything local. 439 var lastDownloadInChain, firstInstallInChain *state.Task 440 var prevDownload, prevInstall *state.Task 441 for _, ts := range tss { 442 // make sure all things happen sequentially 443 // Terminology 444 // A <- B means B waits for A 445 // "download,verify" are part of the "Download" phase 446 // "link,start" is part of "Install" phase 447 // 448 // - all tasks inside ts{Download,Install} already wait for 449 // each other so the chains look something like this: 450 // download1 <- verify1 <- install1 451 // download2 <- verify2 <- install2 452 // download3 <- verify3 <- install3 453 // - add wait of each first ts{Download,Install} task for 454 // the last previous ts{Download,Install} task 455 // Our chains now looks like: 456 // download1 <- verify1 <- install1 (as before) 457 // download2 <- verify2 <- install2 (as before) 458 // download3 <- verify3 <- install3 (as before) 459 // verify1 <- download2 (added) 460 // verify2 <- download3 (added) 461 // install1 <- install2 (added) 462 // install2 <- install3 (added) 463 downloadStart, downloadLast, installFirst, installLast, err := extractDownloadInstallEdgesFromTs(ts) 464 if err != nil { 465 return nil, fmt.Errorf("cannot remodel: %v", err) 466 } 467 if prevDownload != nil { 468 // XXX: we don't strictly need to serialize the download 469 downloadStart.WaitFor(prevDownload) 470 } 471 if prevInstall != nil { 472 installFirst.WaitFor(prevInstall) 473 } 474 prevDownload = downloadLast 475 prevInstall = installLast 476 // update global state 477 lastDownloadInChain = downloadLast 478 if firstInstallInChain == nil { 479 firstInstallInChain = installFirst 480 } 481 } 482 // Make sure the first install waits for the last download. With this 483 // our (simplified) wait chain looks like: 484 // download1 <- verify1 <- download2 <- verify2 <- download3 <- verify3 <- install1 <- install2 <- install3 485 if firstInstallInChain != nil && lastDownloadInChain != nil { 486 firstInstallInChain.WaitFor(lastDownloadInChain) 487 } 488 489 // Set the new model assertion - this *must* be the last thing done 490 // by the change. 491 setModel := st.NewTask("set-model", i18n.G("Set new model assertion")) 492 for _, tsPrev := range tss { 493 setModel.WaitAll(tsPrev) 494 } 495 tss = append(tss, state.NewTaskSet(setModel)) 496 497 return tss, nil 498 } 499 500 // Remodel takes a new model assertion and generates a change that 501 // takes the device from the old to the new model or an error if the 502 // transition is not possible. 503 // 504 // TODO: 505 // - Check estimated disk size delta 506 // - Check all relevant snaps exist in new store 507 // (need to check that even unchanged snaps are accessible) 508 // - Make sure this works with Core 20 as well, in the Core 20 case 509 // we must enforce the default-channels from the model as well 510 func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) { 511 var seeded bool 512 err := st.Get("seeded", &seeded) 513 if err != nil && err != state.ErrNoState { 514 return nil, err 515 } 516 if !seeded { 517 return nil, fmt.Errorf("cannot remodel until fully seeded") 518 } 519 520 current, err := findModel(st) 521 if err != nil { 522 return nil, err 523 } 524 525 if _, err := findSerial(st, nil); err != nil { 526 if err == state.ErrNoState { 527 return nil, fmt.Errorf("cannot remodel without a serial") 528 } 529 return nil, err 530 } 531 532 if current.Series() != new.Series() { 533 return nil, fmt.Errorf("cannot remodel to different series yet") 534 } 535 536 // TODO:UC20: support remodel, also ensure we never remodel to a lower 537 // grade 538 if current.Grade() != asserts.ModelGradeUnset { 539 return nil, fmt.Errorf("cannot remodel Ubuntu Core 20 models yet") 540 } 541 if new.Grade() != asserts.ModelGradeUnset { 542 return nil, fmt.Errorf("cannot remodel to Ubuntu Core 20 models yet") 543 } 544 545 // TODO: we need dedicated assertion language to permit for 546 // model transitions before we allow cross vault 547 // transitions. 548 549 remodelKind := ClassifyRemodel(current, new) 550 551 // TODO: should we restrict remodel from one arch to another? 552 // There are valid use-cases here though, i.e. amd64 machine that 553 // remodels itself to/from i386 (if the HW can do both 32/64 bit) 554 if current.Architecture() != new.Architecture() { 555 return nil, fmt.Errorf("cannot remodel to different architectures yet") 556 } 557 558 // calculate snap differences between the two models 559 // FIXME: this needs work to switch from core->bases 560 if current.Base() == "" && new.Base() != "" { 561 return nil, fmt.Errorf("cannot remodel from core to bases yet") 562 } 563 564 // Do we do this only for the more complicated cases (anything 565 // more than adding required-snaps really)? 566 if err := snapstate.CheckChangeConflictRunExclusively(st, "remodel"); err != nil { 567 return nil, err 568 } 569 570 remodCtx, err := remodelCtx(st, current, new) 571 if err != nil { 572 return nil, err 573 } 574 575 var tss []*state.TaskSet 576 switch remodelKind { 577 case ReregRemodel: 578 requestSerial := st.NewTask("request-serial", i18n.G("Request new device serial")) 579 580 prepare := st.NewTask("prepare-remodeling", i18n.G("Prepare remodeling")) 581 prepare.WaitFor(requestSerial) 582 ts := state.NewTaskSet(requestSerial, prepare) 583 tss = []*state.TaskSet{ts} 584 case StoreSwitchRemodel: 585 sto := remodCtx.Store() 586 if sto == nil { 587 return nil, fmt.Errorf("internal error: a store switch remodeling should have built a store") 588 } 589 // ensure a new session accounting for the new brand store 590 st.Unlock() 591 _, err := sto.EnsureDeviceSession() 592 st.Lock() 593 if err != nil { 594 return nil, fmt.Errorf("cannot get a store session based on the new model assertion: %v", err) 595 } 596 fallthrough 597 case UpdateRemodel: 598 var err error 599 tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "") 600 if err != nil { 601 return nil, err 602 } 603 } 604 605 // we potentially released the lock a couple of times here: 606 // make sure the current model is essentially the same as when 607 // we started 608 current1, err := findModel(st) 609 if err != nil { 610 return nil, err 611 } 612 if current.BrandID() != current1.BrandID() || current.Model() != current1.Model() || current.Revision() != current1.Revision() { 613 return nil, &snapstate.ChangeConflictError{Message: fmt.Sprintf("cannot start remodel, clashing with concurrent remodel to %v/%v (%v)", current1.BrandID(), current1.Model(), current1.Revision())} 614 } 615 // make sure another unfinished remodel wasn't already setup either 616 if Remodeling(st) { 617 return nil, &snapstate.ChangeConflictError{Message: "cannot start remodel, clashing with concurrent one"} 618 } 619 620 var msg string 621 if current.BrandID() == new.BrandID() && current.Model() == new.Model() { 622 msg = fmt.Sprintf(i18n.G("Refresh model assertion from revision %v to %v"), current.Revision(), new.Revision()) 623 } else { 624 msg = fmt.Sprintf(i18n.G("Remodel device to %v/%v (%v)"), new.BrandID(), new.Model(), new.Revision()) 625 } 626 627 chg := st.NewChange("remodel", msg) 628 remodCtx.Init(chg) 629 for _, ts := range tss { 630 chg.AddAll(ts) 631 } 632 633 return chg, nil 634 } 635 636 // Remodeling returns true whether there's a remodeling in progress 637 func Remodeling(st *state.State) bool { 638 for _, chg := range st.Changes() { 639 if !chg.IsReady() && chg.Kind() == "remodel" { 640 return true 641 } 642 } 643 return false 644 } 645 646 type recoverySystemSetup struct { 647 // Label of the recovery system, selected when tasks are created 648 Label string `json:"label"` 649 // Directory inside the seed filesystem where the recovery system files 650 // are kept, typically /run/mnt/ubuntu-seed/systems/<label>, set when 651 // tasks are created 652 Directory string `json:"directory"` 653 // SnapSetupTasks is a list of task IDs that carry snap setup 654 // information, relevant only during remodel, set when tasks are created 655 SnapSetupTasks []string `json:"snap-setup-tasks"` 656 } 657 658 func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []string) (*state.TaskSet, error) { 659 // sanity check, the directory should not exist yet 660 // TODO: we should have a common helper to derive this path 661 systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", label) 662 exists, _, err := osutil.DirExists(systemDirectory) 663 if err != nil { 664 return nil, err 665 } 666 if exists { 667 return nil, fmt.Errorf("recovery system %q already exists", label) 668 } 669 670 create := st.NewTask("create-recovery-system", fmt.Sprintf("Create recovery system with label %q", label)) 671 // the label we want 672 create.Set("recovery-system-setup", &recoverySystemSetup{ 673 Label: label, 674 Directory: systemDirectory, 675 // IDs of the tasks carrying snap-setup 676 SnapSetupTasks: snapSetupTasks, 677 }) 678 679 finalize := st.NewTask("finalize-recovery-system", fmt.Sprintf("Finalize recovery system with label %q", label)) 680 finalize.WaitFor(create) 681 // finalize needs to know the label too 682 finalize.Set("recovery-system-setup-task", create.ID()) 683 return state.NewTaskSet(create, finalize), nil 684 } 685 686 func CreateRecoverySystem(st *state.State, label string) (*state.Change, error) { 687 var seeded bool 688 err := st.Get("seeded", &seeded) 689 if err != nil && err != state.ErrNoState { 690 return nil, err 691 } 692 if !seeded { 693 return nil, fmt.Errorf("cannot create new recovery systems until fully seeded") 694 } 695 chg := st.NewChange("create-recovery-system", fmt.Sprintf("Create new recovery system with label %q", label)) 696 ts, err := createRecoverySystemTasks(st, label, nil) 697 if err != nil { 698 return nil, err 699 } 700 chg.AddAll(ts) 701 return chg, nil 702 }