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