diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index 542f8a9a157..b1288718443 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -2026,7 +2026,7 @@ func (s *sideloadSuite) TestSideloadManyOnlyComponents(c *check.C) { st.Lock() defer st.Unlock() - expectedFileNames := []string{"one+comp-one.comp.comp", "one+comp-two.comp.comp", "one+comp-three.comp.comp", "one+comp-four.comp.comp"} + expectedFileNames := []string{"one+comp-one.comp", "one+comp-two.comp", "one+comp-three.comp", "one+comp-four.comp"} fullComponentNames := make([]string, len(components)) for i, c := range components { diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index a6f31e9ce3a..f91c815f4f7 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -63,6 +63,7 @@ var ( snapstateSwitch = snapstate.Switch snapstateUpdatePathWithDeviceContext = snapstate.UpdatePathWithDeviceContext snapstateDownload = snapstate.Download + snapstateDownloadComponents = snapstate.DownloadComponents ) // findModel returns the device model assertion. @@ -1150,7 +1151,9 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo } // we don't pass in the list of local snaps here because they are // already represented by snapSetupTasks - createRecoveryTasks, err := createRecoverySystemTasks(st, label, snapSetupTasks, CreateRecoverySystemOptions{ + + // TODO:COMPS - pass in the list of component setup tasks + createRecoveryTasks, err := createRecoverySystemTasks(st, label, snapSetupTasks, nil, CreateRecoverySystemOptions{ TestSystem: true, }) if err != nil { @@ -1498,10 +1501,14 @@ type recoverySystemSetup struct { // SnapSetupTasks is a list of task IDs that carry snap setup information. // Tasks could come from a remodel, or from downloading snaps that were // required by a validation set. - SnapSetupTasks []string `json:"snap-setup-tasks"` + SnapSetupTasks []string `json:"snap-setup-tasks,omitempty"` // LocalSnaps is a list of snaps that should be used to create the recovery // system. LocalSnaps []LocalSnap `json:"local-snaps,omitempty"` + // ComponentSetupTasks is a list of task IDs that carry component setup + // information. Tasks could come from a remodel, or from downloading + // components that were required by a validation set. + ComponentSetupTasks []string `json:"component-setup-tasks,omitempty"` // TestSystem is set to true if the new recovery system should // not be verified by rebooting into the new system. Once the system is // created, it will immediately be considered a valid recovery system. @@ -1553,7 +1560,7 @@ func removeRecoverySystemTasks(st *state.State, label string) (*state.TaskSet, e return state.NewTaskSet(remove), nil } -func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []string, opts CreateRecoverySystemOptions) (*state.TaskSet, error) { +func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks, compSetupTasks []string, opts CreateRecoverySystemOptions) (*state.TaskSet, error) { // precondition check, the directory should not exist yet systemDirectory := filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", label) exists, _, err := osutil.DirExists(systemDirectory) @@ -1570,10 +1577,11 @@ func createRecoverySystemTasks(st *state.State, label string, snapSetupTasks []s Label: label, Directory: systemDirectory, // IDs of the tasks carrying snap-setup - SnapSetupTasks: snapSetupTasks, - LocalSnaps: opts.LocalSnaps, - TestSystem: opts.TestSystem, - MarkDefault: opts.MarkDefault, + SnapSetupTasks: snapSetupTasks, + ComponentSetupTasks: compSetupTasks, + LocalSnaps: opts.LocalSnaps, + TestSystem: opts.TestSystem, + MarkDefault: opts.MarkDefault, }) ts := state.NewTaskSet(create) @@ -1667,6 +1675,21 @@ func RemoveRecoverySystem(st *state.State, label string) (*state.Change, error) return chg, nil } +func checkForRequiredSnapsNotPresentInModel(model *asserts.Model, vSets *snapasserts.ValidationSets) error { + snapsInModel := make(map[string]bool, len(model.AllSnaps())) + for _, sn := range model.AllSnaps() { + snapsInModel[sn.SnapName()] = true + } + + for _, sn := range vSets.RequiredSnaps() { + if !snapsInModel[sn] { + return fmt.Errorf("missing required snap in model: %s", sn) + } + } + + return nil +} + // CreateRecoverySystem creates a new recovery system with the given label. See // CreateRecoverySystemOptions for details on the options that can be provided. func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySystemOptions) (chg *state.Change, err error) { @@ -1705,11 +1728,6 @@ func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySyst return nil, err } - revisions, err := valsets.Revisions() - if err != nil { - return nil, err - } - // TODO: this restriction should be lifted eventually (in the case that we // have a dangerous model), and we should fall back to using snap names in // places that IDs are used @@ -1722,64 +1740,118 @@ func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySyst return nil, err } + // the task that creates the recovery system doesn't know anything about + // validation sets, so we cannot create systems with snaps that are not in + // the model. + if err := checkForRequiredSnapsNotPresentInModel(model, valsets); err != nil { + return nil, err + } + tracker := snap.NewSelfContainedSetPrereqTracker() + validRevision := func(current snap.Revision, constraints snapasserts.PresenceConstraint) bool { + return constraints.Revision.Unset() || current == constraints.Revision + } + var downloadTSS []*state.TaskSet for _, sn := range model.AllSnaps() { - rev := revisions[sn.Name] + constraints, err := valsets.Presence(sn) + if err != nil { + return nil, err + } - needsInstall, err := snapNeedsInstall(st, sn.Name, rev) + installed, currentRevision, err := installedSnapRevision(st, sn.Name) if err != nil { return nil, err } - if !needsInstall { - info, err := snapstate.CurrentInfo(st, sn.Name) - if err != nil { - return nil, err - } - tracker.Add(info) + // if the snap is installed, then we must either download it from the + // store, have it provided locally, or it must be installed at the + // correct revision. + // + // TODO: in the case that the snap is installed at the wrong revision, + // we must provide it either from the store or locally. this is because + // doCreateRecoverySystem will install any optional snaps that are + // present on the system. + required := constraints.Presence == asserts.PresenceRequired || sn.Presence == "required" || installed + if !required { continue } - if sn.Presence != "required" { - pres, err := valsets.Presence(sn) + compsToDownload := make([]string, 0, len(sn.Components)) + for name, comp := range sn.Components { + compInstalled, currentCompRevision, err := installedComponentRevision(st, sn.Name, name) if err != nil { return nil, err } - // snap isn't already installed, and it isn't required by model or - // any validation sets, so we should skip it - if pres.Presence != asserts.PresenceRequired { + compConstraints := constraints.Component(name) + + required := comp.Presence == "required" || constraints.Component(name).Presence == asserts.PresenceRequired || compInstalled + + // same deal as with snaps, same TODO as well + if !required { continue } + + switch { + case compInstalled && validRevision(currentCompRevision, compConstraints): + // nothing to do! + case opts.Offline: + // TODO: verify that we have the offline component + default: + compsToDownload = append(compsToDownload, name) + } } - if opts.Offline { - info, err := offlineSnapInfo(sn, rev, opts) + switch { + case installed && validRevision(currentRevision, constraints.PresenceConstraint): + info, err := snapstate.CurrentInfo(st, sn.Name) + if err != nil { + return nil, err + } + tracker.Add(info) + case opts.Offline: + info, err := offlineSnapInfo(sn, constraints.Revision, opts) if err != nil { return nil, err } tracker.Add(info) + default: + // TODO: this respects the passed in validation sets, but does not + // currently respect refresh-control style of constraining snap + // revisions. + // + // TODO: download somewhere other than the default snap blob dir. + ts, _, err := snapstateDownload(context.TODO(), st, sn.Name, compsToDownload, dirs.SnapBlobDir, snapstate.RevisionOptions{ + Channel: sn.DefaultChannel, + ValidationSets: valsets, + }, snapstate.Options{ + PrereqTracker: tracker, + }) + if err != nil { + return nil, err + } + downloadTSS = append(downloadTSS, ts) + + // if we go in this branch, then we'll handle downloading snaps and + // components at the same time. continue } - // TODO: this respects the passed in validation sets, but does not - // currently respect refresh-control style of constraining snap - // revisions. - // - // TODO: download somewhere other than the default snap blob dir. - ts, info, err := snapstateDownload(context.TODO(), st, sn.Name, nil, dirs.SnapBlobDir, snapstate.RevisionOptions{ - Channel: sn.DefaultChannel, - Revision: rev, - ValidationSets: valsets, - }, snapstate.Options{}) - if err != nil { - return nil, err + if len(compsToDownload) > 0 { + // TODO: download somewhere other than the default snap blob dir. + ts, err := snapstateDownloadComponents(context.TODO(), st, sn.Name, compsToDownload, dirs.SnapBlobDir, snapstate.RevisionOptions{ + Channel: sn.DefaultChannel, + ValidationSets: valsets, + }, snapstate.Options{ + PrereqTracker: tracker, + }) + if err != nil { + return nil, err + } + downloadTSS = append(downloadTSS, ts) } - - tracker.Add(info) - downloadTSS = append(downloadTSS, ts) } warnings, errs := tracker.Check() @@ -1787,7 +1859,6 @@ func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySyst logger.Noticef("create recovery system prerequisites warning: %v", w) } - // TODO: use function from other branch if len(errs) > 0 { var builder strings.Builder builder.WriteString("cannot create recovery system from model that is not self-contained:") @@ -1800,16 +1871,13 @@ func CreateRecoverySystem(st *state.State, label string, opts CreateRecoverySyst return nil, errors.New(builder.String()) } - var snapsupTaskIDs []string - if len(downloadTSS) > 0 { - snapsupTaskIDs, err = extractSnapSetupTaskIDs(downloadTSS) - if err != nil { - return nil, err - } + snapsupTaskIDs, compsupTaskIDs, err := extractSnapSetupTaskIDs(downloadTSS) + if err != nil { + return nil, err } chg = st.NewChange("create-recovery-system", fmt.Sprintf("Create new recovery system with label %q", label)) - createTS, err := createRecoverySystemTasks(st, label, snapsupTaskIDs, opts) + createTS, err := createRecoverySystemTasks(st, label, snapsupTaskIDs, compsupTaskIDs, opts) if err != nil { return nil, err } @@ -1870,39 +1938,59 @@ func offlineSnapInfo(sn *asserts.ModelSnap, rev snap.Revision, opts CreateRecove return snap.ReadInfoFromSnapFile(s, localSnap.SideInfo) } -func snapNeedsInstall(st *state.State, name string, rev snap.Revision) (bool, error) { - info, err := snapstate.CurrentInfo(st, name) - if err != nil { - if isNotInstalled(err) { - return true, nil +func installedSnapRevision(st *state.State, name string) (bool, snap.Revision, error) { + var snapst snapstate.SnapState + if err := snapstate.Get(st, name, &snapst); err != nil { + if errors.Is(err, state.ErrNoState) { + return false, snap.Revision{}, nil } - return false, err + return false, snap.Revision{}, err } + return true, snapst.Current, nil +} - if rev.Unset() { - return false, nil +func installedComponentRevision(st *state.State, snapName, compName string) (bool, snap.Revision, error) { + var snapst snapstate.SnapState + if err := snapstate.Get(st, snapName, &snapst); err != nil { + if errors.Is(err, state.ErrNoState) { + return false, snap.Revision{}, nil + } + return false, snap.Revision{}, err + } + + for _, comp := range snapst.CurrentComponentSideInfos() { + if comp.Component.ComponentName == compName { + return true, comp.Revision, nil + } } - return rev != info.Revision, nil + return false, snap.Revision{}, nil } -func extractSnapSetupTaskIDs(tss []*state.TaskSet) ([]string, error) { - var taskIDs []string +func extractSnapSetupTaskIDs(tss []*state.TaskSet) (snapsupTaskIDs, compsupTaskIDs []string, err error) { for _, ts := range tss { - found := false + var snapsupTask *state.Task for _, t := range ts.Tasks() { if t.Has("snap-setup") { - taskIDs = append(taskIDs, t.ID()) - found = true + snapsupTask = t break } } - if !found { - return nil, errors.New("internal error: snap setup task missing from task set") + if snapsupTask == nil { + return nil, nil, errors.New("internal error: snap setup task missing from task set") } + + snapsupTaskIDs = append(snapsupTaskIDs, snapsupTask.ID()) + + var compsups []string + if err := snapsupTask.Get("component-setup-tasks", &compsups); err != nil && !errors.Is(err, state.ErrNoState) { + return nil, nil, err + } + + compsupTaskIDs = append(compsupTaskIDs, compsups...) } - return taskIDs, nil + return snapsupTaskIDs, compsupTaskIDs, nil } // OptionalContainers is used to define the snaps and components that are diff --git a/overlord/devicestate/devicestate_remodel_test.go b/overlord/devicestate/devicestate_remodel_test.go index b94874307e9..4fc5bbffd63 100644 --- a/overlord/devicestate/devicestate_remodel_test.go +++ b/overlord/devicestate/devicestate_remodel_test.go @@ -4282,10 +4282,9 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialSnapsTrackingDifferentCh err = tCreateRecovery.Get("recovery-system-setup", &systemSetupData) c.Assert(err, IsNil) c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ - "label": expectedLabel, - "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), - "snap-setup-tasks": nil, - "test-system": true, + "label": expectedLabel, + "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), + "test-system": true, }) } @@ -4623,10 +4622,9 @@ func (s *deviceMgrRemodelSuite) TestRemodelUC20BaseNoDownloadSimpleChannelSwitch err = tCreateRecovery.Get("recovery-system-setup", &systemSetupData) c.Assert(err, IsNil) c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ - "label": expectedLabel, - "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), - "snap-setup-tasks": nil, - "test-system": true, + "label": expectedLabel, + "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), + "test-system": true, }) } diff --git a/overlord/devicestate/devicestate_systems_test.go b/overlord/devicestate/devicestate_systems_test.go index 82b6ca523ba..6098fca05dd 100644 --- a/overlord/devicestate/devicestate_systems_test.go +++ b/overlord/devicestate/devicestate_systems_test.go @@ -50,6 +50,7 @@ import ( "github.com/snapcore/snapd/overlord/install" "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/sequence" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -1379,10 +1380,10 @@ func (s *deviceMgrSystemsCreateSuite) SetUpTest(c *C) { s.state.Lock() defer s.state.Unlock() - s.makeSnapInState(c, "pc", snap.R(1), nil) - s.makeSnapInState(c, "pc-kernel", snap.R(2), nil) - s.makeSnapInState(c, "core20", snap.R(3), nil) - s.makeSnapInState(c, "snapd", snap.R(4), nil) + s.makeSnapInState(c, "pc", snap.R(1), nil, nil) + s.makeSnapInState(c, "pc-kernel", snap.R(2), nil, nil) + s.makeSnapInState(c, "core20", snap.R(3), nil, nil) + s.makeSnapInState(c, "snapd", snap.R(4), nil, nil) s.bootloader = s.deviceMgrSystemsBaseSuite.bootloader.WithRecoveryAwareTrustedAssets() bootloader.Force(s.bootloader) @@ -1434,10 +1435,9 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemTasks err = tskCreate.Get("recovery-system-setup", &systemSetupData) c.Assert(err, IsNil) c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ - "label": "1234", - "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"), - "snap-setup-tasks": nil, - "test-system": true, + "label": "1234", + "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"), + "test-system": true, }) var otherTaskID string @@ -1470,7 +1470,59 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemNotSe c.Check(chg, IsNil) } -func (s *deviceMgrSystemsCreateSuite) makeSnapInState(c *C, name string, rev snap.Revision, extraFiles [][]string) *snap.Info { +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoveryRequiredInVsetNotInModel(c *C) { + devicestate.SetBootOkRan(s.mgr, true) + + s.state.Lock() + defer s.state.Unlock() + + s.model = s.brands.Model("canonical", "pc-20", map[string]interface{}{ + "architecture": "amd64", + "grade": "dangerous", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.ss.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.ss.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + }, + "revision": "2", + }) + + vset, err := s.brands.Signing("canonical").Sign(asserts.ValidationSetType, map[string]interface{}{ + "type": "validation-set", + "authority-id": "canonical", + "series": "16", + "account-id": "canonical", + "name": "vset-1", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "required-snap", + "id": s.ss.AssertedSnapID("other"), + "presence": "required", + }, + }, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + chg, err := devicestate.CreateRecoverySystem(s.state, "1234", devicestate.CreateRecoverySystemOptions{ + ValidationSets: []*asserts.ValidationSet{vset.(*asserts.ValidationSet)}, + }) + c.Assert(err, ErrorMatches, `missing required snap in model: required-snap`) + c.Check(chg, IsNil) +} + +func (s *deviceMgrSystemsCreateSuite) makeSnapInState(c *C, name string, rev snap.Revision, extraFiles [][]string, components map[string]snap.Revision) *snap.Info { snapID := s.ss.AssertedSnapID(name) if rev.Unset() || rev.Local() { snapID = "" @@ -1489,10 +1541,62 @@ func (s *deviceMgrSystemsCreateSuite) makeSnapInState(c *C, name string, rev sna s.setupSnapDecl(c, info, "canonical") s.setupSnapRevision(c, info, "canonical", rev) } + + seq := snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}) + + for comp, compRev := range components { + if rev.Unset() { + continue + } + + cref := naming.NewComponentRef(name, comp) + + compYaml, ok := componentYamls[cref.String()] + c.Assert(ok, Equals, true, Commentf("component.yaml not found for %q", name)) + + compPath := snaptest.MakeTestComponent(c, compYaml) + + csi := snap.ComponentSideInfo{ + Component: cref, + Revision: compRev, + } + + compInfo := snaptest.MockComponent(c, compYaml, info, csi) + + cpi := snap.MinimalComponentContainerPlaceInfo( + comp, + compRev, + name, + ) + err := os.Rename(compPath, cpi.MountFile()) + c.Assert(err, IsNil) + + s.setupSnapResourcePair( + c, + comp, + snapID, + "canonical", + compRev, + rev, + ) + + s.setupSnapResourceRevision( + c, + cpi.MountFile(), + comp, + snapID, + "canonical", + compRev, + ) + + err = seq.AddComponentForRevision(rev, sequence.NewComponentState(snap.NewComponentSideInfo(cref, compRev), compInfo.Type)) + c.Assert(err, IsNil) + } + snapstate.Set(s.state, info.InstanceName(), &snapstate.SnapState{ SnapType: string(info.Type()), Active: true, - Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{si}), + Sequence: seq, Current: si.Revision, }) @@ -1500,10 +1604,10 @@ func (s *deviceMgrSystemsCreateSuite) makeSnapInState(c *C, name string, rev sna } func (s *deviceMgrSystemsCreateSuite) mockStandardSnapsModeenvAndBootloaderState(c *C) { - s.makeSnapInState(c, "pc", snap.R(1), nil) - s.makeSnapInState(c, "pc-kernel", snap.R(2), nil) - s.makeSnapInState(c, "core20", snap.R(3), nil) - s.makeSnapInState(c, "snapd", snap.R(4), nil) + s.makeSnapInState(c, "pc", snap.R(1), nil, nil) + s.makeSnapInState(c, "pc-kernel", snap.R(2), nil, nil) + s.makeSnapInState(c, "core20", snap.R(3), nil, nil) + s.makeSnapInState(c, "snapd", snap.R(4), nil, nil) err := s.bootloader.SetBootVars(map[string]string{ "snap_kernel": "pc-kernel_2.snap", @@ -1662,7 +1766,7 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemRemod tSnapsup1.Set("snap-setup", snapsupFoo) tSnapsup2.Set("snap-setup", snapsupBar) - tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234", []string{tSnapsup1.ID(), tSnapsup2.ID()}, devicestate.CreateRecoverySystemOptions{ + tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234", []string{tSnapsup1.ID(), tSnapsup2.ID()}, nil, devicestate.CreateRecoverySystemOptions{ TestSystem: true, }) c.Assert(err, IsNil) @@ -1827,7 +1931,7 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemRemod s.state.Lock() - tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234", nil, devicestate.CreateRecoverySystemOptions{ + tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234", nil, nil, devicestate.CreateRecoverySystemOptions{ TestSystem: true, }) c.Assert(err, IsNil) @@ -1842,10 +1946,9 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemRemod err = tskCreate.Get("recovery-system-setup", &systemSetupData) c.Assert(err, IsNil) c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ - "label": "1234", - "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"), - "snap-setup-tasks": nil, - "test-system": true, + "label": "1234", + "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234"), + "test-system": true, }) // add the test tasks to the change chg := s.state.NewChange("create-recovery-system", "create recovery system") @@ -2042,7 +2145,7 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemRemod } tSnapsup1.Set("snap-setup", snapsupFoo) - tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234missingdownload", []string{tSnapsup1.ID()}, devicestate.CreateRecoverySystemOptions{ + tss, err := devicestate.CreateRecoverySystemTasks(s.state, "1234missingdownload", []string{tSnapsup1.ID()}, nil, devicestate.CreateRecoverySystemOptions{ TestSystem: true, }) c.Assert(err, IsNil) @@ -3381,7 +3484,7 @@ func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValid validationSets = append(validationSets, vsetAssert.(*asserts.ValidationSet)) if opts.PreInstallOptionalSnap { - s.makeSnapInState(c, "other-required", snapRevisions["other-required"], nil) + s.makeSnapInState(c, "other-required", snapRevisions["other-required"], nil, nil) } if opts.RequireOptionalSnapInValidationSet { @@ -3472,13 +3575,13 @@ func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValid return nil, nil, fmt.Errorf("unexpected snap name %q", name) } - c.Check(expectedRev, Equals, revOpts.Revision) + c.Check(revOpts.Revision.Unset(), Equals, true) tDownload := s.state.NewTask("mock-download", fmt.Sprintf("Download %s to track %s", name, revOpts.Channel)) si := &snap.SideInfo{ RealName: name, - Revision: revOpts.Revision, + Revision: expectedRev, SnapID: fakeSnapID(name), } tDownload.Set("snap-setup", &snapstate.SnapSetup{ @@ -3489,6 +3592,8 @@ func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValid _, info := snaptest.MakeTestSnapInfoWithFiles(c, snapYamls[name], snapFiles[name], si) + opts.PrereqTracker.Add(info) + tValidate := s.state.NewTask("mock-validate", fmt.Sprintf("Validate %s", name)) tValidate.Set("snap-setup-task", tDownload.ID()) @@ -3634,6 +3739,823 @@ func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValid } } +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponents(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.testDeviceManagerCreateRecoverySystemValidationSetsComponents(c, testCreateRecoverySystemValidationSetsComponentsOpts{ + kmodModelPresence: "required", + kmodVsetPresence: "required", + blobs: []string{"snapd_13.snap", "pc-kernel-with-kmods_11.snap", "pc-kernel-with-kmods+kmod_20.comp", "core20_12.snap", "pc_10.snap"}, + downloadedSnaps: 4, + downloadedComps: 1, + }) +} + +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponentsRequiredInVsets(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // should still download and install the component, despite the correct snap + // being present + s.makeSnapInState(c, "pc-kernel-with-kmods", snap.R(11), nil, nil) + + s.testDeviceManagerCreateRecoverySystemValidationSetsComponents(c, testCreateRecoverySystemValidationSetsComponentsOpts{ + kmodModelPresence: "optional", + kmodVsetPresence: "required", + blobs: []string{"snapd_13.snap", "pc-kernel-with-kmods_11.snap", "pc-kernel-with-kmods+kmod_20.comp", "core20_12.snap", "pc_10.snap"}, + downloadedSnaps: 3, // snapd, core20, pc + downloadedComps: 1, // pc-kernel-with-kmods+kmod + }) +} + +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponentsNoInstall(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.testDeviceManagerCreateRecoverySystemValidationSetsComponents(c, testCreateRecoverySystemValidationSetsComponentsOpts{ + kmodModelPresence: "optional", + kmodVsetPresence: "optional", + blobs: []string{"snapd_13.snap", "pc-kernel-with-kmods_11.snap", "core20_12.snap", "pc_10.snap"}, + downloadedSnaps: 4, + downloadedComps: 0, + }) +} + +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponentsAlreadyInstalledComponent(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // snap and components are already installed, but this component revision is + // wrong. everything should still happen as if the snap was not installed. + s.makeSnapInState(c, "pc-kernel-with-kmods", snap.R(11), nil, map[string]snap.Revision{ + "kmod": snap.R(19), + }) + + s.testDeviceManagerCreateRecoverySystemValidationSetsComponents(c, testCreateRecoverySystemValidationSetsComponentsOpts{ + kmodModelPresence: "required", + kmodVsetPresence: "required", + blobs: []string{"snapd_13.snap", "pc-kernel-with-kmods_11.snap", "pc-kernel-with-kmods+kmod_20.comp", "core20_12.snap", "pc_10.snap"}, + downloadedSnaps: 3, // snapd, core20, pc + downloadedComps: 1, // pc-kernel-with-kmods+kmod + }) +} + +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponentsAlreadyInstalledComponentOptional(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // even though the component is optional, we still download it since it is + // installed on the current system. + s.makeSnapInState(c, "pc-kernel-with-kmods", snap.R(11), nil, map[string]snap.Revision{ + "kmod": snap.R(19), + }) + + s.testDeviceManagerCreateRecoverySystemValidationSetsComponents(c, testCreateRecoverySystemValidationSetsComponentsOpts{ + kmodModelPresence: "optional", + kmodVsetPresence: "optional", + blobs: []string{"snapd_13.snap", "pc-kernel-with-kmods_11.snap", "pc-kernel-with-kmods+kmod_20.comp", "core20_12.snap", "pc_10.snap"}, + downloadedSnaps: 3, // snapd, core20, pc + downloadedComps: 1, // pc-kernel-with-kmods+kmod + }) +} + +type testCreateRecoverySystemValidationSetsComponentsOpts struct { + kmodModelPresence string + kmodVsetPresence string + blobs []string + downloadedSnaps int + downloadedComps int +} + +func (s *deviceMgrSystemsCreateSuite) testDeviceManagerCreateRecoverySystemValidationSetsComponents(c *C, opts testCreateRecoverySystemValidationSetsComponentsOpts) { + devicestate.SetBootOkRan(s.mgr, true) + + snapComponents := map[string][]string{ + "pc-kernel-with-kmods": {"kmod"}, + } + + s.model = s.makeModelAssertionInState(c, "canonical", "pc-20", map[string]interface{}{ + "architecture": "amd64", + "grade": "dangerous", + "base": "core20", + "revision": "2", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": s.ss.AssertedSnapID("pc-kernel-with-kmods"), + "type": "kernel", + "default-channel": "20", + "components": map[string]interface{}{ + "kmod": map[string]interface{}{ + "presence": opts.kmodModelPresence, + }, + "other-kmod": map[string]interface{}{ + "presence": "optional", + }, + }, + }, + map[string]interface{}{ + "name": "pc", + "id": s.ss.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "core20", + "id": s.ss.AssertedSnapID("core20"), + "type": "base", + }, + map[string]interface{}{ + "name": "snapd", + "id": s.ss.AssertedSnapID("snapd"), + "type": "snapd", + }, + }, + "validation-sets": []interface{}{ + map[string]interface{}{ + "account-id": "canonical", + "name": "vset-model", + "mode": "enforce", + }, + }, + }) + + vsetModel, err := s.brands.Signing("canonical").Sign(asserts.ValidationSetType, map[string]interface{}{ + "type": "validation-set", + "authority-id": "canonical", + "series": "16", + "account-id": "canonical", + "name": "vset-model", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": fakeSnapID("pc-kernel-with-kmods"), + "presence": "required", + "revision": "11", + "components": map[string]interface{}{ + "kmod": map[string]interface{}{ + "revision": "20", + "presence": opts.kmodVsetPresence, + }, + }, + }, + }, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + assertstatetest.AddMany(s.state, vsetModel) + assertstate.UpdateValidationSet(s.state, &assertstate.ValidationSetTracking{ + AccountID: "canonical", + Name: "vset-model", + Mode: assertstate.Enforce, + Current: 1, + }) + + snapRevisions := map[string]snap.Revision{ + "pc": snap.R(10), + "pc-kernel-with-kmods": snap.R(11), + "core20": snap.R(12), + "snapd": snap.R(13), + } + + componentRevisions := map[string]snap.Revision{ + "pc-kernel-with-kmods+kmod": snap.R(20), + } + + componentTypes := map[string]snap.ComponentType{ + "pc-kernel-with-kmods+kmod": snap.KernelModulesComponent, + } + + compsToTypes := func(snapName string) map[string]snap.ComponentType { + res := make(map[string]snap.ComponentType) + for _, comps := range snapComponents { + for _, comp := range comps { + res[comp] = componentTypes[naming.NewComponentRef(snapName, comp).String()] + } + } + return res + } + + snapTypes := map[string]snap.Type{ + "pc": snap.TypeGadget, + "pc-kernel-with-kmods": snap.TypeKernel, + "core20": snap.TypeBase, + "snapd": snap.TypeSnapd, + } + + var validationSets []*asserts.ValidationSet + + vsetAssert, err := s.brands.Signing("canonical").Sign(asserts.ValidationSetType, map[string]interface{}{ + "type": "validation-set", + "authority-id": "canonical", + "series": "16", + "account-id": "canonical", + "name": "vset-1", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc", + "id": fakeSnapID("pc"), + "revision": snapRevisions["pc"].String(), + "presence": "required", + }, + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": fakeSnapID("pc-kernel-with-kmods"), + "revision": snapRevisions["pc-kernel-with-kmods"].String(), + "presence": "required", + }, + map[string]interface{}{ + "name": "core20", + "id": fakeSnapID("core20"), + "revision": snapRevisions["core20"].String(), + "presence": "required", + }, + map[string]interface{}{ + "name": "snapd", + "id": fakeSnapID("snapd"), + "revision": snapRevisions["snapd"].String(), + "presence": "required", + }, + }, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + validationSets = append(validationSets, vsetAssert.(*asserts.ValidationSet)) + + s.o.TaskRunner().AddHandler("mock-validate", func(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + snapsup, err := snapstate.TaskSnapSetup(task) + c.Assert(err, IsNil) + + s.setupSnapDeclForNameAndID(c, snapsup.SideInfo.RealName, snapsup.SideInfo.SnapID, "canonical") + s.setupSnapRevisionForFileAndID( + c, snapsup.BlobPath(), snapsup.SideInfo.SnapID, "canonical", snapRevisions[snapsup.SideInfo.RealName], + ) + + return nil + }, nil) + + s.o.TaskRunner().AddHandler("mock-download", func(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + snapsup, err := snapstate.TaskSnapSetup(task) + c.Assert(err, IsNil) + var path string + var files [][]string + switch snapsup.Type { + case snap.TypeBase: + path = snaptest.MakeTestSnapWithFiles( + c, + withComponents( + fmt.Sprintf("name: %s\nversion: 1.0\ntype: %s", + snapsup.SideInfo.RealName, + snapsup.Type, + ), + compsToTypes(snapsup.InstanceName()), + ), + nil, + ) + case snap.TypeGadget: + files = [][]string{ + {"meta/gadget.yaml", uc20gadgetYaml}, + } + fallthrough + default: + path = snaptest.MakeTestSnapWithFiles( + c, + withComponents( + fmt.Sprintf("name: %s\nversion: 1.0\nbase: %s\ntype: %s", + snapsup.SideInfo.RealName, + snapsup.Base, + snapsup.Type, + ), + compsToTypes(snapsup.InstanceName()), + ), + files, + ) + } + + err = os.Rename(path, filepath.Join(dirs.SnapBlobDir, fmt.Sprintf("%s_%s.snap", snapsup.SideInfo.RealName, snapsup.Revision().String()))) + c.Assert(err, IsNil) + return nil + }, nil) + + s.o.TaskRunner().AddHandler("mock-validate-component", func(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + compsup, snapsup, err := snapstate.TaskComponentSetup(task) + c.Assert(err, IsNil) + + s.setupSnapResourceRevision( + c, + compsup.BlobPath(snapsup.InstanceName()), + compsup.ComponentName(), + snapsup.SideInfo.SnapID, + "canonical", + componentRevisions[compsup.CompSideInfo.Component.String()], + ) + + s.setupSnapResourcePair( + c, + compsup.ComponentName(), + snapsup.SideInfo.SnapID, + "canonical", + componentRevisions[compsup.CompSideInfo.Component.String()], + snapRevisions[snapsup.SideInfo.RealName], + ) + + s.setupSnapRevisionForFileAndID( + c, snapsup.BlobPath(), snapsup.SideInfo.SnapID, "canonical", snapRevisions[snapsup.SideInfo.RealName], + ) + + return nil + }, nil) + + s.o.TaskRunner().AddHandler("mock-download-component", func(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + compsup, snapsup, err := snapstate.TaskComponentSetup(task) + c.Assert(err, IsNil) + path := snaptest.MakeTestComponent(c, fmt.Sprintf( + "component: %s\nversion: 1.0\ntype: %s\n", + compsup.CompSideInfo.Component.String(), + compsup.CompType, + )) + + err = os.Rename(path, compsup.BlobPath(snapsup.InstanceName())) + c.Assert(err, IsNil) + + return nil + }, nil) + + restore := devicestate.MockSnapstateDownloadComponents(func( + ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, error, + ) { + c.Assert(revOpts.Revision.Unset(), Equals, true) + + si := &snap.SideInfo{ + RealName: name, + Revision: snapRevisions[name], + SnapID: fakeSnapID(name), + } + + snapsup := &snapstate.SnapSetup{ + SideInfo: si, + Base: "core20", + Type: snapTypes[name], + } + + ts := state.NewTaskSet() + var snapsupTask, prev *state.Task + add := func(t *state.Task) { + ts.AddTask(t) + if prev == nil { + t.Set("snap-setup", snapsup) + snapsupTask = t + ts.MarkEdge(t, snapstate.BeginEdge) + } else { + t.WaitFor(prev) + t.Set("snap-setup-task", snapsupTask.ID()) + } + prev = t + } + + var compsupTaskIDs []string + for _, comp := range components { + cref := naming.NewComponentRef(name, comp) + + download := s.state.NewTask("mock-download-component", fmt.Sprintf("Download component %q", cref)) + download.Set("component-setup", &snapstate.ComponentSetup{ + CompSideInfo: &snap.ComponentSideInfo{ + Component: cref, + Revision: componentRevisions[cref.String()], + }, + CompType: componentTypes[cref.String()], + }) + compsupTaskIDs = append(compsupTaskIDs, download.ID()) + add(download) + + validate := s.state.NewTask("mock-validate-component", fmt.Sprintf("Validate component %q", cref)) + validate.Set("component-setup-task", download.ID()) + add(validate) + } + + snapsupTask.Set("component-setup-tasks", compsupTaskIDs) + ts.MarkEdge(prev, snapstate.LastBeforeLocalModificationsEdge) + + return ts, nil + }) + defer restore() + + restore = devicestate.MockSnapstateDownload(func( + ctx context.Context, st *state.State, name string, components []string, dir string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, *snap.Info, error, + ) { + c.Assert(revOpts.Revision.Unset(), Equals, true) + + si := &snap.SideInfo{ + RealName: name, + Revision: snapRevisions[name], + SnapID: fakeSnapID(name), + } + + download := s.state.NewTask("mock-download", fmt.Sprintf("Download %s to track %s", name, revOpts.Channel)) + download.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + Base: "core20", + Type: snapTypes[name], + }) + + ts := state.NewTaskSet(download) + ts.MarkEdge(download, snapstate.BeginEdge) + prev := download + add := func(t *state.Task) { + t.WaitFor(prev) + t.Set("snap-setup-task", download.ID()) + ts.AddTask(t) + prev = t + } + + validate := s.state.NewTask("mock-validate", fmt.Sprintf("Validate %s", name)) + validate.Set("snap-setup-task", download.ID()) + add(validate) + + var compsupTaskIDs []string + for _, comp := range components { + cref := naming.NewComponentRef(name, comp) + + download := s.state.NewTask("mock-download-component", fmt.Sprintf("Download component %q", cref)) + download.Set("component-setup", &snapstate.ComponentSetup{ + CompSideInfo: &snap.ComponentSideInfo{ + Component: cref, + Revision: componentRevisions[cref.String()], + }, + CompType: componentTypes[cref.String()], + }) + compsupTaskIDs = append(compsupTaskIDs, download.ID()) + add(download) + + validate := s.state.NewTask("mock-validate-component", fmt.Sprintf("Validate component %q", cref)) + validate.Set("component-setup-task", download.ID()) + add(validate) + } + + download.Set("component-setup-tasks", compsupTaskIDs) + ts.MarkEdge(prev, snapstate.LastBeforeLocalModificationsEdge) + + _, info := snaptest.MakeTestSnapInfoWithFiles(c, withComponents(snapYamls[name], compsToTypes(name)), snapFiles[name], si) + opts.PrereqTracker.Add(info) + + return ts, info, nil + }) + defer restore() + + s.state.Set("refresh-privacy-key", "some-privacy-key") + s.mockStandardSnapsModeenvAndBootloaderState(c) + + chg, err := devicestate.CreateRecoverySystem(s.state, "1234", devicestate.CreateRecoverySystemOptions{ + ValidationSets: validationSets, + TestSystem: true, + MarkDefault: true, + }) + c.Assert(err, IsNil) + + s.validateCreateRecoverySystemChange(c, chg, opts) +} + +func (s *deviceMgrSystemsCreateSuite) validateCreateRecoverySystemChange(c *C, chg *state.Change, opts testCreateRecoverySystemValidationSetsComponentsOpts) { + tsks := chg.Tasks() + + // two per snap, two per comp, create system, finalize system + c.Check(tsks, HasLen, (2*opts.downloadedSnaps)+(2*opts.downloadedComps)+2) + + tskCreate := tsks[0] + tskFinalize := tsks[1] + c.Assert(tskCreate.Summary(), Matches, `Create recovery system with label "1234"`) + c.Check(tskFinalize.Summary(), Matches, `Finalize recovery system with label "1234"`) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + c.Assert(tskCreate.Status(), Equals, state.WaitStatus) + c.Assert(tskFinalize.Status(), Equals, state.DoStatus) + + // a reboot is expected + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) + + var runModeSnaps []string + validateCore20Seed(c, "1234", s.model, s.storeSigning.Trusted, runModeSnaps...) + + m, err := s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]string{ + "try_recovery_system": "1234", + "recovery_system_status": "try", + }) + modeenvAfterCreate, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(modeenvAfterCreate, testutil.JsonEquals, boot.Modeenv{ + Mode: "run", + Base: "core20_3.snap", + // the setup of this test suite uses a different kernel. this is correct + // because that is the current kernel that is installed on this system + CurrentKernels: []string{"pc-kernel_2.snap"}, + CurrentRecoverySystems: []string{"othersystem", "1234"}, + GoodRecoverySystems: []string{"othersystem"}, + + Model: s.model.Model(), + BrandID: s.model.BrandID(), + Grade: string(s.model.Grade()), + ModelSignKeyID: s.model.SignKeyID(), + }) + + var expectedFilesLog bytes.Buffer + for _, fname := range opts.blobs { + fmt.Fprintln(&expectedFilesLog, filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", fname)) + } + + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "1234", "snapd-new-file-log"), + testutil.FileEquals, expectedFilesLog.String()) + + // these things happen on snapd startup + restart.MockPending(s.state, restart.RestartUnset) + s.state.Set("tried-systems", []string{"1234"}) + s.bootloader.SetBootVars(map[string]string{ + "try_recovery_system": "", + "recovery_system_status": "", + }) + s.bootloader.SetBootVarsCalls = 0 + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // simulate a restart and run change to completion + s.mockRestartAndSettle(c, s.state, chg) + + c.Assert(chg.Err(), IsNil) + c.Check(chg.IsReady(), Equals, true) + c.Assert(tskCreate.Status(), Equals, state.DoneStatus) + c.Assert(tskFinalize.Status(), Equals, state.DoneStatus) + + var triedSystemsAfterFinalize []string + err = s.state.Get("tried-systems", &triedSystemsAfterFinalize) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) + + modeenvAfterFinalize, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(modeenvAfterFinalize, testutil.JsonEquals, boot.Modeenv{ + Mode: "run", + Base: "core20_3.snap", + // the setup of this test suite uses a different kernel. this is correct + // because that is the current kernel that is installed on this system + CurrentKernels: []string{"pc-kernel_2.snap"}, + CurrentRecoverySystems: []string{"othersystem", "1234"}, + GoodRecoverySystems: []string{"othersystem", "1234"}, + + Model: s.model.Model(), + BrandID: s.model.BrandID(), + Grade: string(s.model.Grade()), + ModelSignKeyID: s.model.SignKeyID(), + }) + + // expect 1 more call to bootloader.SetBootVars, since we're marking this + // system as seeded + c.Check(s.bootloader.SetBootVarsCalls, Equals, 1) + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "1234", "snapd-new-file-log"), testutil.FileAbsent) + + var defaultSystem devicestate.DefaultRecoverySystem + err = s.state.Get("default-recovery-system", &defaultSystem) + c.Assert(err, IsNil) + + c.Assert(defaultSystem.System, Equals, "1234") + c.Assert(defaultSystem.Model, Equals, s.model.Model()) + c.Assert(defaultSystem.BrandID, Equals, s.model.BrandID()) +} + +func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValidationSetsComponentsReuseInstalled(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.makeSnapInState(c, "pc-kernel-with-kmods", snap.R(11), nil, map[string]snap.Revision{ + "kmod": snap.R(22), + }) + + devicestate.SetBootOkRan(s.mgr, true) + + s.model = s.makeModelAssertionInState(c, "canonical", "pc-20", map[string]interface{}{ + "architecture": "amd64", + "grade": "dangerous", + "base": "core20", + "revision": "2", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": s.ss.AssertedSnapID("pc-kernel-with-kmods"), + "type": "kernel", + "default-channel": "20", + "components": map[string]interface{}{ + "kmod": map[string]interface{}{ + "presence": "required", + }, + }, + }, + map[string]interface{}{ + "name": "pc", + "id": s.ss.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "core20", + "id": s.ss.AssertedSnapID("core20"), + "type": "base", + }, + map[string]interface{}{ + "name": "snapd", + "id": s.ss.AssertedSnapID("snapd"), + "type": "snapd", + }, + }, + "validation-sets": []interface{}{ + map[string]interface{}{ + "account-id": "canonical", + "name": "vset-model", + "mode": "enforce", + }, + }, + }) + + vset, err := s.brands.Signing("canonical").Sign(asserts.ValidationSetType, map[string]interface{}{ + "type": "validation-set", + "authority-id": "canonical", + "series": "16", + "account-id": "canonical", + "name": "vset-model", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel-with-kmods", + "id": fakeSnapID("pc-kernel-with-kmods"), + "presence": "required", + "revision": "11", + "components": map[string]interface{}{ + "kmod": map[string]interface{}{ + "revision": "22", + "presence": "required", + }, + }, + }, + }, + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + assertstatetest.AddMany(s.state, vset) + assertstate.UpdateValidationSet(s.state, &assertstate.ValidationSetTracking{ + AccountID: "canonical", + Name: "vset-model", + Mode: assertstate.Enforce, + Current: 1, + }) + + s.state.Set("refresh-privacy-key", "some-privacy-key") + s.mockStandardSnapsModeenvAndBootloaderState(c) + + chg, err := devicestate.CreateRecoverySystem(s.state, "1234", devicestate.CreateRecoverySystemOptions{ + TestSystem: true, + MarkDefault: true, + }) + c.Assert(err, IsNil) + c.Assert(chg, NotNil) + tsks := chg.Tasks() + + // create system + finalize system + c.Check(tsks, HasLen, 2) + + create, finalize := tsks[0], tsks[1] + c.Check(create.Summary(), Matches, `Create recovery system with label "1234"`) + c.Check(finalize.Summary(), Matches, `Finalize recovery system with label "1234"`) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + c.Assert(create.Status(), Equals, state.WaitStatus) + c.Assert(finalize.Status(), Equals, state.DoStatus) + + // a reboot is expected + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) + + var runModeSnaps []string + validateCore20Seed(c, "1234", s.model, s.storeSigning.Trusted, runModeSnaps...) + + m, err := s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Check(m, DeepEquals, map[string]string{ + "try_recovery_system": "1234", + "recovery_system_status": "try", + }) + modeenvAfterCreate, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(modeenvAfterCreate, testutil.JsonEquals, boot.Modeenv{ + Mode: "run", + Base: "core20_3.snap", + // the setup of this test suite uses a different kernel. this is correct + // because that is the current kernel that is installed on this system + CurrentKernels: []string{"pc-kernel_2.snap"}, + CurrentRecoverySystems: []string{"othersystem", "1234"}, + GoodRecoverySystems: []string{"othersystem"}, + + Model: s.model.Model(), + BrandID: s.model.BrandID(), + Grade: string(s.model.Grade()), + ModelSignKeyID: s.model.SignKeyID(), + }) + + // verify that new files are tracked correctly + expectedFiles := []string{"snapd_4.snap", "pc-kernel-with-kmods_11.snap", "pc-kernel-with-kmods+kmod_22.comp", "core20_3.snap", "pc_1.snap"} + + var expectedFilesLog bytes.Buffer + for _, fname := range expectedFiles { + fmt.Fprintln(&expectedFilesLog, filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps", fname)) + } + + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "1234", "snapd-new-file-log"), + testutil.FileEquals, expectedFilesLog.String()) + + // these things happen on snapd startup + restart.MockPending(s.state, restart.RestartUnset) + s.state.Set("tried-systems", []string{"1234"}) + s.bootloader.SetBootVars(map[string]string{ + "try_recovery_system": "", + "recovery_system_status": "", + }) + s.bootloader.SetBootVarsCalls = 0 + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // simulate a restart and run change to completion + s.mockRestartAndSettle(c, s.state, chg) + + c.Assert(chg.Err(), IsNil) + c.Check(chg.IsReady(), Equals, true) + c.Assert(create.Status(), Equals, state.DoneStatus) + c.Assert(finalize.Status(), Equals, state.DoneStatus) + + var triedSystemsAfterFinalize []string + err = s.state.Get("tried-systems", &triedSystemsAfterFinalize) + c.Assert(err, testutil.ErrorIs, state.ErrNoState) + + modeenvAfterFinalize, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(modeenvAfterFinalize, testutil.JsonEquals, boot.Modeenv{ + Mode: "run", + Base: "core20_3.snap", + // the setup of this test suite uses a different kernel. this is correct + // because that is the current kernel that is installed on this system + CurrentKernels: []string{"pc-kernel_2.snap"}, + CurrentRecoverySystems: []string{"othersystem", "1234"}, + GoodRecoverySystems: []string{"othersystem", "1234"}, + + Model: s.model.Model(), + BrandID: s.model.BrandID(), + Grade: string(s.model.Grade()), + ModelSignKeyID: s.model.SignKeyID(), + }) + + // expect 1 more call to bootloader.SetBootVars, since we're marking this + // system as seeded + c.Check(s.bootloader.SetBootVarsCalls, Equals, 1) + c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", "1234", "snapd-new-file-log"), testutil.FileAbsent) + + var defaultSystem devicestate.DefaultRecoverySystem + err = s.state.Get("default-recovery-system", &defaultSystem) + c.Assert(err, IsNil) + + c.Assert(defaultSystem.System, Equals, "1234") + c.Assert(defaultSystem.Model, Equals, s.model.Model()) + c.Assert(defaultSystem.BrandID, Equals, s.model.BrandID()) +} + func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemOnlineWithLocalError(c *C) { devicestate.SetBootOkRan(s.mgr, true) @@ -4317,12 +5239,12 @@ func (s *deviceMgrSystemsCreateSuite) TestDeviceManagerCreateRecoverySystemValid return nil, nil, fmt.Errorf("unexpected snap name %q", name) } - c.Check(expectedRev, Equals, revOpts.Revision) + c.Check(revOpts.Revision.Unset(), Equals, true) tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, revOpts.Channel)) si := &snap.SideInfo{ RealName: name, - Revision: revOpts.Revision, + Revision: expectedRev, SnapID: fakeSnapID(name), } @@ -4356,6 +5278,7 @@ plugs: tDownload.Set("snap-setup", snapsup) _, info := snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, si) + opts.PrereqTracker.Add(info) tValidate := s.state.NewTask("fake-validate", fmt.Sprintf("Validate %s", name)) tValidate.Set("snap-setup-task", tDownload.ID()) @@ -4683,7 +5606,7 @@ func (s *deviceMgrSystemsCreateSuite) testRemoveRecoverySystem(c *C, mockRetry b } // add an extra file in there so that the snap has a new hash - s.makeSnapInState(c, name, rev, [][]string{{"random-file", "random-content"}}) + s.makeSnapInState(c, name, rev, [][]string{{"random-file", "random-content"}}, nil) } vsetAssert, err := s.brands.Signing("canonical").Sign(asserts.ValidationSetType, map[string]interface{}{ diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index d2d287e8b34..4e28338d994 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -191,6 +191,12 @@ func MockSnapstateDownload(f func(ctx context.Context, st *state.State, name str return r } +func MockSnapstateDownloadComponents(f func(ctx context.Context, st *state.State, name string, components []string, blobDirectory string, revOpts snapstate.RevisionOptions, opts snapstate.Options) (*state.TaskSet, error)) (restore func()) { + r := testutil.Backup(&snapstateDownloadComponents) + snapstateDownloadComponents = f + return r +} + func EnsureSeeded(m *DeviceManager) error { return m.ensureSeeded() } diff --git a/overlord/devicestate/systems.go b/overlord/devicestate/systems.go index 9926a607da0..80cd1df2b4a 100644 --- a/overlord/devicestate/systems.go +++ b/overlord/devicestate/systems.go @@ -236,7 +236,68 @@ type setupInfoGetter struct { } func (ig *setupInfoGetter) ComponentInfo(st *state.State, cref naming.ComponentRef, snapInfo *snap.Info) (info *snap.ComponentInfo, path string, present bool, err error) { - return nil, "", false, fmt.Errorf("internal error: creating a recovery system with components from recoverySystemSetup not yet supported") + // components will come from one of these places: + // * passed into the task via a list of side infos (these would have + // come from a user posting components via the API) + // * have just been downloaded by a task in setup.ComponentSetupTasks + // * already installed on the system + + // in a remodel scenario, the components may need to be fetched and thus + // their content can be different from what we have already installed, so we + // should first check the download tasks before consulting snapstate + logger.Debugf("requested info for component %q being installed during remodel", cref) + for _, tskID := range ig.setup.ComponentSetupTasks { + taskWithComponentSetup := st.Task(tskID) + compsup, snapsup, err := snapstate.TaskComponentSetup(taskWithComponentSetup) + if err != nil { + return nil, "", false, err + } + if compsup.CompSideInfo.Component != cref { + continue + } + + mountFile := compsup.BlobPath(snapsup.InstanceName()) + + f, err := snapfile.Open(mountFile) + if err != nil { + return nil, "", false, err + } + + info, err = snap.ReadComponentInfoFromContainer(f, snapInfo, compsup.CompSideInfo) + if err != nil { + return nil, "", false, err + } + + return info, mountFile, true, nil + } + + // either a remodel scenario, in which case the component is not among the + // ones being fetched, or just creating a recovery system, in which case we + // use the components that are already installed + + var snapst snapstate.SnapState + if err := snapstate.Get(st, snapInfo.InstanceName(), &snapst); err != nil { + if errors.Is(err, state.ErrNoState) { + return nil, "", false, nil + } + return nil, "", false, err + } + + info, err = snapst.CurrentComponentInfo(cref) + if err != nil { + if errors.Is(err, snapstate.ErrNoCurrent) { + return nil, "", false, nil + } + return nil, "", false, err + } + + cpi := snap.MinimalComponentContainerPlaceInfo( + cref.ComponentName, + info.Revision, + snapInfo.InstanceName(), + ) + + return info, cpi.MountFile(), true, nil } func (ig *setupInfoGetter) SnapInfo(st *state.State, name string) (info *snap.Info, path string, present bool, err error) { diff --git a/overlord/devicestate/systems_test.go b/overlord/devicestate/systems_test.go index df16dd97996..acba0142c51 100644 --- a/overlord/devicestate/systems_test.go +++ b/overlord/devicestate/systems_test.go @@ -93,6 +93,7 @@ var ( } componentYamls = map[string]string{ "pc-kernel-with-kmods+kmod": "component: pc-kernel-with-kmods+kmod\ntype: kernel-modules\nversion: 1.0", + "pc-kernel+kmod": "component: pc-kernel+kmod\ntype: kernel-modules\nversion: 1.0", "other-unasserted+comp": "component: other-unasserted+comp\ntype: standard\nversion: 10.0", "snap-with-components+comp-1": "component: snap-with-components+comp-1\ntype: standard\nversion: 22.0", "snap-with-components+comp-2": "component: snap-with-components+comp-2\ntype: standard\nversion: 33.0", diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index a9917fd29a4..3355f0dea73 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -274,7 +274,7 @@ func MakeTestComponentWithFiles(c *check.C, componentName, componentYaml string, func MakeTestComponent(c *check.C, compYaml string) string { compInfo, err := snap.InfoFromComponentYaml([]byte(compYaml)) c.Assert(err, check.IsNil) - return MakeTestComponentWithFiles(c, compInfo.FullName()+".comp", compYaml, nil) + return MakeTestComponentWithFiles(c, compInfo.FullName(), compYaml, nil) } func populateContainer(c *check.C, yamlFile, yamlContent string, files [][]string) string { diff --git a/tests/lib/assertions/test-snapd-component-recovery-system-pc-24.json b/tests/lib/assertions/test-snapd-component-recovery-system-pc-24.json new file mode 100644 index 00000000000..506a4a6c019 --- /dev/null +++ b/tests/lib/assertions/test-snapd-component-recovery-system-pc-24.json @@ -0,0 +1,42 @@ +{ + "type": "model", + "authority-id": "developer1", + "series": "16", + "brand-id": "developer1", + "model": "my-model", + "revision": "1", + "architecture": "amd64", + "timestamp": "2024-04-24T00:00:00+00:00", + "grade": "dangerous", + "base": "core24", + "serial-authority": ["generic"], + "snaps": [ + { + "default-channel": "24/edge", + "id": "UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH", + "name": "pc", + "type": "gadget" + }, + { + "default-channel": "24/edge", + "id": "pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza", + "name": "pc-kernel", + "type": "kernel", + "components": { + "wifi-comp": "optional" + } + }, + { + "default-channel": "latest/edge", + "id": "dwTAh7MZZ01zyriOZErqd1JynQLiOGvM", + "name": "core24", + "type": "base" + }, + { + "default-channel": "latest/edge", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", + "name": "snapd", + "type": "snapd" + } + ] +} diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go index 110b5b91e02..4330a61ccd6 100644 --- a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go +++ b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go @@ -29,7 +29,7 @@ import ( type cmdNewSnapDeclaration struct { Positional struct { - Snap string `description:"Snap file"` + Snap string `description:"Path to a snap file"` } `positional-args:"yes"` TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps,