mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
5114 lines
148 KiB
Go
5114 lines
148 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2014-2023 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package image_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
. "gopkg.in/check.v1"
|
|
|
|
"github.com/snapcore/snapd/asserts"
|
|
"github.com/snapcore/snapd/asserts/assertstest"
|
|
"github.com/snapcore/snapd/asserts/snapasserts"
|
|
"github.com/snapcore/snapd/asserts/sysdb"
|
|
"github.com/snapcore/snapd/bootloader"
|
|
"github.com/snapcore/snapd/bootloader/assets"
|
|
"github.com/snapcore/snapd/bootloader/bootloadertest"
|
|
"github.com/snapcore/snapd/bootloader/grubenv"
|
|
"github.com/snapcore/snapd/bootloader/ubootenv"
|
|
"github.com/snapcore/snapd/dirs"
|
|
"github.com/snapcore/snapd/gadget"
|
|
"github.com/snapcore/snapd/image"
|
|
"github.com/snapcore/snapd/image/preseed"
|
|
"github.com/snapcore/snapd/osutil"
|
|
"github.com/snapcore/snapd/overlord/auth"
|
|
"github.com/snapcore/snapd/progress"
|
|
"github.com/snapcore/snapd/seed"
|
|
"github.com/snapcore/snapd/seed/seedtest"
|
|
"github.com/snapcore/snapd/seed/seedwriter"
|
|
"github.com/snapcore/snapd/snap"
|
|
"github.com/snapcore/snapd/snap/snaptest"
|
|
"github.com/snapcore/snapd/store"
|
|
"github.com/snapcore/snapd/store/tooling"
|
|
"github.com/snapcore/snapd/testutil"
|
|
"github.com/snapcore/snapd/timings"
|
|
)
|
|
|
|
func Test(t *testing.T) { TestingT(t) }
|
|
|
|
type imageSuite struct {
|
|
testutil.BaseTest
|
|
root string
|
|
bootloader *bootloadertest.MockBootloader
|
|
|
|
stdout *bytes.Buffer
|
|
stderr *bytes.Buffer
|
|
|
|
storeActionsBunchSizes []int
|
|
storeActions []*store.SnapAction
|
|
curSnaps [][]*store.CurrentSnap
|
|
assertReqs []assertReq
|
|
seqReqs []seqReq
|
|
|
|
assertMaxFormats map[string]int
|
|
|
|
tsto *tooling.ToolingStore
|
|
|
|
// SeedSnaps helps creating and making available seed snaps
|
|
// (it provides MakeAssertedSnap etc.) for the tests.
|
|
*seedtest.SeedSnaps
|
|
|
|
model *asserts.Model
|
|
}
|
|
|
|
type assertReq struct {
|
|
ref asserts.Ref
|
|
maxFormats map[string]int
|
|
}
|
|
|
|
type seqReq struct {
|
|
key []string
|
|
sequence int
|
|
}
|
|
|
|
var _ = Suite(&imageSuite{})
|
|
|
|
var (
|
|
brandPrivKey, _ = assertstest.GenerateKey(752)
|
|
)
|
|
|
|
func (s *imageSuite) SetUpTest(c *C) {
|
|
s.root = c.MkDir()
|
|
s.bootloader = bootloadertest.Mock("grub", c.MkDir())
|
|
bootloader.Force(s.bootloader)
|
|
|
|
s.BaseTest.SetUpTest(c)
|
|
s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
|
|
|
|
s.AddCleanup(osutil.MockMountInfo(""))
|
|
|
|
s.stdout = &bytes.Buffer{}
|
|
image.Stdout = s.stdout
|
|
s.stderr = &bytes.Buffer{}
|
|
image.Stderr = s.stderr
|
|
s.tsto = tooling.MockToolingStore(s)
|
|
|
|
s.SeedSnaps = &seedtest.SeedSnaps{}
|
|
s.SetupAssertSigning("canonical")
|
|
s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{
|
|
"verification": "verified",
|
|
})
|
|
assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...)
|
|
|
|
s.model = s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
})
|
|
|
|
otherAcct := assertstest.NewAccount(s.StoreSigning, "other", map[string]interface{}{
|
|
"account-id": "other",
|
|
}, "")
|
|
s.StoreSigning.Add(otherAcct)
|
|
|
|
// mock the mount cmds (for the extract kernel assets stuff)
|
|
c1 := testutil.MockCommand(c, "mount", "")
|
|
s.AddCleanup(c1.Restore)
|
|
c2 := testutil.MockCommand(c, "umount", "")
|
|
s.AddCleanup(c2.Restore)
|
|
|
|
restore := image.MockWriteResolvedContent(func(_ string, _ *gadget.Info, _, _ string) error {
|
|
return nil
|
|
})
|
|
s.AddCleanup(restore)
|
|
|
|
dirs.SetRootDir(c.MkDir())
|
|
s.AddCleanup(func() { dirs.SetRootDir("") })
|
|
}
|
|
|
|
func (s *imageSuite) TearDownTest(c *C) {
|
|
s.BaseTest.TearDownTest(c)
|
|
bootloader.Force(nil)
|
|
image.Stdout = os.Stdout
|
|
image.Stderr = os.Stderr
|
|
s.storeActions = nil
|
|
s.storeActionsBunchSizes = nil
|
|
s.curSnaps = nil
|
|
s.assertReqs = nil
|
|
s.assertMaxFormats = nil
|
|
}
|
|
|
|
// interface for the store
|
|
func (s *imageSuite) SnapAction(_ context.Context, curSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, _ *auth.UserState, _ *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) {
|
|
if assertQuery != nil {
|
|
return nil, nil, fmt.Errorf("unexpected assertion query")
|
|
}
|
|
|
|
s.storeActionsBunchSizes = append(s.storeActionsBunchSizes, len(actions))
|
|
s.curSnaps = append(s.curSnaps, curSnaps)
|
|
sars := make([]store.SnapActionResult, 0, len(actions))
|
|
for _, a := range actions {
|
|
if a.Action != "download" {
|
|
return nil, nil, fmt.Errorf("unexpected action %q", a.Action)
|
|
}
|
|
|
|
if _, instanceKey := snap.SplitInstanceName(a.InstanceName); instanceKey != "" {
|
|
return nil, nil, fmt.Errorf("unexpected instance key in %q", a.InstanceName)
|
|
}
|
|
// record
|
|
s.storeActions = append(s.storeActions, a)
|
|
|
|
info := s.AssertedSnapInfo(a.InstanceName)
|
|
if info == nil {
|
|
return nil, nil, fmt.Errorf("no %q in the fake store", a.InstanceName)
|
|
}
|
|
info1 := *info
|
|
channel := a.Channel
|
|
redirectChannel := ""
|
|
if strings.HasPrefix(a.InstanceName, "default-track-") {
|
|
channel = "default-track/stable"
|
|
redirectChannel = channel
|
|
}
|
|
info1.Channel = channel
|
|
sars = append(sars, store.SnapActionResult{
|
|
Info: &info1,
|
|
RedirectChannel: redirectChannel,
|
|
})
|
|
}
|
|
|
|
return sars, nil, nil
|
|
}
|
|
|
|
func (s *imageSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error {
|
|
return osutil.CopyFile(s.AssertedSnap(name), targetFn, 0)
|
|
}
|
|
|
|
func (s *imageSuite) SetAssertionMaxFormats(m map[string]int) {
|
|
s.assertMaxFormats = m
|
|
}
|
|
|
|
func (s *imageSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) {
|
|
ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey}
|
|
s.assertReqs = append(s.assertReqs, assertReq{
|
|
ref: *ref,
|
|
maxFormats: s.assertMaxFormats,
|
|
})
|
|
if s.assertMaxFormats == nil {
|
|
return ref.Resolve(s.StoreSigning.Find)
|
|
} else {
|
|
h, err := asserts.HeadersFromPrimaryKey(assertType, primaryKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.StoreSigning.FindMaxFormat(assertType, h, s.assertMaxFormats[assertType.Name])
|
|
}
|
|
}
|
|
|
|
func (s *imageSuite) SeqFormingAssertion(assertType *asserts.AssertionType, sequenceKey []string, sequence int, user *auth.UserState) (asserts.Assertion, error) {
|
|
headers, err := asserts.HeadersFromSequenceKey(assertType, sequenceKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.seqReqs = append(s.seqReqs, seqReq{
|
|
key: sequenceKey,
|
|
sequence: sequence,
|
|
})
|
|
if sequence > 0 {
|
|
headers["sequence"] = strconv.Itoa(sequence)
|
|
return s.StoreSigning.Find(assertType, headers)
|
|
}
|
|
return s.StoreSigning.FindSequence(assertType, headers, -1, assertType.MaxSupportedFormat())
|
|
}
|
|
|
|
// TODO: use seedtest.SampleSnapYaml for some of these
|
|
const packageGadget = `
|
|
name: pc
|
|
version: 1.0
|
|
type: gadget
|
|
`
|
|
|
|
const packageGadgetWithBase = `
|
|
name: pc18
|
|
version: 1.0
|
|
type: gadget
|
|
base: core18
|
|
`
|
|
const packageClassicGadget = `
|
|
name: classic-gadget
|
|
version: 1.0
|
|
type: gadget
|
|
`
|
|
|
|
const packageClassicGadget18 = `
|
|
name: classic-gadget18
|
|
version: 1.0
|
|
type: gadget
|
|
base: core18
|
|
`
|
|
|
|
const packageKernel = `
|
|
name: pc-kernel
|
|
version: 4.4-1
|
|
type: kernel
|
|
`
|
|
|
|
const packageCore = `
|
|
name: core
|
|
version: 16.04
|
|
type: os
|
|
`
|
|
|
|
const packageCore18 = `
|
|
name: core18
|
|
version: 18.04
|
|
type: base
|
|
`
|
|
|
|
const snapdSnap = `
|
|
name: snapd
|
|
version: 3.14
|
|
type: snapd
|
|
`
|
|
|
|
const otherBase = `
|
|
name: other-base
|
|
version: 2.5029
|
|
type: base
|
|
`
|
|
|
|
const marchSnap = `
|
|
name: march-snap
|
|
version: 1.0
|
|
type: app
|
|
architectures: [ppc64el, arm64]
|
|
`
|
|
|
|
const march2Snap = `
|
|
name: march-snap
|
|
version: 1.0
|
|
type: app
|
|
architectures: [ppc64el, arm64, amd64]
|
|
`
|
|
|
|
const devmodeSnap = `
|
|
name: devmode-snap
|
|
version: 1.0
|
|
type: app
|
|
confinement: devmode
|
|
`
|
|
|
|
const classicSnap = `
|
|
name: classic-snap
|
|
version: 1.0
|
|
type: app
|
|
confinement: classic
|
|
`
|
|
|
|
const requiredSnap1 = `
|
|
name: required-snap1
|
|
version: 1.0
|
|
`
|
|
|
|
const requiredSnap18 = `
|
|
name: required-snap18
|
|
version: 1.0
|
|
base: core18
|
|
`
|
|
|
|
const defaultTrackSnap18 = `
|
|
name: default-track-snap18
|
|
version: 1.0
|
|
base: core18
|
|
`
|
|
|
|
const snapReqOtherBase = `
|
|
name: snap-req-other-base
|
|
version: 1.0
|
|
base: other-base
|
|
`
|
|
|
|
const snapReqCore16Base = `
|
|
name: snap-req-core16-base
|
|
version: 1.0
|
|
base: core16
|
|
`
|
|
|
|
const snapReqContentProvider = `
|
|
name: snap-req-content-provider
|
|
version: 1.0
|
|
plugs:
|
|
gtk-3-themes:
|
|
interface: content
|
|
default-provider: gtk-common-themes
|
|
target: $SNAP/data-dir/themes
|
|
`
|
|
|
|
const snapBaseNone = `
|
|
name: snap-base-none
|
|
version: 1.0
|
|
base: none
|
|
`
|
|
|
|
func (s *imageSuite) TestMissingModelAssertions(c *C) {
|
|
_, err := image.DecodeModelAssertion(&image.Options{})
|
|
c.Assert(err, ErrorMatches, "cannot read model assertion: open : no such file or directory")
|
|
}
|
|
|
|
func (s *imageSuite) TestIncorrectModelAssertions(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "broken-model.assertion")
|
|
err := os.WriteFile(fn, nil, 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot decode model assertion "%s": assertion content/signature separator not found`, fn))
|
|
}
|
|
|
|
func (s *imageSuite) TestValidButDifferentAssertion(c *C) {
|
|
var differentAssertion = []byte(`type: snap-declaration
|
|
authority-id: canonical
|
|
series: 16
|
|
snap-id: snap-id-1
|
|
snap-name: first
|
|
publisher-id: dev-id1
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`)
|
|
|
|
fn := filepath.Join(c.MkDir(), "different.assertion")
|
|
err := os.WriteFile(fn, differentAssertion, 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, ErrorMatches, fmt.Sprintf(`assertion in "%s" is not a model assertion`, fn))
|
|
}
|
|
|
|
func (s *imageSuite) TestModelAssertionReservedHeaders(c *C) {
|
|
const mod = `type: model
|
|
authority-id: brand
|
|
series: 16
|
|
brand-id: brand
|
|
model: baz-3000
|
|
architecture: armhf
|
|
gadget: brand-gadget
|
|
kernel: kernel
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`
|
|
|
|
reserved := []string{
|
|
"core",
|
|
"os",
|
|
"class",
|
|
"allowed-modes",
|
|
}
|
|
|
|
for _, rsvd := range reserved {
|
|
tweaked := strings.Replace(mod, "kernel: kernel\n", fmt.Sprintf("kernel: kernel\n%s: stuff\n", rsvd), 1)
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, []byte(tweaked), 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Check(err, ErrorMatches, fmt.Sprintf("model assertion cannot have reserved/unsupported header %q set", rsvd))
|
|
}
|
|
}
|
|
|
|
func (s *imageSuite) TestModelAssertionNoParallelInstancesOfSnaps(c *C) {
|
|
const mod = `type: model
|
|
authority-id: brand
|
|
series: 16
|
|
brand-id: brand
|
|
model: baz-3000
|
|
architecture: armhf
|
|
gadget: brand-gadget
|
|
kernel: kernel
|
|
required-snaps:
|
|
- foo_instance
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, []byte(mod), 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Check(err, ErrorMatches, `.* assertion model: invalid snap name in "required-snaps" header: foo_instance`)
|
|
}
|
|
|
|
func (s *imageSuite) TestModelAssertionNoParallelInstancesOfKernel(c *C) {
|
|
const mod = `type: model
|
|
authority-id: brand
|
|
series: 16
|
|
brand-id: brand
|
|
model: baz-3000
|
|
architecture: armhf
|
|
gadget: brand-gadget
|
|
kernel: kernel_instance
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, []byte(mod), 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Check(err, ErrorMatches, `.* assertion model: invalid snap name in "kernel" header: kernel_instance`)
|
|
}
|
|
|
|
func (s *imageSuite) TestModelAssertionNoParallelInstancesOfGadget(c *C) {
|
|
const mod = `type: model
|
|
authority-id: brand
|
|
series: 16
|
|
brand-id: brand
|
|
model: baz-3000
|
|
architecture: armhf
|
|
gadget: brand-gadget_instance
|
|
kernel: kernel
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, []byte(mod), 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Check(err, ErrorMatches, `.* assertion model: invalid snap name in "gadget" header: brand-gadget_instance`)
|
|
}
|
|
|
|
func (s *imageSuite) TestModelAssertionNoParallelInstancesOfBase(c *C) {
|
|
const mod = `type: model
|
|
authority-id: brand
|
|
series: 16
|
|
brand-id: brand
|
|
model: baz-3000
|
|
architecture: armhf
|
|
gadget: brand-gadget
|
|
kernel: kernel
|
|
base: core18_instance
|
|
timestamp: 2016-01-02T10:00:00-05:00
|
|
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
|
|
|
|
AXNpZw==
|
|
`
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, []byte(mod), 0644)
|
|
c.Assert(err, IsNil)
|
|
_, err = image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Check(err, ErrorMatches, `.* assertion model: invalid snap name in "base" header: core18_instance`)
|
|
}
|
|
|
|
func (s *imageSuite) TestHappyDecodeModelAssertion(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
a, err := image.DecodeModelAssertion(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
c.Check(a.Type(), Equals, asserts.ModelType)
|
|
}
|
|
|
|
func (s *imageSuite) MakeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) {
|
|
s.SeedSnaps.MakeAssertedSnap(c, snapYaml, files, revision, developerID, s.StoreSigning.Database)
|
|
}
|
|
|
|
const stableChannel = "stable"
|
|
|
|
const pcGadgetYaml = `
|
|
volumes:
|
|
pc:
|
|
bootloader: grub
|
|
`
|
|
|
|
const pcUC20GadgetYaml = `
|
|
volumes:
|
|
pc:
|
|
bootloader: grub
|
|
structure:
|
|
- name: ubuntu-seed
|
|
role: system-seed
|
|
type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B
|
|
size: 100M
|
|
- name: ubuntu-data
|
|
role: system-data
|
|
type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
|
|
size: 200M
|
|
`
|
|
|
|
const piUC20GadgetYaml = `
|
|
volumes:
|
|
pi:
|
|
schema: mbr
|
|
bootloader: u-boot
|
|
structure:
|
|
- name: ubuntu-seed
|
|
role: system-seed
|
|
type: 0C
|
|
size: 100M
|
|
- name: ubuntu-data
|
|
role: system-data
|
|
type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4
|
|
size: 200M
|
|
`
|
|
|
|
var snapdInfoFile = []string{"/usr/lib/snapd/info", `VERSION=2.55`}
|
|
|
|
func (s *imageSuite) setupSnaps(c *C, publishers map[string]string, defaultsYaml string) {
|
|
gadgetYaml := pcGadgetYaml + defaultsYaml
|
|
if _, ok := publishers["pc"]; ok {
|
|
s.MakeAssertedSnap(c, packageGadget, [][]string{
|
|
{"grub.conf", ""}, {"grub.cfg", "I'm a grub.cfg"},
|
|
{"meta/gadget.yaml", gadgetYaml},
|
|
}, snap.R(1), publishers["pc"])
|
|
}
|
|
if _, ok := publishers["pc18"]; ok {
|
|
s.MakeAssertedSnap(c, packageGadgetWithBase, [][]string{
|
|
{"grub.conf", ""}, {"grub.cfg", "I'm a grub.cfg"},
|
|
{"meta/gadget.yaml", gadgetYaml},
|
|
}, snap.R(4), publishers["pc18"])
|
|
}
|
|
|
|
if _, ok := publishers["classic-gadget"]; ok {
|
|
s.MakeAssertedSnap(c, packageClassicGadget, [][]string{
|
|
{"some-file", "Some file"},
|
|
}, snap.R(5), publishers["classic-gadget"])
|
|
}
|
|
|
|
if _, ok := publishers["classic-gadget18"]; ok {
|
|
s.MakeAssertedSnap(c, packageClassicGadget18, [][]string{
|
|
{"some-file", "Some file"},
|
|
}, snap.R(5), publishers["classic-gadget18"])
|
|
}
|
|
|
|
if _, ok := publishers["pc-kernel"]; ok {
|
|
s.MakeAssertedSnap(c, packageKernel, nil, snap.R(2), publishers["pc-kernel"])
|
|
}
|
|
|
|
s.MakeAssertedSnap(c, packageCore, [][]string{
|
|
{"/usr/lib/snapd/info", `VERSION=2.44`},
|
|
}, snap.R(3), "canonical")
|
|
s.MakeAssertedSnap(c, snapdSnap, [][]string{snapdInfoFile}, snap.R(18), "canonical")
|
|
|
|
s.MakeAssertedSnap(c, packageCore18, nil, snap.R(18), "canonical")
|
|
|
|
s.MakeAssertedSnap(c, otherBase, nil, snap.R(18), "other")
|
|
|
|
s.MakeAssertedSnap(c, snapReqCore16Base, nil, snap.R(16), "other")
|
|
|
|
s.MakeAssertedSnap(c, requiredSnap1, nil, snap.R(3), "other")
|
|
s.AssertedSnapInfo("required-snap1").LegacyEditedContact = "mailto:foo@example.com"
|
|
|
|
s.MakeAssertedSnap(c, requiredSnap18, nil, snap.R(6), "other")
|
|
s.AssertedSnapInfo("required-snap18").LegacyEditedContact = "mailto:foo@example.com"
|
|
|
|
s.MakeAssertedSnap(c, defaultTrackSnap18, nil, snap.R(5), "other")
|
|
|
|
s.MakeAssertedSnap(c, snapReqOtherBase, nil, snap.R(5), "other")
|
|
|
|
s.MakeAssertedSnap(c, snapReqContentProvider, nil, snap.R(5), "other")
|
|
|
|
s.MakeAssertedSnap(c, snapBaseNone, nil, snap.R(1), "other")
|
|
}
|
|
|
|
func (s *imageSuite) loadSeed(c *C, seeddir string) (essSnaps []*seed.Snap, runSnaps []*seed.Snap, roDB asserts.RODatabase) {
|
|
label := ""
|
|
systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*"))
|
|
c.Assert(err, IsNil)
|
|
if len(systems) > 1 {
|
|
c.Fatal("expected at most 1 Core 20 recovery system")
|
|
} else if len(systems) == 1 {
|
|
label = filepath.Base(systems[0])
|
|
}
|
|
|
|
sd, err := seed.Open(seeddir, label)
|
|
c.Assert(err, IsNil)
|
|
|
|
db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
|
|
Backstore: asserts.NewMemoryBackstore(),
|
|
Trusted: s.StoreSigning.Trusted,
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
commitTo := func(b *asserts.Batch) error {
|
|
return b.CommitTo(db, nil)
|
|
}
|
|
|
|
err = sd.LoadAssertions(db, commitTo)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = sd.LoadMeta(seed.AllModes, nil, timings.New(nil))
|
|
c.Assert(err, IsNil)
|
|
|
|
essSnaps = sd.EssentialSnaps()
|
|
runSnaps, err = sd.ModeSnaps("run")
|
|
c.Assert(err, IsNil)
|
|
|
|
return essSnaps, runSnaps, db
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeed(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
preparedir := c.MkDir()
|
|
rootdir := filepath.Join(preparedir, "image")
|
|
blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
gadgetWriteResolvedContentCalled := 0
|
|
restore = image.MockWriteResolvedContent(func(prepareImageDir string, info *gadget.Info, gadgetRoot, kernelRoot string) error {
|
|
c.Check(prepareImageDir, Equals, preparedir)
|
|
c.Check(gadgetRoot, Equals, filepath.Join(preparedir, "gadget"))
|
|
c.Check(kernelRoot, Equals, filepath.Join(preparedir, "kernel"))
|
|
gadgetWriteResolvedContentCalled++
|
|
return nil
|
|
})
|
|
defer restore()
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: preparedir,
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
|
|
SideInfo: &info.SideInfo,
|
|
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
|
|
Channel: stableChannel,
|
|
})
|
|
// precondition
|
|
if name == "core" {
|
|
c.Check(essSnaps[i].SideInfo.SnapID, Equals, s.AssertedSnapID("core"))
|
|
}
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, s.AssertedSnapInfo("required-snap1").Filename()),
|
|
|
|
SideInfo: &s.AssertedSnapInfo("required-snap1").SideInfo,
|
|
|
|
Required: true,
|
|
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check assertions
|
|
model1, err := s.model.Ref().Resolve(roDB.Find)
|
|
c.Assert(err, IsNil)
|
|
c.Check(model1, DeepEquals, s.model)
|
|
|
|
storeAccountKey := s.StoreSigning.StoreAccountKey("")
|
|
brandPubKey := s.Brands.PublicKey("my-brand")
|
|
_, err = roDB.Find(asserts.AccountKeyType, map[string]string{
|
|
"public-key-sha3-384": storeAccountKey.PublicKeyID(),
|
|
})
|
|
c.Check(err, IsNil)
|
|
_, err = roDB.Find(asserts.AccountKeyType, map[string]string{
|
|
"public-key-sha3-384": brandPubKey.ID(),
|
|
})
|
|
c.Check(err, IsNil)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core", "snap_menuentry")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Check(m["snap_core"], Equals, "core_3.snap")
|
|
c.Check(m["snap_menuentry"], Equals, "my display name")
|
|
|
|
// check symlinks from snap blob dir
|
|
kernelInfo := s.AssertedSnapInfo("pc-kernel")
|
|
coreInfo := s.AssertedSnapInfo("core")
|
|
kernelBlob := filepath.Join(blobdir, kernelInfo.Filename())
|
|
dst, err := os.Readlink(kernelBlob)
|
|
c.Assert(err, IsNil)
|
|
c.Check(dst, Equals, "../seed/snaps/pc-kernel_2.snap")
|
|
c.Check(kernelBlob, testutil.FilePresent)
|
|
|
|
coreBlob := filepath.Join(blobdir, coreInfo.Filename())
|
|
dst, err = os.Readlink(coreBlob)
|
|
c.Assert(err, IsNil)
|
|
c.Check(dst, Equals, "../seed/snaps/core_3.snap")
|
|
c.Check(coreBlob, testutil.FilePresent)
|
|
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{4})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 4,
|
|
}
|
|
declCount := 0
|
|
for _, req := range s.assertReqs {
|
|
if req.ref.Type == asserts.SnapDeclarationType {
|
|
c.Check(req.maxFormats, DeepEquals, expectedAssertMaxFormats)
|
|
declCount += 1
|
|
}
|
|
}
|
|
c.Check(declCount, Equals, 4)
|
|
|
|
// content was resolved and written for ubuntu-image
|
|
c.Check(gadgetWriteResolvedContentCalled, Equals, 1)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalCoreBrandKernel(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
coreFn := snaptest.MakeTestSnapWithFiles(c, packageCore, [][]string{{"local", ""}, snapdInfoFile})
|
|
requiredSnap1Fn := snaptest.MakeTestSnapWithFiles(c, requiredSnap1, [][]string{{"local", ""}})
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
coreFn,
|
|
requiredSnap1Fn,
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core_x1.snap", "pc-kernel", "pc"} {
|
|
channel := stableChannel
|
|
info := s.AssertedSnapInfo(name)
|
|
var pinfo snap.PlaceInfo = info
|
|
var sideInfo *snap.SideInfo
|
|
var snapType snap.Type
|
|
if info == nil {
|
|
switch name {
|
|
case "core_x1.snap":
|
|
pinfo = snap.MinimalPlaceInfo("core", snap.R(-1))
|
|
sideInfo = &snap.SideInfo{
|
|
RealName: "core",
|
|
}
|
|
channel = ""
|
|
snapType = snap.TypeOS
|
|
}
|
|
} else {
|
|
sideInfo = &info.SideInfo
|
|
snapType = info.Type()
|
|
}
|
|
|
|
fn := pinfo.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
|
|
SideInfo: sideInfo,
|
|
|
|
EssentialType: snapType,
|
|
Essential: true,
|
|
Required: true,
|
|
|
|
Channel: channel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_x1.snap"),
|
|
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
},
|
|
Required: true,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
// check assertions
|
|
decls, err := roDB.FindMany(asserts.SnapDeclarationType, nil)
|
|
c.Assert(err, IsNil)
|
|
// nothing for core
|
|
c.Check(decls, HasLen, 2)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core_x1.snap")
|
|
|
|
c.Check(s.stderr.String(), Equals, "WARNING: \"core\", \"required-snap1\" installed from local snaps disconnected from a store cannot be refreshed subsequently!\n")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithWideCohort(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{snapFile},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
WideCohortKey: "wide-cohort-key",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{4})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core",
|
|
Channel: stableChannel,
|
|
CohortKey: "wide-cohort-key",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: stableChannel,
|
|
CohortKey: "wide-cohort-key",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: stableChannel,
|
|
CohortKey: "wide-cohort-key",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
CohortKey: "wide-cohort-key",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedDevmodeSnap(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{snapFile},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Channel: "beta",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 2)
|
|
|
|
for i, name := range []string{"core", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, info.Filename()),
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "beta",
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required-snap1").SideInfo,
|
|
Required: true,
|
|
Channel: "beta",
|
|
})
|
|
// ensure local snaps are put last in seed.yaml
|
|
c.Check(runSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "devmode-snap_x1.snap"),
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "devmode-snap",
|
|
},
|
|
DevMode: true,
|
|
// no channel for unasserted snaps
|
|
Channel: "",
|
|
})
|
|
// check devmode-snap blob
|
|
c.Check(runSnaps[1].Path, testutil.FilePresent)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedImageManifest(c *C) {
|
|
// Use almost identical setup as TestSetupSeedDevmodeSnap to
|
|
// get each type of snap into the manifest
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
|
|
seedManifestPath := path.Join(rootdir, "seed.manifest")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{snapFile},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
SeedManifestPath: seedManifestPath,
|
|
Channel: "beta",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Assert(seedManifestPath, testutil.FilePresent)
|
|
c.Check(seedManifestPath, testutil.FileContains, `core 3
|
|
devmode-snap x1
|
|
pc 1
|
|
pc-kernel 2
|
|
required-snap1 3
|
|
`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithClassicSnapFails(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
s.MakeAssertedSnap(c, classicSnap, nil, snap.R(1), "other")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{s.AssertedSnap("classic-snap")},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Channel: "beta",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot use classic snap "classic-snap" in a core system`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBase(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
"required-snaps": []interface{}{"other-base"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
blobdir := filepath.Join(rootdir, "/var/lib/snapd/snaps")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snapd": "canonical",
|
|
"other-base": "other",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "core18_18.snap", "pc-kernel", "pc18"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core18_18.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
SnapID: s.AssertedSnapID("core18"),
|
|
RealName: "core18",
|
|
Revision: snap.R("18"),
|
|
},
|
|
SnapType: snap.TypeBase,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "other-base_18.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("other-base").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 5)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core18_18.snap")
|
|
|
|
// check symlinks from snap blob dir
|
|
kernelInfo := s.AssertedSnapInfo("pc-kernel")
|
|
baseInfo := s.AssertedSnapInfo("core18")
|
|
kernelBlob := filepath.Join(blobdir, kernelInfo.Filename())
|
|
dst, err := os.Readlink(kernelBlob)
|
|
c.Assert(err, IsNil)
|
|
c.Check(dst, Equals, "../seed/snaps/pc-kernel_2.snap")
|
|
c.Check(kernelBlob, testutil.FilePresent)
|
|
|
|
baseBlob := filepath.Join(blobdir, baseInfo.Filename())
|
|
dst, err = os.Readlink(baseBlob)
|
|
c.Assert(err, IsNil)
|
|
c.Check(dst, Equals, "../seed/snaps/core18_18.snap")
|
|
c.Check(baseBlob, testutil.FilePresent)
|
|
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{5})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "snapd",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core18",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc18",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 5,
|
|
"system-user": 1,
|
|
}
|
|
declCount := 0
|
|
for _, req := range s.assertReqs {
|
|
if req.ref.Type == asserts.SnapDeclarationType {
|
|
c.Check(req.maxFormats, DeepEquals, expectedAssertMaxFormats)
|
|
declCount += 1
|
|
}
|
|
}
|
|
c.Check(declCount, Equals, 5)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseWithCloudConf(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snapd": "canonical",
|
|
}, "")
|
|
s.MakeAssertedSnap(c, packageGadgetWithBase, [][]string{
|
|
{"grub.conf", ""},
|
|
{"grub.cfg", "I'm a grub.cfg"},
|
|
{"cloud.conf", "# cloud config"},
|
|
{"meta/gadget.yaml", pcGadgetYaml},
|
|
}, snap.R(5), "canonical")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
c.Check(filepath.Join(rootdir, "/etc/cloud/cloud.cfg"), testutil.FileEquals, "# cloud config")
|
|
}
|
|
|
|
func (s *imageSuite) testSetupSeedWithBaseWithCustomizationsAndDefaults(c *C, withDefaults bool) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
defaults := ""
|
|
if withDefaults {
|
|
defaults = `defaults:
|
|
system:
|
|
service:
|
|
ssh:
|
|
disable: true
|
|
`
|
|
}
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
})
|
|
|
|
tmpdir := c.MkDir()
|
|
rootdir := filepath.Join(tmpdir, "image")
|
|
cloudInitUserData := filepath.Join(tmpdir, "cloudstuff")
|
|
err := os.WriteFile(cloudInitUserData, []byte(`# user cloud data`), 0644)
|
|
c.Assert(err, IsNil)
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snapd": "canonical",
|
|
}, defaults)
|
|
s.MakeAssertedSnap(c, packageGadgetWithBase, [][]string{
|
|
{"grub.conf", ""},
|
|
{"grub.cfg", "I'm a grub.cfg"},
|
|
{"meta/gadget.yaml", pcGadgetYaml + defaults},
|
|
}, snap.R(5), "canonical")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
ConsoleConf: "disabled",
|
|
CloudInitUserData: cloudInitUserData,
|
|
},
|
|
}
|
|
|
|
err = image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check customization impl files were written
|
|
varCloudDir := filepath.Join(rootdir, "/var/lib/cloud/seed/nocloud-net")
|
|
c.Check(filepath.Join(varCloudDir, "meta-data"), testutil.FileEquals, "instance-id: nocloud-static\n")
|
|
c.Check(filepath.Join(varCloudDir, "user-data"), testutil.FileEquals, "# user cloud data")
|
|
// console-conf disable
|
|
c.Check(filepath.Join(rootdir, "_writable_defaults", "var/lib/console-conf/complete"), testutil.FilePresent)
|
|
|
|
// ssh file should be there if and only if the defaults are used
|
|
sshConfigExpected := withDefaults
|
|
c.Check(osutil.FileExists(filepath.Join(rootdir, "_writable_defaults/etc/ssh/sshd_not_to_be_run")), Equals, sshConfigExpected)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseWithCustomizations(c *C) {
|
|
withDefaults := false
|
|
s.testSetupSeedWithBaseWithCustomizationsAndDefaults(c, withDefaults)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseWithCustomizationsAndDefaults(c *C) {
|
|
withDefaults := true
|
|
s.testSetupSeedWithBaseWithCustomizationsAndDefaults(c, withDefaults)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareUC20CustomizationsUnsupported(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.makeUC20Model(nil)
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Customizations: image.Customizations{
|
|
ConsoleConf: "disabled",
|
|
CloudInitUserData: "cloud-init-user-data",
|
|
},
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot support with UC20\+ model requested customizations: console-conf disable, cloud-init user-data`)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicCustomizationsUnsupported(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
})
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
Classic: true,
|
|
ModelFile: fn,
|
|
Customizations: image.Customizations{
|
|
ConsoleConf: "disabled",
|
|
CloudInitUserData: "cloud-init-user-data",
|
|
BootFlags: []string{"boot-flag"},
|
|
},
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot support with classic model requested customizations: console-conf disable, boot flags \(boot-flag\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareUC18CustomizationsUnsupported(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
})
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Customizations: image.Customizations{
|
|
ConsoleConf: "disabled",
|
|
CloudInitUserData: "cloud-init-user-data",
|
|
BootFlags: []string{"boot-flag"},
|
|
},
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot support with UC16/18 model requested customizations: boot flags \(boot-flag\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseLegacySnap(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
})
|
|
|
|
// required-snap1 needs core, for backward compatibility
|
|
// we will add it implicitly but warn about this
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snapd": "canonical",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 2)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "core18_18.snap", "pc-kernel", "pc18"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core18_18.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
SnapID: s.AssertedSnapID("core18"),
|
|
RealName: "core18",
|
|
Revision: snap.R("18"),
|
|
},
|
|
SnapType: snap.TypeBase,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, s.AssertedSnapInfo("core").Filename()),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
Required: false, // strange but expected
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
c.Check(runSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, s.AssertedSnapInfo("required-snap1").Filename()),
|
|
SideInfo: &s.AssertedSnapInfo("required-snap1").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[1].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 6)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core18_18.snap")
|
|
|
|
c.Check(s.stderr.String(), Equals, "WARNING: model has base \"core18\" but some snaps (\"required-snap1\") require \"core\" as base as well, for compatibility it was added implicitly, adding \"core\" explicitly is recommended\n")
|
|
|
|
// current snap info sent
|
|
c.Check(s.curSnaps, HasLen, 2)
|
|
c.Check(s.curSnaps[0], HasLen, 0)
|
|
c.Check(s.curSnaps[1], DeepEquals, []*store.CurrentSnap{
|
|
{
|
|
InstanceName: "core18",
|
|
SnapID: s.AssertedSnapID("core18"),
|
|
Revision: snap.R(18),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
{
|
|
InstanceName: "pc-kernel",
|
|
SnapID: s.AssertedSnapID("pc-kernel"),
|
|
Revision: snap.R(2),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
{
|
|
InstanceName: "pc18",
|
|
SnapID: s.AssertedSnapID("pc18"),
|
|
Revision: snap.R(4),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
{
|
|
InstanceName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
{
|
|
InstanceName: "snapd",
|
|
SnapID: s.AssertedSnapID("snapd"),
|
|
Revision: snap.R(18),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseDefaultTrackSnap(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
"required-snaps": []interface{}{"default-track-snap18"},
|
|
})
|
|
|
|
// default-track-snap18 has a default-track
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snapd": "canonical",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "core18_18.snap", "pc-kernel", "pc18"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core18_18.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
SnapID: s.AssertedSnapID("core18"),
|
|
RealName: "core18",
|
|
Revision: snap.R("18"),
|
|
},
|
|
SnapType: snap.TypeBase,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, s.AssertedSnapInfo("default-track-snap18").Filename()),
|
|
SideInfo: &s.AssertedSnapInfo("default-track-snap18").SideInfo,
|
|
Required: true,
|
|
Channel: "default-track/stable",
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 5)
|
|
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedKernelPublisherMismatch(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "other",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot use kernel "pc-kernel" published by "other" for model by "my-brand"`)
|
|
}
|
|
|
|
func (s *imageSuite) TestInstallCloudConfigNoConfig(c *C) {
|
|
targetDir := c.MkDir()
|
|
emptyGadgetDir := c.MkDir()
|
|
|
|
err := image.InstallCloudConfig(targetDir, emptyGadgetDir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(osutil.FileExists(filepath.Join(targetDir, "etc/cloud")), Equals, false)
|
|
}
|
|
|
|
func (s *imageSuite) TestInstallCloudConfigWithCloudConfig(c *C) {
|
|
canary := []byte("ni! ni! ni!")
|
|
|
|
targetDir := c.MkDir()
|
|
gadgetDir := c.MkDir()
|
|
err := os.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), canary, 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.InstallCloudConfig(targetDir, gadgetDir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(filepath.Join(targetDir, "etc/cloud/cloud.cfg"), testutil.FileEquals, canary)
|
|
}
|
|
|
|
func (s *imageSuite) addSnapDecl(c *C, snapName, publisher string, headers map[string]interface{}) {
|
|
snapID := s.AssertedSnapID(snapName)
|
|
fullHeaders := map[string]interface{}{
|
|
"series": "16",
|
|
"snap-id": snapID,
|
|
"publisher-id": publisher,
|
|
"snap-name": snapName,
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
for h, v := range headers {
|
|
fullHeaders[h] = v
|
|
}
|
|
declA, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, fullHeaders, nil, "")
|
|
c.Assert(err, IsNil)
|
|
c.Assert(s.StoreSigning.Database.Add(declA), IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAsserts(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
s.AssertedSnap("required-snap1"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
s.addSnapDecl(c, "required-snap1", "my-brand", map[string]interface{}{
|
|
"revision": "1",
|
|
"format": "4",
|
|
})
|
|
s.addSnapDecl(c, "required-snap1", "my-brand", map[string]interface{}{
|
|
"revision": "2",
|
|
"format": "5",
|
|
})
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core_3.snap", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core_3.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
RealName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
},
|
|
SnapType: snap.TypeOS,
|
|
}
|
|
default:
|
|
c.Errorf("cannot have %s", name)
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
Required: true,
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
},
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check assertions
|
|
decls, err := roDB.FindMany(asserts.SnapDeclarationType, nil)
|
|
c.Assert(err, IsNil)
|
|
c.Check(decls, HasLen, 4)
|
|
for _, a := range decls {
|
|
decl := a.(*asserts.SnapDeclaration)
|
|
if decl.SnapName() == "required-snap1" {
|
|
// this was replaced with the format 4 one
|
|
c.Check(decl.Format(), Equals, 4)
|
|
}
|
|
}
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core_3.snap")
|
|
|
|
c.Check(s.stderr.String(), Equals, `WARNING: proceeding to download snaps ignoring validations, this default will change in the future. For now use --validation=enforce for validations to be taken into account, pass instead --validation=ignore to preserve current behavior going forward`+"\n")
|
|
|
|
// current snap info sent
|
|
c.Check(s.curSnaps, HasLen, 1)
|
|
c.Check(s.curSnaps[0], DeepEquals, []*store.CurrentSnap{
|
|
{
|
|
InstanceName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
{
|
|
InstanceName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: true,
|
|
},
|
|
})
|
|
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 4,
|
|
}
|
|
initial := make(map[string]bool)
|
|
later := make(map[string]bool)
|
|
for _, req := range s.assertReqs {
|
|
if req.ref.Type == asserts.SnapDeclarationType {
|
|
// we first fetch assertions for local required-snap1
|
|
// and core using default assertion max formats
|
|
if len(initial) < 2 {
|
|
c.Check(req.maxFormats, IsNil)
|
|
initial[req.ref.PrimaryKey[1]] = true
|
|
continue
|
|
}
|
|
c.Check(req.maxFormats, DeepEquals, expectedAssertMaxFormats)
|
|
later[req.ref.PrimaryKey[1]] = true
|
|
}
|
|
}
|
|
c.Check(initial, DeepEquals, map[string]bool{
|
|
s.AssertedSnapID("core"): true,
|
|
s.AssertedSnapID("requiredsnap1"): true,
|
|
})
|
|
c.Check(later, DeepEquals, map[string]bool{
|
|
s.AssertedSnapID("pc-kernel"): true,
|
|
s.AssertedSnapID("pc"): true,
|
|
s.AssertedSnapID("requiredsnap1"): true,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapsWithChannels(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
"core",
|
|
s.AssertedSnap("required-snap1"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
SnapChannels: map[string]string{
|
|
"core": "candidate",
|
|
// keep this comment for gofmt 1.9
|
|
s.AssertedSnap("required-snap1"): "edge",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core_3.snap", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
channel := stableChannel
|
|
if info == nil {
|
|
switch name {
|
|
case "core_3.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
RealName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
},
|
|
SnapType: snap.TypeOS,
|
|
}
|
|
channel = "candidate"
|
|
default:
|
|
c.Errorf("cannot have %s", name)
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: channel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
Required: true,
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
},
|
|
Channel: "edge",
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAssertsValidationEnforce(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("pc"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core_3.snap", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core_3.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
RealName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
},
|
|
SnapType: snap.TypeOS,
|
|
}
|
|
default:
|
|
c.Errorf("cannot have %s", name)
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
Required: true,
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
LegacyEditedContact: "mailto:foo@example.com",
|
|
},
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check assertions
|
|
decls, err := roDB.FindMany(asserts.SnapDeclarationType, nil)
|
|
c.Assert(err, IsNil)
|
|
c.Check(decls, HasLen, 4)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core_3.snap")
|
|
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
c.Check(s.curSnaps, HasLen, 1)
|
|
c.Check(s.curSnaps[0], DeepEquals, []*store.CurrentSnap{
|
|
{
|
|
InstanceName: "pc",
|
|
SnapID: s.AssertedSnapID("pc"),
|
|
Revision: snap.R(1),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: false,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestCannotCreateGadgetUnpackDir(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Channel: "stable",
|
|
PrepareDir: "/no-where",
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot create unpack dir "/no-where/gadget": mkdir .*`)
|
|
}
|
|
|
|
func (s *imageSuite) TestNoLocalParallelSnapInstances(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Snaps: []string{"foo_instance"},
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot use snap "foo_instance", parallel snap instances are unsupported`)
|
|
}
|
|
|
|
func (s *imageSuite) TestNoInvalidSnapNames(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Snaps: []string{"foo.invalid.name"},
|
|
})
|
|
c.Assert(err, ErrorMatches, `invalid snap name: "foo.invalid.name"`)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareInvalidChannel(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Channel: "x/x/x/x",
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot use global default option channel: channel name has too many components: x/x/x/x`)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicModeNoClassicModel(c *C) {
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(s.model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
Classic: true,
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, ErrorMatches, "cannot prepare the image for a core model with --classic mode specified")
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicModelNoClassicMode(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
})
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, ErrorMatches, "--classic mode is required to prepare the image for a classic model")
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicModelArchOverrideFails(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"architecture": "amd64",
|
|
})
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
Classic: true,
|
|
ModelFile: fn,
|
|
Architecture: "i386",
|
|
})
|
|
c.Assert(err, ErrorMatches, "cannot override model architecture: amd64")
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicModelSnapsButNoArchFails(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"gadget": "classic-gadget",
|
|
})
|
|
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
err := os.WriteFile(fn, asserts.Encode(model), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
err = image.Prepare(&image.Options{
|
|
Classic: true,
|
|
ModelFile: fn,
|
|
})
|
|
c.Assert(err, ErrorMatches, "cannot have snaps for a classic image without an architecture in the model or from --arch")
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareClassicModelNoModelAssertion(c *C) {
|
|
preparedir := c.MkDir()
|
|
s.setupSnaps(c, nil, "")
|
|
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
restore = sysdb.MockGenericClassicModel(s.StoreSigning.GenericClassicModel)
|
|
defer restore()
|
|
restore = image.MockNewToolingStoreFromModel(func(model *asserts.Model, fallbackArchitecture string) (*tooling.ToolingStore, error) {
|
|
return s.tsto, nil
|
|
})
|
|
defer restore()
|
|
|
|
// prepare an image with no model assertion but classic set to true
|
|
// to ensure the GenericClassicModel is used without error
|
|
err := image.Prepare(&image.Options{
|
|
Architecture: "amd64",
|
|
PrepareDir: preparedir,
|
|
Classic: true,
|
|
Snaps: []string{"required-snap18", "core18"},
|
|
})
|
|
c.Assert(err, IsNil)
|
|
|
|
// ensure the prepareDir was preseeded
|
|
seeddir := filepath.Join(preparedir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
c.Check(filepath.Join(seeddir, "seed.yaml"), testutil.FilePresent)
|
|
m, err := filepath.Glob(filepath.Join(seedsnapsdir, "*"))
|
|
c.Assert(err, IsNil)
|
|
// generic classic model has no other snaps, so we expect only the snaps
|
|
// that were passed in options to be present
|
|
c.Check(m, DeepEquals, []string{
|
|
filepath.Join(seedsnapsdir, "core18_18.snap"),
|
|
filepath.Join(seedsnapsdir, "required-snap18_6.snap"),
|
|
})
|
|
|
|
// check assertions
|
|
seedassertsdir := filepath.Join(seeddir, "assertions")
|
|
l, err := os.ReadDir(seedassertsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 9)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithKernelAndGadgetTrack(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc=18",
|
|
"kernel": "pc-kernel=18",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Channel: "stable",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
|
|
c.Check(essSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "core_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
EssentialType: snap.TypeOS,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "stable",
|
|
})
|
|
c.Check(essSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
|
|
EssentialType: snap.TypeKernel,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "18/stable",
|
|
})
|
|
c.Check(essSnaps[2], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "pc_1.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
|
|
EssentialType: snap.TypeGadget,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "18/stable",
|
|
})
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core",
|
|
Channel: "stable",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: "18/stable",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: "18/stable",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithKernelTrackWithDefaultChannel(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel=18",
|
|
})
|
|
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Channel: "edge",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
|
|
c.Check(essSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "core_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
EssentialType: snap.TypeOS,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "edge",
|
|
})
|
|
c.Check(essSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
|
|
EssentialType: snap.TypeKernel,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "18/edge",
|
|
})
|
|
c.Check(essSnaps[2], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "pc_1.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
|
|
EssentialType: snap.TypeGadget,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "edge",
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithKernelTrackOnLocalSnap(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel=18",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
// pretend we downloaded the core,kernel already
|
|
cfn := s.AssertedSnap("core")
|
|
kfn := s.AssertedSnap("pc-kernel")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Snaps: []string{kfn, cfn},
|
|
Channel: "beta",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
|
|
c.Check(essSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "core_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
EssentialType: snap.TypeOS,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "beta",
|
|
})
|
|
c.Check(essSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "pc-kernel_2.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
|
|
EssentialType: snap.TypeKernel,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: "18/beta",
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseAndLocalLegacyCoreOrdering(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"base": "core18",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
coreFn := snaptest.MakeTestSnapWithFiles(c, packageCore, [][]string{{"local", ""}})
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Snaps: []string{
|
|
coreFn,
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 2)
|
|
|
|
c.Check(essSnaps[0].Path, Equals, filepath.Join(seedsnapsdir, "snapd_18.snap"))
|
|
c.Check(essSnaps[1].Path, Equals, filepath.Join(seedsnapsdir, "core18_18.snap"))
|
|
c.Check(essSnaps[2].Path, Equals, filepath.Join(seedsnapsdir, "pc-kernel_2.snap"))
|
|
c.Check(essSnaps[3].Path, Equals, filepath.Join(seedsnapsdir, "pc18_4.snap"))
|
|
|
|
c.Check(runSnaps[0].Path, Equals, filepath.Join(seedsnapsdir, "core_x1.snap"))
|
|
c.Check(runSnaps[1].Path, Equals, filepath.Join(seedsnapsdir, "required-snap1_3.snap"))
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedWithBaseAndLegacyCoreOrdering(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"base": "core18",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1", "core"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"core": "canonical",
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 2)
|
|
|
|
c.Check(essSnaps[0].Path, Equals, filepath.Join(seedsnapsdir, "snapd_18.snap"))
|
|
c.Check(essSnaps[1].Path, Equals, filepath.Join(seedsnapsdir, "core18_18.snap"))
|
|
c.Check(essSnaps[2].Path, Equals, filepath.Join(seedsnapsdir, "pc-kernel_2.snap"))
|
|
c.Check(essSnaps[3].Path, Equals, filepath.Join(seedsnapsdir, "pc18_4.snap"))
|
|
|
|
c.Check(runSnaps[0].Path, Equals, filepath.Join(seedsnapsdir, "core_3.snap"))
|
|
c.Check(runSnaps[1].Path, Equals, filepath.Join(seedsnapsdir, "required-snap1_3.snap"))
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedGadgetBaseModelBaseMismatch(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
// replace model with a model that uses core18 and a gadget
|
|
// without a base
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"base": "core18",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core18": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot use gadget snap because its base "" is different from model base "core18"`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapReqBase(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"snap-req-other-base"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snap-req-other-base": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot add snap "snap-req-other-base" without also adding its base "other-base" explicitly`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedBaseNone(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"snap-base-none"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snap-base-none": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
c.Assert(image.SetupSeed(s.tsto, model, opts), IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore18GadgetDefaults(c *C) {
|
|
systemctlMock := testutil.MockCommand(c, "systemctl", "")
|
|
defer systemctlMock.Restore()
|
|
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
})
|
|
|
|
defaults := `defaults:
|
|
system:
|
|
service:
|
|
ssh:
|
|
disable: true
|
|
`
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, defaults)
|
|
|
|
snapdFn := snaptest.MakeTestSnapWithFiles(c, snapdSnap, [][]string{{"local", ""}, snapdInfoFile})
|
|
core18Fn := snaptest.MakeTestSnapWithFiles(c, packageCore18, [][]string{{"local", ""}})
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
snapdFn,
|
|
core18Fn,
|
|
},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
c.Check(osutil.FileExists(filepath.Join(rootdir, "_writable_defaults/etc/ssh/sshd_not_to_be_run")), Equals, true)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedStoreAssertionMissing(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"store": "my-store",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedStoreAssertionFetched(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// add store assertion
|
|
storeAs, err := s.StoreSigning.Sign(asserts.StoreType, map[string]interface{}{
|
|
"store": "my-store",
|
|
"operator-id": "canonical",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}, nil, "")
|
|
c.Assert(err, IsNil)
|
|
err = s.StoreSigning.Add(storeAs)
|
|
c.Assert(err, IsNil)
|
|
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"store": "my-store",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err = image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
|
|
// check the store assertion was fetched
|
|
_, err = roDB.Find(asserts.StoreType, map[string]string{
|
|
"store": "my-store",
|
|
})
|
|
c.Check(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapReqBaseFromLocal(c *C) {
|
|
// As originally written it let an extra snap fullfil
|
|
// the prereq of a required one, this does not work anymore!
|
|
// See TestSetupSeedSnapReqBaseFromExtraFails.
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"other-base", "snap-req-other-base"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snap-req-other-base": "canonical",
|
|
"other-base": "canonical",
|
|
}, "")
|
|
bfn := s.AssertedSnap("other-base")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Snaps: []string{bfn},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapReqBaseFromExtraFails(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"snap-req-other-base"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snap-req-other-base": "canonical",
|
|
"other-base": "canonical",
|
|
}, "")
|
|
bfn := s.AssertedSnap("other-base")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Snaps: []string{bfn},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Check(err, ErrorMatches, `cannot add snap "snap-req-other-base" without also adding its base "other-base" explicitly`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedMissingContentProvider(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"snap-req-content-provider"},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "canonical",
|
|
"snap-req-content-snap": "canonical",
|
|
}, "")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
// XXX content is empty because we disable SanitizePlugsSlots
|
|
c.Check(err, ErrorMatches, `prerequisites need to be added explicitly: cannot use snap "snap-req-content-provider": default provider "gtk-common-themes" or any alternative provider for content "" is missing`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassic(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// classic model with gadget etc
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"architecture": "amd64",
|
|
"gadget": "classic-gadget",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
})
|
|
|
|
rootdir := c.MkDir()
|
|
s.setupSnaps(c, map[string]string{
|
|
"classic-gadget": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
PrepareDir: rootdir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 2)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
c.Check(essSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "core_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
EssentialType: snap.TypeOS,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(essSnaps[0].Path, testutil.FilePresent)
|
|
c.Check(essSnaps[1], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "classic-gadget_5.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo,
|
|
EssentialType: snap.TypeGadget,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(essSnaps[1].Path, testutil.FilePresent)
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required-snap1").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 3)
|
|
|
|
// check that the bootloader is unset
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m, DeepEquals, map[string]string{
|
|
"snap_core": "",
|
|
"snap_kernel": "",
|
|
})
|
|
|
|
c.Check(s.stderr.String(), Matches, `WARNING: ensure that the contents under .*/var/lib/snapd/seed are owned by root:root in the \(final\) image\n`)
|
|
|
|
// no blob dir created
|
|
blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
|
|
c.Check(osutil.FileExists(blobdir), Equals, false)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassicUC20(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
{"grub-recovery.conf", "# recovery grub.cfg"},
|
|
{"grub.conf", "# boot grub.cfg"},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "")
|
|
s.makeSnap(c, "required20", nil, snap.R(21), "other")
|
|
|
|
// classic UC20+ based model
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"distribution": "ubuntu",
|
|
"display-name": "my model",
|
|
"architecture": "amd64",
|
|
"base": "core20",
|
|
"snaps": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"type": "kernel",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "pc",
|
|
"id": s.AssertedSnapID("pc"),
|
|
"type": "gadget",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "required20",
|
|
"id": s.AssertedSnapID("required20"),
|
|
},
|
|
},
|
|
})
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
PrepareDir: prepareDir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(prepareDir, "system-seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
stableChannel := "latest/stable"
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "pc-kernel", "core20", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
|
|
channel := stableChannel
|
|
switch name {
|
|
case "pc", "pc-kernel":
|
|
channel = "20"
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: channel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required20_21.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required20").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 5)
|
|
|
|
// Ensure that system-seed/ dir does not contain any bootloader or
|
|
// extra files other than "snaps" ans "systems".
|
|
dirs, err := filepath.Glob(seeddir + "/*")
|
|
c.Assert(err, IsNil)
|
|
c.Check(dirs, DeepEquals, []string{
|
|
seeddir + "/snaps", seeddir + "/systems",
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassicWithLocalClassicSnap(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// classic model
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"architecture": "amd64",
|
|
})
|
|
|
|
rootdir := c.MkDir()
|
|
s.setupSnaps(c, nil, "")
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, classicSnap, nil)
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
Snaps: []string{snapFile},
|
|
PrepareDir: rootdir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 1)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
c.Check(essSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "core_3.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("core").SideInfo,
|
|
EssentialType: snap.TypeOS,
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(essSnaps[0].Path, testutil.FilePresent)
|
|
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "classic-snap_x1.snap"),
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "classic-snap",
|
|
},
|
|
Classic: true,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 2)
|
|
|
|
// check that the bootloader is unset
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m, DeepEquals, map[string]string{
|
|
"snap_core": "",
|
|
"snap_kernel": "",
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassicSnapdOnly(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// classic model with gadget etc
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"architecture": "amd64",
|
|
"gadget": "classic-gadget18",
|
|
"required-snaps": []interface{}{"core18", "required-snap18"},
|
|
})
|
|
|
|
rootdir := c.MkDir()
|
|
s.setupSnaps(c, map[string]string{
|
|
"classic-gadget18": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
PrepareDir: rootdir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "classic-gadget18", "core18"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap18_6.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required-snap18").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check that the bootloader is unset
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m, DeepEquals, map[string]string{
|
|
"snap_core": "",
|
|
"snap_kernel": "",
|
|
})
|
|
|
|
c.Check(s.stderr.String(), Matches, `WARNING: ensure that the contents under .*/var/lib/snapd/seed are owned by root:root in the \(final\) image\n`)
|
|
|
|
// no blob dir created
|
|
blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
|
|
c.Check(osutil.FileExists(blobdir), Equals, false)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassicNoSnaps(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// classic model with gadget etc
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
})
|
|
|
|
rootdir := c.MkDir()
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
PrepareDir: rootdir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 0)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 0)
|
|
|
|
// check that the bootloader is unset
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m, DeepEquals, map[string]string{
|
|
"snap_core": "",
|
|
"snap_kernel": "",
|
|
})
|
|
|
|
// no blob dir created
|
|
blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
|
|
c.Check(osutil.FileExists(blobdir), Equals, false)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedClassicSnapdOnlyMissingCore16(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// classic model with gadget etc
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"classic": "true",
|
|
"architecture": "amd64",
|
|
"gadget": "classic-gadget18",
|
|
"required-snaps": []interface{}{"core18", "snap-req-core16-base"},
|
|
})
|
|
|
|
rootdir := c.MkDir()
|
|
s.setupSnaps(c, map[string]string{
|
|
"classic-gadget18": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Classic: true,
|
|
PrepareDir: rootdir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot use "snap-req-core16-base" requiring base "core16" without adding "core16" \(or "core"\) explicitly`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapd(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// replace model with a model that uses core18
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"gadget": "pc18",
|
|
"kernel": "pc-kernel",
|
|
"base": "core18",
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc18": "canonical",
|
|
"pc-kernel": "canonical",
|
|
}, "")
|
|
|
|
snapdFn := snaptest.MakeTestSnapWithFiles(c, snapdSnap, [][]string{{"local", ""}, snapdInfoFile})
|
|
core18Fn := snaptest.MakeTestSnapWithFiles(c, packageCore18, [][]string{{"local", ""}})
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
snapdFn,
|
|
core18Fn,
|
|
},
|
|
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(s.stdout.String(), Matches, `(?ms).*Copying ".*/snapd_3.14_all.snap" \(snapd\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestCore20MakeLabel(c *C) {
|
|
c.Check(image.MakeLabel(time.Date(2019, 10, 30, 0, 0, 0, 0, time.UTC)), Equals, "20191030")
|
|
}
|
|
|
|
func (s *imageSuite) makeSnap(c *C, yamlKey string, files [][]string, revno snap.Revision, publisher string) {
|
|
if publisher == "" {
|
|
publisher = "canonical"
|
|
}
|
|
s.MakeAssertedSnap(c, seedtest.SampleSnapYaml[yamlKey], files, revno, publisher)
|
|
}
|
|
|
|
func (s *imageSuite) makeUC20Model(extraHeaders map[string]interface{}) *asserts.Model {
|
|
comps := map[string]interface{}{
|
|
"comp1": "required",
|
|
"comp2": "optional",
|
|
}
|
|
headers := map[string]interface{}{
|
|
"display-name": "my model",
|
|
"architecture": "amd64",
|
|
"base": "core20",
|
|
"snaps": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"type": "kernel",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "pc",
|
|
"id": s.AssertedSnapID("pc"),
|
|
"type": "gadget",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "required20",
|
|
"id": s.AssertedSnapID("required20"),
|
|
"components": comps,
|
|
}},
|
|
}
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
return s.Brands.Model("my-brand", "my-model", headers)
|
|
}
|
|
|
|
func (s *imageSuite) testSetupSeedCore20Grub(c *C, kernelContent [][]string, expectedAssertMaxFormats map[string]int) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", kernelContent, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
{"grub-recovery.conf", "# recovery grub.cfg"},
|
|
{"grub.conf", "# boot grub.cfg"},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "")
|
|
s.makeSnap(c, "required20", nil, snap.R(21), "other")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(prepareDir, "system-seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
stableChannel := "latest/stable"
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "pc-kernel", "core20", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
|
|
channel := stableChannel
|
|
switch name {
|
|
case "pc", "pc-kernel":
|
|
channel = "20"
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: channel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required20_21.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required20").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 5)
|
|
|
|
// check boot config
|
|
grubCfg := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grub.cfg")
|
|
seedGrubenv := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grubenv")
|
|
grubRecoveryCfgAsset := assets.Internal("grub-recovery.cfg")
|
|
c.Assert(grubRecoveryCfgAsset, NotNil)
|
|
c.Check(grubCfg, testutil.FileEquals, string(grubRecoveryCfgAsset))
|
|
// make sure that grub.cfg and grubenv are the only files present inside
|
|
// the directory
|
|
gl, err := filepath.Glob(filepath.Join(prepareDir, "system-seed/EFI/ubuntu/*"))
|
|
c.Assert(err, IsNil)
|
|
c.Check(gl, DeepEquals, []string{
|
|
grubCfg,
|
|
seedGrubenv,
|
|
})
|
|
|
|
// check recovery system specific config
|
|
systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*"))
|
|
c.Assert(err, IsNil)
|
|
c.Assert(systems, HasLen, 1)
|
|
|
|
seedGenv := grubenv.NewEnv(seedGrubenv)
|
|
c.Assert(seedGenv.Load(), IsNil)
|
|
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, filepath.Base(systems[0]))
|
|
c.Check(seedGenv.Get("snapd_recovery_mode"), Equals, "install")
|
|
c.Check(seedGenv.Get("snapd_boot_flags"), Equals, "factory")
|
|
|
|
systemGenv := grubenv.NewEnv(filepath.Join(systems[0], "grubenv"))
|
|
c.Assert(systemGenv.Load(), IsNil)
|
|
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_1.snap")
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{5})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "snapd",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core20",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[4], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required20",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
declCount := 0
|
|
for _, req := range s.assertReqs {
|
|
if req.ref.Type == asserts.SnapDeclarationType {
|
|
c.Check(req.maxFormats, DeepEquals, expectedAssertMaxFormats)
|
|
declCount += 1
|
|
}
|
|
}
|
|
c.Check(declCount, Equals, 5)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20Grub(c *C) {
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 5,
|
|
"system-user": 1,
|
|
}
|
|
s.testSetupSeedCore20Grub(c, [][]string{{"snapd-info", `VERSION=2.55`}}, expectedAssertMaxFormats)
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20GrubMaxFormatsLCD(c *C) {
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 4,
|
|
"system-user": 0,
|
|
}
|
|
s.testSetupSeedCore20Grub(c, [][]string{{"snapd-info", `VERSION=2.44`}}, expectedAssertMaxFormats)
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20GrubNoKernelMaxFormats(c *C) {
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 5,
|
|
"system-user": 1,
|
|
}
|
|
s.testSetupSeedCore20Grub(c, nil, expectedAssertMaxFormats)
|
|
c.Check(s.stderr.String(), Equals, "WARNING: the kernel for the specified UC20+ model does not carry assertion max formats information, assuming possibly incorrectly the kernel revision can use the same formats as snapd\n")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20UBoot(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20 and our gadget
|
|
headers := map[string]interface{}{
|
|
"display-name": "my model",
|
|
"architecture": "arm64",
|
|
"base": "core20",
|
|
"snaps": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "arm-kernel",
|
|
"id": s.AssertedSnapID("arm-kernel"),
|
|
"type": "kernel",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "uboot-gadget",
|
|
"id": s.AssertedSnapID("uboot-gadget"),
|
|
"type": "gadget",
|
|
"default-channel": "20",
|
|
},
|
|
},
|
|
}
|
|
model := s.Brands.Model("my-brand", "my-model", headers)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
kernelContent := [][]string{
|
|
{"kernel.img", "some kernel"},
|
|
{"initrd.img", "some initrd"},
|
|
{"dtbs/foo.dtb", "some dtb"},
|
|
}
|
|
s.makeSnap(c, "arm-kernel=20", kernelContent, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
// this file must be empty
|
|
// TODO:UC20: write this test with non-empty uboot.env when we support
|
|
// that
|
|
{"uboot.conf", ""},
|
|
{"meta/gadget.yaml", piUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "uboot-gadget=20", gadgetContent, snap.R(22), "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// validity checks
|
|
seeddir := filepath.Join(prepareDir, "system-seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 0)
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check boot config
|
|
|
|
// uboot.env will be missing
|
|
ubootEnv := filepath.Join(prepareDir, "system-seed", "uboot.env")
|
|
c.Check(ubootEnv, testutil.FileAbsent)
|
|
|
|
// boot.sel will be present and have snapd_recovery_system set
|
|
expectedLabel := image.MakeLabel(time.Now())
|
|
bootSel := filepath.Join(prepareDir, "system-seed", "uboot", "ubuntu", "boot.sel")
|
|
|
|
env, err := ubootenv.Open(bootSel)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(env.Get("snapd_recovery_system"), Equals, expectedLabel)
|
|
c.Assert(env.Get("snapd_recovery_mode"), Equals, "install")
|
|
c.Assert(env.Get("snapd_boot_flags"), Equals, "factory")
|
|
|
|
// check recovery system specific config
|
|
systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*"))
|
|
c.Assert(err, IsNil)
|
|
c.Assert(systems, HasLen, 1)
|
|
c.Check(filepath.Base(systems[0]), Equals, expectedLabel)
|
|
|
|
// check we extracted the kernel assets
|
|
for _, fileAndContent := range kernelContent {
|
|
file := fileAndContent[0]
|
|
content := fileAndContent[1]
|
|
c.Assert(filepath.Join(systems[0], "kernel", file), testutil.FileEquals, content)
|
|
}
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20NoKernelRefsConsumed(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20 and our gadget
|
|
headers := map[string]interface{}{
|
|
"display-name": "my model",
|
|
"architecture": "arm64",
|
|
"base": "core20",
|
|
"snaps": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "arm-kernel",
|
|
"id": s.AssertedSnapID("arm-kernel"),
|
|
"type": "kernel",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "uboot-gadget",
|
|
"id": s.AssertedSnapID("uboot-gadget"),
|
|
"type": "gadget",
|
|
"default-channel": "20",
|
|
},
|
|
},
|
|
}
|
|
model := s.Brands.Model("my-brand", "my-model", headers)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
kernelYaml := `
|
|
assets:
|
|
ref:
|
|
update: true
|
|
content:
|
|
- dtbs/`
|
|
kernelContent := [][]string{
|
|
{"meta/kernel.yaml", kernelYaml},
|
|
{"kernel.img", "some kernel"},
|
|
{"initrd.img", "some initrd"},
|
|
{"dtbs/foo.dtb", "some dtb"},
|
|
}
|
|
s.makeSnap(c, "arm-kernel=20", kernelContent, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
// this file must be empty
|
|
// TODO:UC20: write this test with non-empty uboot.env when we support
|
|
// that
|
|
{"uboot.conf", ""},
|
|
{"meta/gadget.yaml", piUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "uboot-gadget=20", gadgetContent, snap.R(22), "")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `no asset from the kernel.yaml needing synced update is consumed by the gadget at "/.*"`)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareWithUC20Preseed(c *C) {
|
|
restoreSetupSeed := image.MockSetupSeed(func(tsto *tooling.ToolingStore, model *asserts.Model, opts *image.Options) error {
|
|
return nil
|
|
})
|
|
defer restoreSetupSeed()
|
|
|
|
var preseedCalled bool
|
|
restorePreseedCore20 := image.MockPreseedCore20(func(opts *preseed.CoreOptions) error {
|
|
preseedCalled = true
|
|
c.Assert(opts.PrepareImageDir, Equals, "/a/dir")
|
|
c.Assert(opts.PreseedSignKey, Equals, "foo")
|
|
c.Assert(opts.AppArmorKernelFeaturesDir, Equals, "/custom/aa/features")
|
|
c.Assert(opts.SysfsOverlay, Equals, "/sysfs-overlay")
|
|
return nil
|
|
})
|
|
defer restorePreseedCore20()
|
|
|
|
model := s.makeUC20Model(nil)
|
|
fn := filepath.Join(c.MkDir(), "model.assertion")
|
|
c.Assert(os.WriteFile(fn, asserts.Encode(model), 0644), IsNil)
|
|
|
|
err := image.Prepare(&image.Options{
|
|
ModelFile: fn,
|
|
Preseed: true,
|
|
PrepareDir: "/a/dir",
|
|
PreseedSignKey: "foo",
|
|
SysfsOverlay: "/sysfs-overlay",
|
|
|
|
AppArmorKernelFeaturesDir: "/custom/aa/features",
|
|
})
|
|
c.Assert(err, IsNil)
|
|
c.Check(preseedCalled, Equals, true)
|
|
}
|
|
|
|
func (s *imageSuite) TestPrepareWithClassicPreseedError(c *C) {
|
|
restoreSetupSeed := image.MockSetupSeed(func(tsto *tooling.ToolingStore, model *asserts.Model, opts *image.Options) error {
|
|
return nil
|
|
})
|
|
defer restoreSetupSeed()
|
|
|
|
err := image.Prepare(&image.Options{
|
|
Preseed: true,
|
|
Classic: true,
|
|
PrepareDir: "/a/dir",
|
|
})
|
|
c.Assert(err, ErrorMatches, `cannot preseed the image for a classic model`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20DelegatedSnap(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
{"grub.conf", "# boot grub.cfg"},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "")
|
|
|
|
ra := map[string]interface{}{
|
|
"account-id": "my-brand",
|
|
"provenance": []interface{}{"delegated-prov"},
|
|
}
|
|
s.MakeAssertedDelegatedSnap(c, seedtest.SampleSnapYaml["required20"]+"\nprovenance: delegated-prov\n", nil, snap.R(1), "my-brand", "my-brand", "delegated-prov", ra, s.StoreSigning.Database)
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Check(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) prepSetupSeedCore20DelegatedSnapAssertionMaxFormats(c *C) {
|
|
s.makeSnap(c, "snapd", [][]string{{"/usr/lib/snapd/info", `VERSION=2.44`}}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
{"grub.conf", "# boot grub.cfg"},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20DelegatedSnapAssertionMaxFormatsHappy(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.prepSetupSeedCore20DelegatedSnapAssertionMaxFormats(c)
|
|
|
|
ra := map[string]interface{}{
|
|
"account-id": "my-brand",
|
|
"provenance": []interface{}{"delegated-prov"},
|
|
}
|
|
s.MakeAssertedDelegatedSnap(c, seedtest.SampleSnapYaml["required20"]+"\nprovenance: delegated-prov\n", nil, snap.R(1), "my-brand", "my-brand", "delegated-prov", ra, s.StoreSigning.Database)
|
|
|
|
s.addSnapDecl(c, "required20", "my-brand", map[string]interface{}{
|
|
"revision": "1",
|
|
"format": "4",
|
|
"revision-authority": []interface{}{ra},
|
|
})
|
|
s.addSnapDecl(c, "required20", "my-brand", map[string]interface{}{
|
|
"revision": "2",
|
|
"format": "5",
|
|
"revision-authority": []interface{}{ra},
|
|
})
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Snaps: []string{
|
|
s.AssertedSnap("required20"),
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Check(err, IsNil)
|
|
|
|
expectedAssertMaxFormats := map[string]int{
|
|
"snap-declaration": 4,
|
|
}
|
|
initial := make(map[string]bool)
|
|
later := make(map[string]bool)
|
|
for _, req := range s.assertReqs {
|
|
if req.ref.Type == asserts.SnapDeclarationType {
|
|
// we first fetch assertions for local required20
|
|
// using default assertion max formats
|
|
if len(initial) < 1 {
|
|
c.Check(req.maxFormats, IsNil)
|
|
initial[req.ref.PrimaryKey[1]] = true
|
|
continue
|
|
}
|
|
c.Check(req.maxFormats, DeepEquals, expectedAssertMaxFormats)
|
|
later[req.ref.PrimaryKey[1]] = true
|
|
}
|
|
}
|
|
c.Check(initial, DeepEquals, map[string]bool{
|
|
s.AssertedSnapID("required20"): true,
|
|
})
|
|
c.Check(later, DeepEquals, map[string]bool{
|
|
s.AssertedSnapID("snapd"): true,
|
|
s.AssertedSnapID("core20"): true,
|
|
s.AssertedSnapID("pc-kernel"): true,
|
|
s.AssertedSnapID("pc"): true,
|
|
s.AssertedSnapID("required20"): true,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedCore20DelegatedSnapAssertionMaxFormatsAuthorityMismatch(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.prepSetupSeedCore20DelegatedSnapAssertionMaxFormats(c)
|
|
|
|
ra := map[string]interface{}{
|
|
"account-id": "my-brand",
|
|
"provenance": []interface{}{"delegated-prov"},
|
|
}
|
|
s.MakeAssertedDelegatedSnap(c, seedtest.SampleSnapYaml["required20"]+"\nprovenance: delegated-prov\n", nil, snap.R(1), "my-brand", "my-brand", "delegated-prov", ra, s.StoreSigning.Database)
|
|
|
|
// format 4 will be used but does not have revision-authority set up
|
|
s.addSnapDecl(c, "required20", "my-brand", map[string]interface{}{
|
|
"revision": "1",
|
|
"format": "4",
|
|
})
|
|
s.addSnapDecl(c, "required20", "my-brand", map[string]interface{}{
|
|
"revision": "2",
|
|
"format": "5",
|
|
"revision-authority": []interface{}{ra},
|
|
})
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Snaps: []string{
|
|
s.AssertedSnap("required20"),
|
|
},
|
|
}
|
|
|
|
// consistency checks will fail as format 4 has no revision-authority
|
|
// set up
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Check(err, ErrorMatches, `cannot add assertion snap-revision \(.*; provenance:delegated-prov\): snap-revision assertion with provenance "delegated-prov" for snap id "required20ididididididididididid" is not signed by an authorized authority: my-brand`)
|
|
}
|
|
|
|
func (s *imageSuite) testSetupSeedWithMixedSnapsAndRevisions(c *C, rules map[string]*seedwriter.ManifestSnapRevision) error {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
coreFn := snaptest.MakeTestSnapWithFiles(c, packageCore, [][]string{{"local", ""}, snapdInfoFile})
|
|
requiredSnap1Fn := snaptest.MakeTestSnapWithFiles(c, requiredSnap1, [][]string{{"local", ""}})
|
|
|
|
seedManifest := seedwriter.MockManifest(rules, nil, nil, nil)
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
coreFn,
|
|
requiredSnap1Fn,
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
SeedManifest: seedManifest,
|
|
}
|
|
|
|
if err := image.SetupSeed(s.tsto, s.model, opts); err != nil {
|
|
// let each unit test test against this
|
|
return err
|
|
}
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(runSnaps, DeepEquals, []*seed.Snap{
|
|
{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_x1.snap"),
|
|
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
},
|
|
Required: true,
|
|
},
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
// check the essential snaps, we have to do this in runtime instead
|
|
// of hardcoding as the information is "calculated".
|
|
c.Check(essSnaps, HasLen, 3)
|
|
for i, name := range []string{"core_x1.snap", "pc-kernel", "pc"} {
|
|
channel := stableChannel
|
|
info := s.AssertedSnapInfo(name)
|
|
var pinfo snap.PlaceInfo = info
|
|
var sideInfo *snap.SideInfo
|
|
var snapType snap.Type
|
|
if info == nil {
|
|
switch name {
|
|
case "core_x1.snap":
|
|
pinfo = snap.MinimalPlaceInfo("core", snap.R(-1))
|
|
sideInfo = &snap.SideInfo{
|
|
RealName: "core",
|
|
}
|
|
channel = ""
|
|
snapType = snap.TypeOS
|
|
}
|
|
} else {
|
|
sideInfo = &info.SideInfo
|
|
snapType = info.Type()
|
|
}
|
|
|
|
fn := pinfo.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
|
|
SideInfo: sideInfo,
|
|
|
|
EssentialType: snapType,
|
|
Essential: true,
|
|
Required: true,
|
|
|
|
Channel: channel,
|
|
})
|
|
}
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{2})
|
|
c.Check(s.storeActions, DeepEquals, []*store.SnapAction{
|
|
{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Revision: seedManifest.AllowedSnapRevision("pc-kernel"),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
},
|
|
{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Revision: seedManifest.AllowedSnapRevision("pc"),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapRevisionsWithCorrectLocalSnap(c *C) {
|
|
// It doesn't make sense to use a local snap when doing a reproducible build,
|
|
// so if a revision is provided, and we are trying to provide that snap locally,
|
|
// then we should return an error.
|
|
// Our helper creates two local snaps
|
|
// 1. core.
|
|
// 2. required-snap.
|
|
// So lets provide a revision for one of them and it should then fail
|
|
err := s.testSetupSeedWithMixedSnapsAndRevisions(c, map[string]*seedwriter.ManifestSnapRevision{
|
|
"pc-kernel": {SnapName: "pc-kernel", Revision: snap.R(2)},
|
|
"pc": {SnapName: "pc", Revision: snap.R(1)},
|
|
"core": {SnapName: "core", Revision: snap.R(-1)},
|
|
})
|
|
c.Check(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapRevisionsWithLocalSnapFails(c *C) {
|
|
// It doesn't make sense to use a local snap when doing a reproducible build,
|
|
// so if a revision is provided, and we are trying to provide that snap locally,
|
|
// then we should return an error.
|
|
// Our helper creates two local snaps
|
|
// 1. core.
|
|
// 2. required-snap.
|
|
// So lets provide a revision for one of them and it should then fail
|
|
err := s.testSetupSeedWithMixedSnapsAndRevisions(c, map[string]*seedwriter.ManifestSnapRevision{
|
|
"pc-kernel": {SnapName: "pc-kernel", Revision: snap.R(2)},
|
|
"pc": {SnapName: "pc", Revision: snap.R(1)},
|
|
"core": {SnapName: "core", Revision: snap.R(5)},
|
|
})
|
|
c.Check(err, ErrorMatches, `cannot record snap for manifest: snap "core" \(x1\) does not match the allowed revision 5`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapRevisionsWithLocalSnapHappy(c *C) {
|
|
// Make sure we can still provide specific revisions for snaps that are
|
|
// non-local.
|
|
err := s.testSetupSeedWithMixedSnapsAndRevisions(c, map[string]*seedwriter.ManifestSnapRevision{
|
|
"pc-kernel": {SnapName: "pc-kernel", Revision: snap.R(2)},
|
|
"pc": {SnapName: "pc", Revision: snap.R(1)},
|
|
})
|
|
c.Check(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapRevisionsDownloadHappy(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
prepareDir := c.MkDir()
|
|
|
|
// Create a new core20 image with the following snaps:
|
|
// snapd, core20, pc-kernel, pc, required20
|
|
// We will use revisions for each of them to guarantee that
|
|
// exact revisions will be used when the store action is invoked.
|
|
// The revisions provided to s.makeSnap won't matter, and they shouldn't.
|
|
// Instead the revision provided in the revisions map should be used instead.
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(133), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(58), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(15), "")
|
|
gadgetContent := [][]string{
|
|
{"uboot.conf", ""},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(12), "")
|
|
s.makeSnap(c, "required20", nil, snap.R(59), "other")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
SeedManifest: seedwriter.MockManifest(map[string]*seedwriter.ManifestSnapRevision{
|
|
"snapd": {SnapName: "snapd", Revision: snap.R(133)},
|
|
"core20": {SnapName: "core20", Revision: snap.R(58)},
|
|
"pc-kernel": {SnapName: "pc-kernel", Revision: snap.R(15)},
|
|
"pc": {SnapName: "pc", Revision: snap.R(12)},
|
|
"required20": {SnapName: "required20", Revision: snap.R(59)},
|
|
}, nil, nil, nil),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(prepareDir, "system-seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
stableChannel := "latest/stable"
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "pc-kernel", "core20", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
|
|
channel := stableChannel
|
|
switch name {
|
|
case "pc", "pc-kernel":
|
|
channel = "20"
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: channel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required20_59.snap"),
|
|
SideInfo: &s.AssertedSnapInfo("required20").SideInfo,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 5)
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{5})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "snapd",
|
|
Revision: snap.R(133),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Revision: snap.R(15),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core20",
|
|
Revision: snap.R(58),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Revision: snap.R(12),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[4], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required20",
|
|
Revision: snap.R(59),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapRevisionsDownloadWrongRevision(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(nil)
|
|
prepareDir := c.MkDir()
|
|
|
|
// Create a new core20 image with the following snaps:
|
|
// snapd, core20, pc-kernel, pc, required20
|
|
// We will use revisions for each of them to guarantee that
|
|
// exact revisions will be used when the store action is invoked.
|
|
// The revisions provided to s.makeSnap won't matter, and they shouldn't.
|
|
// Instead the revision provided in the revisions map should be used instead.
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(133), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(58), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(15), "")
|
|
gadgetContent := [][]string{
|
|
{"uboot.conf", ""},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(12), "")
|
|
|
|
// Create required20 with a revision of 100, that does not match
|
|
// the revision we want. We end up requesting revision 15, but end
|
|
// up with 100. This must error.
|
|
s.makeSnap(c, "required20", nil, snap.R(100), "other")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
SeedManifest: seedwriter.MockManifest(map[string]*seedwriter.ManifestSnapRevision{
|
|
"required20": {SnapName: "required20", Revision: snap.R(15)},
|
|
}, nil, nil, nil),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `cannot record snap for manifest: snap "required20" \(100\) does not match the allowed revision 15`)
|
|
|
|
// check the downloads, make sure that required20 was requested
|
|
// as revision 15
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{5})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "snapd",
|
|
Channel: "latest/stable",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core20",
|
|
Channel: "latest/stable",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[4], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required20",
|
|
Revision: snap.R(15),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestLocalSnapRevisionMatchingStoreRevision(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
SeedManifest: seedwriter.MockManifest(map[string]*seedwriter.ManifestSnapRevision{
|
|
"core": {SnapName: "core", Revision: snap.R(3)},
|
|
}, nil, nil, nil),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, roDB := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 3)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"core_3.snap", "pc-kernel", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
if info == nil {
|
|
switch name {
|
|
case "core_3.snap":
|
|
info = &snap.Info{
|
|
SideInfo: snap.SideInfo{
|
|
RealName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
},
|
|
SnapType: snap.TypeOS,
|
|
}
|
|
default:
|
|
c.Errorf("cannot have %s", name)
|
|
}
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: stableChannel,
|
|
})
|
|
}
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(seedsnapsdir, "required-snap1_3.snap"),
|
|
Required: true,
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required-snap1",
|
|
SnapID: s.AssertedSnapID("required-snap1"),
|
|
Revision: snap.R(3),
|
|
LegacyEditedContact: "mailto:foo@example.com",
|
|
},
|
|
Channel: stableChannel,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
// check assertions
|
|
decls, err := roDB.FindMany(asserts.SnapDeclarationType, nil)
|
|
c.Assert(err, IsNil)
|
|
c.Check(decls, HasLen, 4)
|
|
|
|
// check the bootloader config
|
|
m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
|
|
c.Assert(err, IsNil)
|
|
c.Check(m["snap_core"], Equals, "core_3.snap")
|
|
|
|
c.Check(s.stderr.String(), Equals, "")
|
|
|
|
// check the downloads, make sure no core snap downloads are
|
|
// present as we are using the local file for this.
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
})
|
|
|
|
// Verify that the local file is of correct revision (3)
|
|
c.Check(s.curSnaps, HasLen, 1)
|
|
c.Check(s.curSnaps[0], DeepEquals, []*store.CurrentSnap{
|
|
{
|
|
InstanceName: "core",
|
|
SnapID: s.AssertedSnapID("core"),
|
|
Revision: snap.R(3),
|
|
TrackingChannel: "stable",
|
|
Epoch: snap.E("0"),
|
|
IgnoreValidation: false,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) setupValidationSet(c *C, name string, snaps []interface{}) *asserts.ValidationSet {
|
|
vs, err := s.StoreSigning.Sign(asserts.ValidationSetType, map[string]interface{}{
|
|
"type": "validation-set",
|
|
"authority-id": "canonical",
|
|
"series": "16",
|
|
"account-id": "canonical",
|
|
"name": name,
|
|
"sequence": "1",
|
|
"snaps": snaps,
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}, nil, "")
|
|
c.Assert(err, IsNil)
|
|
err = s.StoreSigning.Add(vs)
|
|
c.Check(err, IsNil)
|
|
return vs.(*asserts.ValidationSet)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedValidationSetsUnmetCriteria(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses validation-sets
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"validation-sets": []interface{}{
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "base-set",
|
|
"mode": "enforce",
|
|
},
|
|
},
|
|
})
|
|
|
|
// setup validation-sets that will fail the check
|
|
vsa := s.setupValidationSet(c, "base-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
"revision": "6",
|
|
},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err.Error(), testutil.Contains, `pc-kernel (required at revision 6 by sets canonical/base-set)`)
|
|
|
|
// ensure download actions were invoked with the validation-sets
|
|
// described in the model.
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
// For pc snap we must have both correct revision (6) and
|
|
// the validation-set applied
|
|
Revision: snap.R(6),
|
|
Flags: store.SnapActionEnforceValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedValidationSetsUnmetCriteriaButIgnoredValidation(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses validation-sets
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"validation-sets": []interface{}{
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "base-set",
|
|
"mode": "enforce",
|
|
},
|
|
},
|
|
})
|
|
|
|
// setup validation-sets that will fail the check
|
|
vsa := s.setupValidationSet(c, "base-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
"revision": "6",
|
|
},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "ignore",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// ensure download actions were invoked with the validation-sets
|
|
// described in the model.
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
// For pc snap we must have both correct revision (6) and
|
|
// the validation-set applied
|
|
Revision: snap.R(6),
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestDownloadSnapsModelValidationSets(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses validation-sets
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"validation-sets": []interface{}{
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "base-set",
|
|
"mode": "enforce",
|
|
},
|
|
},
|
|
})
|
|
|
|
// setup validation-sets
|
|
vsa := s.setupValidationSet(c, "base-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
// setupSnaps sets pc-kernel to snap.R(2)
|
|
"revision": "2",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "pc",
|
|
"id": s.AssertedSnapID("pc"),
|
|
"presence": "required",
|
|
"revision": "1",
|
|
},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// ensure download actions were invoked with the validation-sets
|
|
// described in the model.
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
// For pc snap we must have both correct revision (2) and
|
|
// the validation-set applied
|
|
Revision: snap.R(2),
|
|
Flags: store.SnapActionEnforceValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
// For pc snap we must have both correct revision (1) and
|
|
// the validation-set applied
|
|
Revision: snap.R(1),
|
|
Flags: store.SnapActionEnforceValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestDownloadSnapsManifestValidationSets(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses validation-sets
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"validation-sets": []interface{}{
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "base-set",
|
|
"mode": "enforce",
|
|
},
|
|
},
|
|
})
|
|
|
|
// setup validation-sets
|
|
vsa := s.setupValidationSet(c, "base-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
// setupSnaps sets pc-kernel to snap.R(2)
|
|
"revision": "2",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "pc",
|
|
"id": s.AssertedSnapID("pc"),
|
|
"presence": "required",
|
|
"revision": "1",
|
|
},
|
|
})
|
|
|
|
rootDir := c.MkDir()
|
|
imageDir := filepath.Join(rootDir, "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
// write a seed.manifest we will provide to image
|
|
manifestFile := filepath.Join(rootDir, "seed.manifest")
|
|
err := os.WriteFile(manifestFile, []byte("canonical/base-set 1"), 0644)
|
|
c.Assert(err, IsNil)
|
|
|
|
manifest, err := seedwriter.ReadManifest(manifestFile)
|
|
c.Assert(err, IsNil)
|
|
c.Assert(manifest.AllowedValidationSets(), HasLen, 1)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(imageDir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
SeedManifest: manifest,
|
|
}
|
|
|
|
err = image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// ensure that the assertion fetcher was called
|
|
c.Check(s.seqReqs, DeepEquals, []seqReq{
|
|
{
|
|
key: []string{"16", "canonical", "base-set"},
|
|
sequence: 1,
|
|
},
|
|
})
|
|
|
|
// ensure download actions were invoked with the validation-sets
|
|
// described in the model.
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{3})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
// For pc-kernel snap we must have both correct revision (2) and
|
|
// the validation-set applied
|
|
Revision: snap.R(2),
|
|
Flags: store.SnapActionEnforceValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
// For pc snap we must have both correct revision (1) and
|
|
// the validation-set applied
|
|
Revision: snap.R(1),
|
|
Flags: store.SnapActionEnforceValidation,
|
|
ValidationSets: []snapasserts.ValidationSetKey{
|
|
snapasserts.NewValidationSetKey(vsa),
|
|
},
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "required-snap1",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionEnforceValidation,
|
|
// Empty validation-sets for this one as no validation-set applies here
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestImageSeedValidationSetConflict(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses validation-sets
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"display-name": "my display name",
|
|
"architecture": "amd64",
|
|
"gadget": "pc",
|
|
"kernel": "pc-kernel",
|
|
"required-snaps": []interface{}{"required-snap1"},
|
|
"validation-sets": []interface{}{
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "base-set",
|
|
"mode": "enforce",
|
|
},
|
|
map[string]interface{}{
|
|
"account-id": "canonical",
|
|
"name": "other-set",
|
|
"mode": "enforce",
|
|
},
|
|
},
|
|
})
|
|
|
|
// setup conflicting validation-sets, one that requests revision
|
|
// 1 of pc-kernel, and one that requests revision 7
|
|
s.setupValidationSet(c, "base-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
"revision": "1",
|
|
},
|
|
})
|
|
s.setupValidationSet(c, "other-set", []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"presence": "required",
|
|
"revision": "7",
|
|
},
|
|
})
|
|
|
|
rootDir := c.MkDir()
|
|
imageDir := filepath.Join(rootDir, "image")
|
|
s.setupSnaps(c, map[string]string{
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{
|
|
s.AssertedSnap("core"),
|
|
},
|
|
PrepareDir: filepath.Dir(imageDir),
|
|
Customizations: image.Customizations{
|
|
Validation: "enforce",
|
|
},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `*cannot constrain snap "pc-kernel" at different revisions 1 \(canonical/base-set\), 7 \(canonical/other-set\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapWithInvalidArchitecture(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
a64Snap := snaptest.MakeTestSnapWithFiles(c, marchSnap, nil)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{a64Snap},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, ErrorMatches, `snap "march-snap" supported architectures \(ppc64el, arm64\) are incompatible with the model architecture \(amd64\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapWithInvalidModelArchButArchOverriden(c *C) {
|
|
// Test that the local snap has a architecture that does not match the model, however
|
|
// that we can indeed override this with the image options.
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
a64Snap := snaptest.MakeTestSnapWithFiles(c, marchSnap, nil)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{a64Snap},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
Architecture: "arm64",
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalSnapWithMultipleArchs(c *C) {
|
|
// Test that the architecture is correctly validated when there is a mix
|
|
// of architectures specified in the snap. (march2Snap)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
s.setupSnaps(c, map[string]string{
|
|
"core": "canonical",
|
|
"pc": "canonical",
|
|
"pc-kernel": "my-brand",
|
|
}, "")
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
sn := snaptest.MakeTestSnapWithFiles(c, march2Snap, nil)
|
|
|
|
opts := &image.Options{
|
|
Snaps: []string{sn},
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, s.model, opts)
|
|
c.Assert(err, IsNil)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedSnapInvalidArchitecture(c *C) {
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "")
|
|
s.makeSnap(c, "pc=20", nil, snap.R(22), "")
|
|
s.MakeAssertedSnap(c, marchSnap, nil, snap.R(18), "canonical")
|
|
|
|
// replace model with a model that has an extra snap
|
|
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
|
|
"architecture": "amd64",
|
|
"base": "core20",
|
|
"grade": "dangerous",
|
|
"snaps": []interface{}{
|
|
map[string]interface{}{
|
|
"name": "pc-kernel",
|
|
"id": s.AssertedSnapID("pc-kernel"),
|
|
"type": "kernel",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "pc",
|
|
"id": s.AssertedSnapID("pc"),
|
|
"type": "gadget",
|
|
"default-channel": "20",
|
|
},
|
|
map[string]interface{}{
|
|
"name": "march-snap",
|
|
"id": s.AssertedSnapID("march-snap"),
|
|
"type": "app",
|
|
},
|
|
},
|
|
})
|
|
|
|
rootdir := filepath.Join(c.MkDir(), "image")
|
|
opts := &image.Options{
|
|
PrepareDir: filepath.Dir(rootdir),
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, `snap "march-snap" supported architectures \(ppc64el, arm64\) are incompatible with the model architecture \(amd64\)`)
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedFetchText(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
model := s.makeUC20Model(nil)
|
|
prepareDir := c.MkDir()
|
|
|
|
// initialize a bunch of snaps for the model
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(133), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(1), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(2), "")
|
|
gadgetContent := [][]string{
|
|
{"uboot.conf", ""},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(10), "")
|
|
s.makeSnap(c, "required20", nil, snap.R(2), "other")
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
// Make sure we also test the case of a specific revision
|
|
SeedManifest: seedwriter.MockManifest(map[string]*seedwriter.ManifestSnapRevision{
|
|
"snapd": {SnapName: "snapd", Revision: snap.R(133)},
|
|
}, nil, nil, nil),
|
|
}
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// test that the fetching looks correct
|
|
c.Assert(s.stdout.String(), testutil.Contains, "Fetching snapd (133)")
|
|
c.Assert(s.stdout.String(), testutil.Contains, "Fetching core20 (1)")
|
|
c.Assert(s.stdout.String(), testutil.Contains, "Fetching pc-kernel (2)")
|
|
c.Assert(s.stdout.String(), testutil.Contains, "Fetching pc (10)")
|
|
c.Assert(s.stdout.String(), testutil.Contains, "Fetching required20 (2)")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalComponents(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(map[string]interface{}{"grade": "dangerous"})
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
s.makeSnap(c, "snapd", [][]string{snapdInfoFile}, snap.R(1), "")
|
|
s.makeSnap(c, "core20", nil, snap.R(20), "")
|
|
s.makeSnap(c, "pc-kernel=20", nil, snap.R(1), "")
|
|
gadgetContent := [][]string{
|
|
{"grub.conf", "# boot grub.cfg"},
|
|
{"meta/gadget.yaml", pcUC20GadgetYaml},
|
|
}
|
|
s.makeSnap(c, "pc=20", gadgetContent, snap.R(22), "")
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, seedtest.SampleSnapYaml["required20"], nil)
|
|
comp1File := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp1"])
|
|
comp2File := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp2"])
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
Snaps: []string{snapFile},
|
|
Components: []string{comp1File, comp2File},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, IsNil)
|
|
|
|
// check seed
|
|
seeddir := filepath.Join(prepareDir, "system-seed")
|
|
seedsnapsdir := filepath.Join(seeddir, "snaps")
|
|
essSnaps, runSnaps, _ := s.loadSeed(c, seeddir)
|
|
c.Check(essSnaps, HasLen, 4)
|
|
c.Check(runSnaps, HasLen, 1)
|
|
|
|
stableChannel := "latest/stable"
|
|
|
|
// check the files are in place
|
|
for i, name := range []string{"snapd", "pc-kernel", "core20", "pc"} {
|
|
info := s.AssertedSnapInfo(name)
|
|
|
|
channel := stableChannel
|
|
switch name {
|
|
case "pc", "pc-kernel":
|
|
channel = "20"
|
|
}
|
|
|
|
fn := info.Filename()
|
|
p := filepath.Join(seedsnapsdir, fn)
|
|
c.Check(p, testutil.FilePresent)
|
|
c.Check(essSnaps[i], DeepEquals, &seed.Snap{
|
|
Path: p,
|
|
SideInfo: &info.SideInfo,
|
|
EssentialType: info.Type(),
|
|
Essential: true,
|
|
Required: true,
|
|
Channel: channel,
|
|
})
|
|
}
|
|
expectedLabel := image.MakeLabel(time.Now())
|
|
extraSnapsDir := filepath.Join(seeddir, "systems", expectedLabel, "snaps")
|
|
c.Check(runSnaps[0], DeepEquals, &seed.Snap{
|
|
Path: filepath.Join(extraSnapsDir, "required20_1.0.snap"),
|
|
SideInfo: &snap.SideInfo{
|
|
RealName: "required20",
|
|
},
|
|
Required: true,
|
|
})
|
|
c.Check(runSnaps[0].Path, testutil.FilePresent)
|
|
|
|
// TODO these files will be loaded when opening the seed, but that is
|
|
// not implemented yet
|
|
c.Check(osutil.FileExists(filepath.Join(extraSnapsDir, "required20+comp1_1.0.comp")),
|
|
Equals, true)
|
|
c.Check(osutil.FileExists(filepath.Join(extraSnapsDir, "required20+comp2_2.0.comp")),
|
|
Equals, true)
|
|
|
|
l, err := os.ReadDir(seedsnapsdir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 4)
|
|
|
|
l, err = os.ReadDir(extraSnapsDir)
|
|
c.Assert(err, IsNil)
|
|
c.Check(l, HasLen, 3)
|
|
|
|
// check boot config
|
|
grubCfg := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grub.cfg")
|
|
seedGrubenv := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grubenv")
|
|
grubRecoveryCfgAsset := assets.Internal("grub-recovery.cfg")
|
|
c.Assert(grubRecoveryCfgAsset, NotNil)
|
|
c.Check(grubCfg, testutil.FileEquals, string(grubRecoveryCfgAsset))
|
|
// make sure that grub.cfg and grubenv are the only files present inside
|
|
// the directory
|
|
gl, err := filepath.Glob(filepath.Join(prepareDir, "system-seed/EFI/ubuntu/*"))
|
|
c.Assert(err, IsNil)
|
|
c.Check(gl, DeepEquals, []string{
|
|
grubCfg,
|
|
seedGrubenv,
|
|
})
|
|
|
|
// check recovery system specific config
|
|
systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*"))
|
|
c.Assert(err, IsNil)
|
|
c.Assert(systems, HasLen, 1)
|
|
|
|
seedGenv := grubenv.NewEnv(seedGrubenv)
|
|
c.Assert(seedGenv.Load(), IsNil)
|
|
c.Check(seedGenv.Get("snapd_recovery_system"), Equals, filepath.Base(systems[0]))
|
|
c.Check(seedGenv.Get("snapd_recovery_mode"), Equals, "install")
|
|
c.Check(seedGenv.Get("snapd_boot_flags"), Equals, "factory")
|
|
|
|
systemGenv := grubenv.NewEnv(filepath.Join(systems[0], "grubenv"))
|
|
c.Assert(systemGenv.Load(), IsNil)
|
|
c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_1.snap")
|
|
|
|
// check the downloads
|
|
c.Check(s.storeActionsBunchSizes, DeepEquals, []int{4})
|
|
c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "snapd",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc-kernel",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "core20",
|
|
Channel: stableChannel,
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
|
|
Action: "download",
|
|
InstanceName: "pc",
|
|
Channel: "20",
|
|
Flags: store.SnapActionIgnoreValidation,
|
|
})
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalComponentsNoLocalSnap(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(map[string]interface{}{"grade": "dangerous"})
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
comp1File := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp1"])
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
Components: []string{comp1File},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, "missing local snaps:\n.* local component does not have a matching local snap.*")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalComponentNotDefinedBySnap(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(map[string]interface{}{"grade": "dangerous"})
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, seedtest.SampleSnapYaml["required20"], nil)
|
|
comp1File := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+unknown"])
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
Snaps: []string{snapFile},
|
|
Components: []string{comp1File},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, "component unknown is not defined by snap required20")
|
|
}
|
|
|
|
func (s *imageSuite) TestSetupSeedLocalComponentBadType(c *C) {
|
|
bootloader.Force(nil)
|
|
restore := image.MockTrusted(s.StoreSigning.Trusted)
|
|
defer restore()
|
|
|
|
// a model that uses core20
|
|
model := s.makeUC20Model(map[string]interface{}{"grade": "dangerous"})
|
|
|
|
prepareDir := c.MkDir()
|
|
|
|
snapFile := snaptest.MakeTestSnapWithFiles(c, seedtest.SampleSnapYaml["required20"], nil)
|
|
comp1File := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp1_kernel"])
|
|
|
|
opts := &image.Options{
|
|
PrepareDir: prepareDir,
|
|
Customizations: image.Customizations{
|
|
BootFlags: []string{"factory"},
|
|
Validation: "ignore",
|
|
},
|
|
Snaps: []string{snapFile},
|
|
Components: []string{comp1File},
|
|
}
|
|
|
|
err := image.SetupSeed(s.tsto, model, opts)
|
|
c.Assert(err, ErrorMatches, "component comp1 has type kernel-modules while snap required20 defines type test for it")
|
|
}
|