summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2018-04-10 09:15:58 +0200
committerMichael Vogt <mvo@ubuntu.com>2018-04-10 09:15:58 +0200
commitac83fbddce5c22391e5f11ce519e1bcd2ecad0f7 (patch)
tree3aedf1c744d3b941cd2a4d2f76c60478b7136a09
parent2940f1c4d33a0abb5d1cf4770b8f23ce34859b55 (diff)
parentc25b7318ad3a60fdcfe1b18d1f245ebf0161503c (diff)
Merge remote-tracking branch 'upstream/master' into whoopsie-dupsiewhoopsie-dupsie
-rw-r--r--advisor/backend.go65
-rw-r--r--advisor/cmdfinder.go1
-rw-r--r--advisor/cmdfinder_test.go20
-rw-r--r--advisor/pkgfinder.go5
-rw-r--r--advisor/pkgfinder_test.go40
-rw-r--r--cmd/snap-confine/snap-confine.apparmor.in4
-rwxr-xr-xcmd/snap-confine/snap-device-helper7
-rw-r--r--cmd/snap-mgmt/snap-mgmt.sh.in9
-rw-r--r--cmd/snap/cmd_snap_op.go2
-rw-r--r--daemon/api.go8
-rw-r--r--daemon/api_test.go31
-rw-r--r--daemon/daemon.go10
-rw-r--r--data/selinux/snappy.fc5
-rw-r--r--data/selinux/snappy.te42
-rw-r--r--errtracker/errtracker.go137
-rw-r--r--errtracker/errtracker_test.go172
-rw-r--r--errtracker/export_test.go49
-rw-r--r--interfaces/apparmor/template.go1
-rw-r--r--interfaces/builtin/firewall_control.go3
-rw-r--r--interfaces/builtin/fuse_support.go8
-rw-r--r--interfaces/builtin/hostname_control.go89
-rw-r--r--interfaces/builtin/hostname_control_test.go111
-rw-r--r--interfaces/builtin/process_control.go15
-rw-r--r--interfaces/repo.go3
-rw-r--r--interfaces/system_key.go2
-rw-r--r--osutil/mountentry.go9
-rw-r--r--osutil/mountentry_test.go12
-rw-r--r--osutil/mountprofile.go11
-rw-r--r--osutil/mountprofile_test.go20
-rw-r--r--overlord/devicestate/devicestate_test.go11
-rw-r--r--overlord/devicestate/handlers.go3
-rw-r--r--overlord/hookstate/ctlcmd/services_test.go22
-rw-r--r--overlord/hookstate/hookmgr.go34
-rw-r--r--overlord/hookstate/hookstate_test.go78
-rw-r--r--overlord/ifacestate/export_test.go3
-rw-r--r--overlord/ifacestate/handlers.go21
-rw-r--r--overlord/ifacestate/helpers.go65
-rw-r--r--overlord/ifacestate/ifacestate_test.go183
-rw-r--r--overlord/managers_test.go95
-rw-r--r--overlord/overlord_test.go3
-rw-r--r--overlord/snapstate/autorefresh_test.go18
-rw-r--r--overlord/snapstate/backend.go6
-rw-r--r--overlord/snapstate/backend_test.go218
-rw-r--r--overlord/snapstate/catalogrefresh_test.go14
-rw-r--r--overlord/snapstate/export_test.go9
-rw-r--r--overlord/snapstate/handlers.go77
-rw-r--r--overlord/snapstate/handlers_download_test.go12
-rw-r--r--overlord/snapstate/handlers_link_test.go72
-rw-r--r--overlord/snapstate/handlers_prereq_test.go34
-rw-r--r--overlord/snapstate/refreshhints_test.go18
-rw-r--r--overlord/snapstate/snapmgr.go15
-rw-r--r--overlord/snapstate/snapstate.go19
-rw-r--r--overlord/snapstate/snapstate_test.go694
-rw-r--r--overlord/snapstate/storehelpers.go290
-rw-r--r--packaging/arch/PKGBUILD2
-rw-r--r--packaging/fedora/snapd.spec17
-rw-r--r--packaging/opensuse-42.2/snapd.changes5
-rw-r--r--packaging/opensuse-42.2/snapd.spec2
-rw-r--r--packaging/ubuntu-14.04/changelog18
-rw-r--r--packaging/ubuntu-14.04/snapd.postrm9
-rw-r--r--packaging/ubuntu-16.04/changelog18
-rw-r--r--packaging/ubuntu-16.04/gbp.conf7
-rw-r--r--packaging/ubuntu-16.04/snapd.postrm9
-rwxr-xr-xrelease-tools/repack-debian-tarball.sh87
-rw-r--r--store/errors.go3
-rw-r--r--store/store.go5
-rw-r--r--store/store_test.go18
-rw-r--r--store/storetest/storetest.go6
-rw-r--r--tests/lib/fakestore/store/store.go161
-rw-r--r--tests/lib/fakestore/store/store_test.go186
-rw-r--r--tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml3
-rw-r--r--tests/main/interfaces-opengl-nvidia/task.yaml35
-rw-r--r--tests/main/snap-mgmt/task.yaml18
-rw-r--r--tests/main/snap-system-key/task.yaml14
74 files changed, 3094 insertions, 434 deletions
diff --git a/advisor/backend.go b/advisor/backend.go
index bde83db353..917bc99ea0 100644
--- a/advisor/backend.go
+++ b/advisor/backend.go
@@ -20,8 +20,8 @@
package advisor
import (
+ "encoding/json"
"os"
- "strings"
"time"
"github.com/snapcore/bolt"
@@ -45,7 +45,7 @@ type writer struct {
type CommandDB interface {
// AddSnap adds the entries for commands pointing to the given
// snap name to the commands database.
- AddSnap(snapName, summary string, commands []string) error
+ AddSnap(snapName, version, summary string, commands []string) error
// Commit persist the changes, and closes the database. If the
// database has already been committed/rollbacked, does nothing.
Commit() error
@@ -98,23 +98,38 @@ func Create() (CommandDB, error) {
return t, nil
}
-func (t *writer) AddSnap(snapName, summary string, commands []string) error {
- bname := []byte(snapName)
-
+func (t *writer) AddSnap(snapName, version, summary string, commands []string) error {
for _, cmd := range commands {
+ var sil []Package
+
bcmd := []byte(cmd)
row := t.cmdBucket.Get(bcmd)
- if row == nil {
- row = bname
- } else {
- row = append(append(row, ','), bname...)
+ if row != nil {
+ if err := json.Unmarshal(row, &sil); err != nil {
+ return err
+ }
+ }
+ // For the mapping of command->snap we do not need the summary, nothing is using that.
+ sil = append(sil, Package{Snap: snapName, Version: version})
+ row, err := json.Marshal(sil)
+ if err != nil {
+ return err
}
if err := t.cmdBucket.Put(bcmd, row); err != nil {
return err
}
}
- if err := t.pkgBucket.Put([]byte(snapName), []byte(summary)); err != nil {
+ // TODO: use json here as well and put the version information here
+ bj, err := json.Marshal(Package{
+ Snap: snapName,
+ Version: version,
+ Summary: summary,
+ })
+ if err != nil {
+ return err
+ }
+ if err := t.pkgBucket.Put([]byte(snapName), bj); err != nil {
return err
}
@@ -154,7 +169,7 @@ func (t *writer) done(commit bool) error {
// DumpCommands returns the whole database as a map. For use in
// testing and debugging.
-func DumpCommands() (map[string][]string, error) {
+func DumpCommands() (map[string]string, error) {
db, err := bolt.Open(dirs.SnapCommandsDB, 0644, &bolt.Options{
ReadOnly: true,
Timeout: 1 * time.Second,
@@ -175,10 +190,10 @@ func DumpCommands() (map[string][]string, error) {
return nil, nil
}
- m := map[string][]string{}
+ m := map[string]string{}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
- m[string(k)] = strings.Split(string(v), ",")
+ m[string(k)] = string(v)
}
return m, nil
@@ -224,12 +239,15 @@ func (f *boltFinder) FindCommand(command string) ([]Command, error) {
if buf == nil {
return nil, nil
}
-
- snaps := strings.Split(string(buf), ",")
- cmds := make([]Command, len(snaps))
- for i, snap := range snaps {
+ var sil []Package
+ if err := json.Unmarshal(buf, &sil); err != nil {
+ return nil, err
+ }
+ cmds := make([]Command, len(sil))
+ for i, si := range sil {
cmds[i] = Command{
- Snap: snap,
+ Snap: si.Snap,
+ Version: si.Version,
Command: command,
}
}
@@ -249,10 +267,15 @@ func (f *boltFinder) FindPackage(pkgName string) (*Package, error) {
return nil, nil
}
- bsummary := b.Get([]byte(pkgName))
- if bsummary == nil {
+ bj := b.Get([]byte(pkgName))
+ if bj == nil {
return nil, nil
}
+ var si Package
+ err = json.Unmarshal(bj, &si)
+ if err != nil {
+ return nil, err
+ }
- return &Package{Snap: pkgName, Summary: string(bsummary)}, nil
+ return &Package{Snap: pkgName, Version: si.Version, Summary: si.Summary}, nil
}
diff --git a/advisor/cmdfinder.go b/advisor/cmdfinder.go
index 1c103e5e64..7cfadb5c68 100644
--- a/advisor/cmdfinder.go
+++ b/advisor/cmdfinder.go
@@ -25,6 +25,7 @@ import (
type Command struct {
Snap string
+ Version string `json:"Version,omitempty"`
Command string
}
diff --git a/advisor/cmdfinder_test.go b/advisor/cmdfinder_test.go
index 6a30849eff..5a83e5409c 100644
--- a/advisor/cmdfinder_test.go
+++ b/advisor/cmdfinder_test.go
@@ -44,8 +44,8 @@ func (s *cmdfinderSuite) SetUpTest(c *C) {
db, err := advisor.Create()
c.Assert(err, IsNil)
- c.Assert(db.AddSnap("foo", "foo summary", []string{"foo", "meh"}), IsNil)
- c.Assert(db.AddSnap("bar", "bar summary", []string{"bar", "meh"}), IsNil)
+ c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "meh"}), IsNil)
+ c.Assert(db.AddSnap("bar", "2.0", "bar summary", []string{"bar", "meh"}), IsNil)
c.Assert(db.Commit(), IsNil)
}
@@ -99,8 +99,8 @@ func (s *cmdfinderSuite) TestFindCommandHit(c *C) {
cmds, err := advisor.FindCommand("meh")
c.Assert(err, IsNil)
c.Check(cmds, DeepEquals, []advisor.Command{
- {Snap: "foo", Command: "meh"},
- {Snap: "bar", Command: "meh"},
+ {Snap: "foo", Version: "1.0", Command: "meh"},
+ {Snap: "bar", Version: "2.0", Command: "meh"},
})
}
@@ -114,8 +114,8 @@ func (s *cmdfinderSuite) TestFindMisspelledCommandHit(c *C) {
cmds, err := advisor.FindMisspelledCommand("moh")
c.Assert(err, IsNil)
c.Check(cmds, DeepEquals, []advisor.Command{
- {Snap: "foo", Command: "meh"},
- {Snap: "bar", Command: "meh"},
+ {Snap: "foo", Version: "1.0", Command: "meh"},
+ {Snap: "bar", Version: "2.0", Command: "meh"},
})
}
@@ -128,10 +128,10 @@ func (s *cmdfinderSuite) TestFindMisspelledCommandMiss(c *C) {
func (s *cmdfinderSuite) TestDumpCommands(c *C) {
cmds, err := advisor.DumpCommands()
c.Assert(err, IsNil)
- c.Check(cmds, DeepEquals, map[string][]string{
- "foo": {"foo"},
- "bar": {"bar"},
- "meh": {"foo", "bar"},
+ c.Check(cmds, DeepEquals, map[string]string{
+ "foo": `[{"snap":"foo","version":"1.0"}]`,
+ "bar": `[{"snap":"bar","version":"2.0"}]`,
+ "meh": `[{"snap":"foo","version":"1.0"},{"snap":"bar","version":"2.0"}]`,
})
}
diff --git a/advisor/pkgfinder.go b/advisor/pkgfinder.go
index a210d2c145..bae4f820e5 100644
--- a/advisor/pkgfinder.go
+++ b/advisor/pkgfinder.go
@@ -24,8 +24,9 @@ import (
)
type Package struct {
- Snap string
- Summary string
+ Snap string `json:"snap"`
+ Version string `json:"version"`
+ Summary string `json:"summary,omitempty"`
}
func FindPackage(pkgName string) (*Package, error) {
diff --git a/advisor/pkgfinder_test.go b/advisor/pkgfinder_test.go
new file mode 100644
index 0000000000..fd08017bf8
--- /dev/null
+++ b/advisor/pkgfinder_test.go
@@ -0,0 +1,40 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 advisor_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/advisor"
+)
+
+func (s *cmdfinderSuite) TestFindPackageHit(c *C) {
+ pkg, err := advisor.FindPackage("foo")
+ c.Assert(err, IsNil)
+ c.Check(pkg, DeepEquals, &advisor.Package{
+ Snap: "foo", Version: "1.0", Summary: "foo summary",
+ })
+}
+
+func (s *cmdfinderSuite) TestFindPackageMiss(c *C) {
+ pkg, err := advisor.FindPackage("moh")
+ c.Assert(err, IsNil)
+ c.Check(pkg, IsNil)
+}
diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in
index ed00d74444..8185f3bd7f 100644
--- a/cmd/snap-confine/snap-confine.apparmor.in
+++ b/cmd/snap-confine/snap-confine.apparmor.in
@@ -259,12 +259,12 @@
/usr/** r,
mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/,
mount options=(rw bind) /usr/lib{,32}/nvidia-*/ -> /{tmp/snap.rootfs_*/,}var/lib/snapd/lib/gl{,32}/,
- /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/* w,
+ /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/{,*} w,
mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/,
mount options=(remount ro) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/gl{,32}/,
# Vulkan support
- /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/* w,
+ /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/{,*} w,
mount fstype=tmpfs options=(rw nodev noexec) none -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/,
mount options=(remount ro) -> /tmp/snap.rootfs_*/var/lib/snapd/lib/vulkan/,
diff --git a/cmd/snap-confine/snap-device-helper b/cmd/snap-confine/snap-device-helper
index bc779179f7..5c06400e59 100755
--- a/cmd/snap-confine/snap-device-helper
+++ b/cmd/snap-confine/snap-device-helper
@@ -18,6 +18,13 @@ MAJMIN="$4"
APPNAME="$( echo "$APPNAME" | tr '_' '.' )"
app_dev_cgroup="/sys/fs/cgroup/devices/$APPNAME"
+# The cgroup is only present after snap start so ignore any cgroup changes
+# (eg, 'add' on boot, hotplug, hotunplug) when the cgroup doesn't exist
+# yet. LP: #1762182.
+if [ ! -e "$app_dev_cgroup" ]; then
+ exit 0
+fi
+
# check if it's a block or char dev
if [ "${DEVPATH#*/block/}" != "$DEVPATH" ]; then
type="b"
diff --git a/cmd/snap-mgmt/snap-mgmt.sh.in b/cmd/snap-mgmt/snap-mgmt.sh.in
index 360fb4bd6e..5e6f502498 100644
--- a/cmd/snap-mgmt/snap-mgmt.sh.in
+++ b/cmd/snap-mgmt/snap-mgmt.sh.in
@@ -97,6 +97,15 @@ purge() {
rmdir --ignore-fail-on-non-empty "$d"
fi
done
+ # udev rules
+ find /etc/udev/rules.d -name "*-snap.${snap}.rules" -execdir rm -f "{}" \;
+ # dbus policy files
+ find /etc/dbus-1/system.d -name "snap.${snap}.*.conf" -execdir rm -f "{}" \;
+ # timer files
+ find /etc/systemd/system -name "snap.${snap}.*.timer" | while read -r f; do
+ systemctl_stop "$(basename $f)"
+ rm -f "$f"
+ done
fi
fi
diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go
index d7299efbcb..e9a08032b4 100644
--- a/cmd/snap/cmd_snap_op.go
+++ b/cmd/snap/cmd_snap_op.go
@@ -284,7 +284,7 @@ func showDone(names []string, op string) error {
default:
fmt.Fprintf(Stdout, "internal error: unknown op %q", op)
}
- if snap.TrackingChannel != snap.Channel {
+ if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
// TRANSLATORS: first %s is a snap name, following %s is a channel name
fmt.Fprintf(Stdout, i18n.G("Snap %s is no longer tracking %s.\n"), snap.Name, snap.TrackingChannel)
}
diff --git a/daemon/api.go b/daemon/api.go
index 47c90911c1..e123a8ee9f 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -1176,7 +1176,13 @@ func (inst *snapInstruction) errToResponse(err error) Response {
var snapName string
switch err {
- case store.ErrSnapNotFound:
+ case store.ErrSnapNotFound, store.ErrRevisionNotAvailable:
+ // TODO: treating ErrRevisionNotAvailable the same as
+ // ErrSnapNotFound preserves the old error handling
+ // behavior of the REST API and the snap command. We
+ // should revisit this once the store returns more
+ // precise errors and makes it possible to distinguish
+ // the why a revision wasn't available.
switch len(inst.Snaps) {
case 1:
return SnapNotFound(inst.Snaps[0], err)
diff --git a/daemon/api_test.go b/daemon/api_test.go
index e95c7c5706..88e7105fdc 100644
--- a/daemon/api_test.go
+++ b/daemon/api_test.go
@@ -86,7 +86,8 @@ type apiBaseSuite struct {
d *Daemon
user *auth.UserState
restoreBackends func()
- refreshCandidates []*store.RefreshCandidate
+ currentSnaps []*store.CurrentSnap
+ actions []*store.SnapAction
buyOptions *store.BuyOptions
buyResult *store.BuyResult
storeSigning *assertstest.StoreStack
@@ -126,18 +127,12 @@ func (s *apiBaseSuite) Find(search *store.Search, user *auth.UserState) ([]*snap
return s.rsnaps, s.err
}
-func (s *apiBaseSuite) LookupRefresh(snap *store.RefreshCandidate, user *auth.UserState) (*snap.Info, error) {
- s.refreshCandidates = []*store.RefreshCandidate{snap}
- s.user = user
-
- return s.rsnaps[0], s.err
-}
-
-func (s *apiBaseSuite) ListRefresh(ctx context.Context, snaps []*store.RefreshCandidate, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, error) {
+func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
if ctx == nil {
panic("context required")
}
- s.refreshCandidates = snaps
+ s.currentSnaps = currentSnaps
+ s.actions = actions
s.user = user
return s.rsnaps, s.err
@@ -232,7 +227,8 @@ func (s *apiBaseSuite) SetUpTest(c *check.C) {
s.vars = nil
s.user = nil
s.d = nil
- s.refreshCandidates = nil
+ s.currentSnaps = nil
+ s.actions = nil
// Disable real security backends for all API tests
s.restoreBackends = ifacestate.MockSecurityBackends(nil)
@@ -1510,7 +1506,8 @@ func (s *apiSuite) TestFind(c *check.C) {
c.Check(rsp.SuggestedCurrency, check.Equals, "EUR")
c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"})
- c.Check(s.refreshCandidates, check.HasLen, 0)
+ c.Check(s.currentSnaps, check.HasLen, 0)
+ c.Check(s.actions, check.HasLen, 0)
}
func (s *apiSuite) TestFindRefreshes(c *check.C) {
@@ -1533,7 +1530,8 @@ func (s *apiSuite) TestFindRefreshes(c *check.C) {
snaps := snapList(rsp.Result)
c.Assert(snaps, check.HasLen, 1)
c.Assert(snaps[0]["name"], check.Equals, "store")
- c.Check(s.refreshCandidates, check.HasLen, 1)
+ c.Check(s.currentSnaps, check.HasLen, 1)
+ c.Check(s.actions, check.HasLen, 1)
}
func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) {
@@ -1569,9 +1567,9 @@ func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) {
rsp := searchStore(findCmd, req, nil).(*resp)
snaps := snapList(rsp.Result)
- c.Assert(snaps, check.HasLen, 1)
- c.Assert(snaps[0]["name"], check.Equals, "store")
- c.Check(s.refreshCandidates, check.HasLen, 0)
+ c.Assert(snaps, check.HasLen, 0)
+ c.Check(s.currentSnaps, check.HasLen, 0)
+ c.Check(s.actions, check.HasLen, 0)
}
func (s *apiSuite) TestFindPrivate(c *check.C) {
@@ -6547,6 +6545,7 @@ func (s *appSuite) TestErrToResponseNoSnapsDoesNotPanic(c *check.C) {
si := &snapInstruction{Action: "frobble"}
errors := []error{
store.ErrSnapNotFound,
+ store.ErrRevisionNotAvailable,
store.ErrNoUpdateAvailable,
store.ErrLocalSnap,
&snap.AlreadyInstalledError{Snap: "foo"},
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 784cc162cf..beddb8793d 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -468,7 +468,17 @@ func (d *Daemon) Start() {
func (d *Daemon) Stop() error {
d.tomb.Kill(nil)
d.snapdListener.Close()
+
if d.snapListener != nil {
+ // stop running hooks first
+ // and do it more gracefully if we are restarting
+ hookMgr := d.overlord.HookManager()
+ if d.overlord.State().Restarting() {
+ logger.Noticef("gracefully waiting for running hooks")
+ hookMgr.GracefullyWaitRunningHooks()
+ logger.Noticef("done waiting for running hooks")
+ }
+ hookMgr.Stop()
d.snapListener.Close()
}
diff --git a/data/selinux/snappy.fc b/data/selinux/snappy.fc
index b928aed2e3..e56a4dfb60 100644
--- a/data/selinux/snappy.fc
+++ b/data/selinux/snappy.fc
@@ -35,7 +35,12 @@ ifdef(`distro_debian',`
/lib/systemd/system/snapd.* -- gen_context(system_u:object_r:snappy_unit_file_t,s0)
')
+/var/run/snapd(/.*)? -- gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/run/snapd\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/run/snapd-snap\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
/var/lib/snapd(/.*)? gen_context(system_u:object_r:snappy_var_lib_t,s0)
/var/snap(/.*)? gen_context(system_u:object_r:snappy_var_t,s0)
+
+/run/snapd(/.*)? -- gen_context(system_u:object_r:snappy_var_run_t,s0)
+/run/snapd\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
+/run/snapd-snap\.socket -s gen_context(system_u:object_r:snappy_var_run_t,s0)
diff --git a/data/selinux/snappy.te b/data/selinux/snappy.te
index cd2f0fccce..59b6c47a7d 100644
--- a/data/selinux/snappy.te
+++ b/data/selinux/snappy.te
@@ -17,7 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-policy_module(snappy,0.0.13)
+policy_module(snappy,0.0.14)
########################################
#
@@ -88,9 +88,9 @@ allow snappy_t sysfs_t:lnk_file read;
# Allow snapd to read SSL cert store
gen_require(` type cert_t; ')
-allow snappy_t cert_t:dir search;
+allow snappy_t cert_t:dir { search open read };
allow snappy_t cert_t:file { getattr open read };
-allow snappy_t cert_t:lnk_file read;
+allow snappy_t cert_t:lnk_file { getattr open read };
# Allow snapd to read config files
read_files_pattern(snappy_t, snappy_config_t, snappy_config_t)
@@ -135,10 +135,10 @@ allow snappy_t udev_var_run_t:file { getattr open read };
allow snappy_t udev_var_run_t:sock_file { getattr open read write };
# Allow snapd to read/write systemd units and use systemctl for managing snaps
-gen_require(` type systemd_unit_file_t; type systemd_systemctl_exec_t; ')
-allow snappy_t systemd_unit_file_t:dir { search open read write add_name remove_name };
-allow snappy_t systemd_unit_file_t:file { getattr open read write create rename unlink };
-allow snappy_t systemd_systemctl_exec_t:file { execute execute_no_trans getattr open read };
+systemd_config_all_services(snappy_t)
+systemd_manage_all_unit_files(snappy_t)
+systemd_manage_all_unit_lnk_files(snappy_t)
+systemd_exec_systemctl(snappy_t)
# Allow snapd to mount snaps
gen_require(` type mount_exec_t; ')
@@ -177,19 +177,34 @@ gen_require(` type tmp_t; ')
allow snappy_t tmp_t:dir { getattr setattr add_name create read remove_name rmdir write };
allow snappy_t tmp_t:file { getattr setattr create open unlink write };
+# Allow snapd to use ssh-keygen
+gen_require(` type ssh_keygen_exec_t; ')
+allow snappy_t ssh_keygen_exec_t:file { execute execute_no_trans getattr open read };
+
+# Allow snapd to access passwd file for lookup
+auth_read_passwd(snappy_t);
+
# Until we can figure out how to apply the label to mounted snaps,
# we need to grant snapd access to "unlabeled files"
gen_require(` type unlabeled_t; ')
allow snappy_t unlabeled_t:dir { getattr search open read };
allow snappy_t unlabeled_t:file { getattr open read };
+# Until we can figure out why some things are randomly getting unconfined_t,
+# we need to grant access to "unconfined" files
+gen_require(` type unconfined_t; ')
+allow snappy_t unconfined_t:dir { getattr search open read };
+allow snappy_t unconfined_t:file { getattr open read };
+
logging_send_syslog_msg(snappy_t);
-allow snappy_t self:capability { sys_admin dac_override chown kill fowner fsetid mknod net_admin net_bind_service net_raw setfcap };
+allow snappy_t self:capability { sys_admin sys_chroot dac_override dac_read_search chown kill fowner fsetid mknod net_admin net_bind_service net_raw setfcap };
allow snappy_t self:tun_socket relabelto;
allow snappy_t self:process { getcap signal_perms setrlimit setfscreate };
+# Various socket permissions
allow snappy_t self:fifo_file rw_fifo_file_perms;
+allow snappy_t self:netlink_route_socket create_netlink_socket_perms;
allow snappy_t self:unix_stream_socket create_stream_socket_perms;
allow snappy_t self:tcp_socket create_stream_socket_perms;
allow snappy_t self:udp_socket create_stream_socket_perms;
@@ -220,3 +235,14 @@ corenet_sendrecv_dns_client_packets(snappy_t)
optional_policy(`
policykit_dbus_chat(snappy_t)
')
+
+# allow communication with system bus
+optional_policy(`
+ dbus_system_bus_client(snappy_t)
+')
+
+# allow reading sssd files
+optional_policy(`
+ sssd_read_public_files(snappy_t)
+ sssd_stream_connect(snappy_t)
+')
diff --git a/errtracker/errtracker.go b/errtracker/errtracker.go
index cd20b4158a..168f4b525a 100644
--- a/errtracker/errtracker.go
+++ b/errtracker/errtracker.go
@@ -58,7 +58,13 @@ var (
snapConfineProfile = "/etc/apparmor.d/usr.lib.snapd.snap-confine"
- timeNow = time.Now
+ procCpuinfo = "/proc/cpuinfo"
+ procSelfExe = "/proc/self/exe"
+ procSelfCwd = "/proc/self/cwd"
+ procSelfCmdline = "/proc/self/cmdline"
+
+ osGetenv = os.Getenv
+ timeNow = time.Now
)
func whoopsieEnabled() bool {
@@ -109,7 +115,7 @@ func snapConfineProfileDigest(suffix string) string {
var didSnapdReExec = func() string {
// TODO: move this into osutil.Reexeced() ?
- exe, err := os.Readlink("/proc/self/exe")
+ exe, err := os.Readlink(procSelfExe)
if err != nil {
return "unknown"
}
@@ -151,6 +157,117 @@ func detectVirt() string {
return strings.TrimSpace(string(output))
}
+func journalError() string {
+ // TODO: look into using systemd package (needs refactor)
+
+ // Before changing this line to be more consistent or nicer or anything
+ // else, remember it needs to run a lot of different systemd's: today,
+ // anything from 238 (on arch) to 204 (on ubuntu 14.04); this is why
+ // doing the refactor to the systemd package to only worry about this in
+ // there might be worth it.
+ output, err := exec.Command("journalctl", "-b", "--priority=warning..err", "--lines=1000").CombinedOutput()
+ if err != nil {
+ if len(output) == 0 {
+ return fmt.Sprintf("error: %v", err)
+ }
+ output = append(output, fmt.Sprintf("\nerror: %v", err)...)
+ }
+ return string(output)
+}
+
+func procCpuinfoMinimal() string {
+ buf, err := ioutil.ReadFile(procCpuinfo)
+ if err != nil {
+ // if we can't read cpuinfo, we want to know _why_
+ return fmt.Sprintf("error: %v", err)
+ }
+ idx := bytes.LastIndex(buf, []byte("\nprocessor\t:"))
+
+ // if not found (which will happen on non-x86 architectures, which is ok
+ // because they'd typically not have the same info over and over again),
+ // return whole buffer; otherwise, return from just after the \n
+ return string(buf[idx+1:])
+}
+
+func procExe() string {
+ out, err := os.Readlink(procSelfExe)
+ if err != nil {
+ return fmt.Sprintf("error: %v", err)
+ }
+ return out
+}
+
+func procCwd() string {
+ out, err := os.Readlink(procSelfCwd)
+ if err != nil {
+ return fmt.Sprintf("error: %v", err)
+ }
+ return out
+}
+
+func procCmdline() string {
+ out, err := ioutil.ReadFile(procSelfCmdline)
+ if err != nil {
+ return fmt.Sprintf("error: %v", err)
+ }
+ return string(out)
+}
+
+func environ() string {
+ safeVars := []string{
+ "SHELL", "TERM", "LANGUAGE", "LANG", "LC_CTYPE",
+ "LC_COLLATE", "LC_TIME", "LC_NUMERIC",
+ "LC_MONETARY", "LC_MESSAGES", "LC_PAPER",
+ "LC_NAME", "LC_ADDRESS", "LC_TELEPHONE",
+ "LC_MEASUREMENT", "LC_IDENTIFICATION", "LOCPATH",
+ }
+ unsafeVars := []string{"XDG_RUNTIME_DIR", "LD_PRELOAD", "LD_LIBRARY_PATH"}
+ knownPaths := map[string]bool{
+ "/snap/bin": true,
+ "/var/lib/snapd/snap/bin": true,
+ "/sbin": true,
+ "/bin": true,
+ "/usr/sbin": true,
+ "/usr/bin": true,
+ "/usr/local/sbin": true,
+ "/usr/local/bin": true,
+ "/usr/local/games": true,
+ "/usr/games": true,
+ }
+
+ // + 1 for PATH
+ out := make([]string, 0, len(safeVars)+len(unsafeVars)+1)
+
+ for _, k := range safeVars {
+ if v := osGetenv(k); v != "" {
+ out = append(out, fmt.Sprintf("%s=%s", k, v))
+ }
+ }
+
+ for _, k := range unsafeVars {
+ if v := osGetenv(k); v != "" {
+ out = append(out, k+"=<set>")
+ }
+ }
+
+ if paths := filepath.SplitList(osGetenv("PATH")); len(paths) > 0 {
+ for i, p := range paths {
+ p = filepath.Clean(p)
+ if !knownPaths[p] {
+ if strings.Contains(p, "/home") || strings.Contains(p, "/tmp") {
+ p = "(user)"
+ } else {
+ p = "(custom)"
+ }
+ }
+ paths[i] = p
+ }
+ out = append(out, fmt.Sprintf("PATH=%s", strings.Join(paths, string(filepath.ListSeparator))))
+ }
+
+ return strings.Join(out, "\n")
+}
+
func report(errMsg, dupSig string, extra map[string]string) (string, error) {
if CrashDbURLBase == "" {
return "", nil
@@ -188,7 +305,6 @@ func report(errMsg, dupSig string, extra map[string]string) (string, error) {
if coreBuildID == "" {
coreBuildID = "unknown"
}
- detectedVirt := detectVirt()
report := map[string]string{
"Architecture": arch.UbuntuArchitecture(),
@@ -201,15 +317,28 @@ func report(errMsg, dupSig string, extra map[string]string) (string, error) {
"ErrorMessage": errMsg,
"DuplicateSignature": dupSig,
+ "JournalError": journalError(),
+ "ExecutablePath": procExe(),
+ "ProcCmdline": procCmdline(),
+ "ProcCpuinfoMinimal": procCpuinfoMinimal(),
+ "ProcCwd": procCwd(),
+ "ProcEnviron": environ(),
+ "DetectedVirt": detectVirt(),
+ "SourcePackage": "snapd",
+
"DidSnapdReExec": didSnapdReExec(),
}
+
+ if desktop := osGetenv("XDG_CURRENT_DESKTOP"); desktop != "" {
+ report["CurrentDesktop"] = desktop
+ }
+
for k, v := range extra {
// only set if empty
if _, ok := report[k]; !ok {
report[k] = v
}
}
- report["DetectedVirt"] = detectedVirt
// include md5 hashes of the apparmor conffile for easier debbuging
// of not-updated snap-confine apparmor profiles
diff --git a/errtracker/errtracker_test.go b/errtracker/errtracker_test.go
index 14b2f9ba21..9cd38eb545 100644
--- a/errtracker/errtracker_test.go
+++ b/errtracker/errtracker_test.go
@@ -27,6 +27,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
+ "sort"
"strings"
"testing"
"time"
@@ -61,6 +62,8 @@ var _ = Suite(&ErrtrackerTestSuite{})
var truePath = osutil.LookPathDefault("true", "/bin/true")
var falsePath = osutil.LookPathDefault("false", "/bin/false")
+const someJournalEntry = "Mar 29 22:08:00 localhost kernel: [81B blob data]"
+
func (s *ErrtrackerTestSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
@@ -88,6 +91,38 @@ func (s *ErrtrackerTestSuite) SetUpTest(c *C) {
} else {
s.distroRelease = fmt.Sprintf("%s %s", release.ReleaseInfo.ID, release.ReleaseInfo.VersionID)
}
+
+ mockCpuinfo := filepath.Join(s.tmpdir, "cpuinfo")
+ mockSelfCmdline := filepath.Join(s.tmpdir, "self.cmdline")
+ mockSelfExe := filepath.Join(s.tmpdir, "self.exe")
+ mockSelfCwd := filepath.Join(s.tmpdir, "self.cwd")
+
+ c.Assert(ioutil.WriteFile(mockCpuinfo, []byte(`
+processor : 0
+bugs : very yes
+etc : ...
+
+processor : 42
+bugs : very yes
+`[1:]), 0644), IsNil)
+ c.Assert(ioutil.WriteFile(mockSelfCmdline, []byte("foo\x00bar\x00baz"), 0644), IsNil)
+ c.Assert(os.Symlink("target of /proc/self/exe", mockSelfExe), IsNil)
+ c.Assert(os.Symlink("target of /proc/self/cwd", mockSelfCwd), IsNil)
+
+ s.AddCleanup(errtracker.MockOsGetenv(func(s string) string {
+ switch s {
+ case "SHELL":
+ return "/bin/sh"
+ case "XDG_CURRENT_DESKTOP":
+ return "Unity"
+ }
+ return ""
+ }))
+ s.AddCleanup(errtracker.MockProcCpuinfo(mockCpuinfo))
+ s.AddCleanup(errtracker.MockProcSelfCmdline(mockSelfCmdline))
+ s.AddCleanup(errtracker.MockProcSelfExe(mockSelfExe))
+ s.AddCleanup(errtracker.MockProcSelfCwd(mockSelfCwd))
+ s.AddCleanup(testutil.MockCommand(c, "journalctl", "echo "+someJournalEntry).Restore)
}
func (s *ErrtrackerTestSuite) TestReport(c *C) {
@@ -135,10 +170,19 @@ func (s *ErrtrackerTestSuite) TestReport(c *C) {
"Architecture": arch.UbuntuArchitecture(),
"DidSnapdReExec": "yes",
- "ProblemType": "Snap",
- "Snap": "some-snap",
- "Channel": "beta",
- "DetectedVirt": "none",
+ "ProblemType": "Snap",
+ "Snap": "some-snap",
+ "Channel": "beta",
+
+ "ProcCpuinfoMinimal": "processor\t: 42\nbugs\t\t: very yes\n",
+ "ExecutablePath": "target of /proc/self/exe",
+ "ProcCwd": "target of /proc/self/cwd",
+ "ProcCmdline": "foo\x00bar\x00baz",
+ "ProcEnviron": "SHELL=/bin/sh",
+ "JournalError": someJournalEntry + "\n",
+ "SourcePackage": "snapd",
+ "CurrentDesktop": "Unity",
+ "DetectedVirt": "none",
"MD5SumSnapConfineAppArmorProfile": "7a7aa5f21063170c1991b84eb8d86de1",
"MD5SumSnapConfineAppArmorProfileDpkgNew": "93b885adfe0da089cdf634904fd59f71",
@@ -267,6 +311,15 @@ func (s *ErrtrackerTestSuite) TestReportRepair(c *C) {
"ErrorMessage": "failure in script",
"DuplicateSignature": "[dupSig]",
"BrandID": "canonical",
+
+ "ProcCpuinfoMinimal": "processor\t: 42\nbugs\t\t: very yes\n",
+ "ExecutablePath": "target of /proc/self/exe",
+ "ProcCwd": "target of /proc/self/cwd",
+ "ProcCmdline": "foo\x00bar\x00baz",
+ "ProcEnviron": "SHELL=/bin/sh",
+ "JournalError": someJournalEntry + "\n",
+ "SourcePackage": "snapd",
+ "CurrentDesktop": "Unity",
"DetectedVirt": "none",
})
fmt.Fprintf(w, "c14388aa-f78d-11e6-8df0-fa163eaf9b83 OOPSID")
@@ -328,3 +381,114 @@ func (s *ErrtrackerTestSuite) TestReportWithNoWhoopsieInstalled(c *C) {
c.Check(id, Equals, "1234-oopsid")
c.Check(n, Equals, 1)
}
+
+func (s *ErrtrackerTestSuite) TestProcCpuinfo(c *C) {
+ fn := filepath.Join(s.tmpdir, "cpuinfo")
+ // sanity check
+ buf, err := ioutil.ReadFile(fn)
+ c.Assert(err, IsNil)
+ c.Check(string(buf), Equals, `
+processor : 0
+bugs : very yes
+etc : ...
+
+processor : 42
+bugs : very yes
+`[1:])
+
+ // just the last processor entry
+ c.Check(errtracker.ProcCpuinfoMinimal(), Equals, `
+processor : 42
+bugs : very yes
+`[1:])
+
+ // if no processor line, just return the whole thing
+ c.Assert(ioutil.WriteFile(fn, []byte("yadda yadda\n"), 0644), IsNil)
+ c.Check(errtracker.ProcCpuinfoMinimal(), Equals, "yadda yadda\n")
+
+ c.Assert(os.Remove(fn), IsNil)
+ c.Check(errtracker.ProcCpuinfoMinimal(), Matches, "error: .* no such file or directory")
+}
+
+func (s *ErrtrackerTestSuite) TestProcExe(c *C) {
+ c.Check(errtracker.ProcExe(), Equals, "target of /proc/self/exe")
+ c.Assert(os.Remove(filepath.Join(s.tmpdir, "self.exe")), IsNil)
+ c.Check(errtracker.ProcExe(), Matches, "error: .* no such file or directory")
+}
+
+func (s *ErrtrackerTestSuite) TestProcCwd(c *C) {
+ c.Check(errtracker.ProcCwd(), Equals, "target of /proc/self/cwd")
+ c.Assert(os.Remove(filepath.Join(s.tmpdir, "self.cwd")), IsNil)
+ c.Check(errtracker.ProcCwd(), Matches, "error: .* no such file or directory")
+}
+
+func (s *ErrtrackerTestSuite) TestProcCmdline(c *C) {
+ c.Check(errtracker.ProcCmdline(), Equals, "foo\x00bar\x00baz")
+ c.Assert(os.Remove(filepath.Join(s.tmpdir, "self.cmdline")), IsNil)
+ c.Check(errtracker.ProcCmdline(), Matches, "error: .* no such file or directory")
+}
+
+func (s *ErrtrackerTestSuite) TestJournalError(c *C) {
+ jctl := testutil.MockCommand(c, "journalctl", "echo "+someJournalEntry)
+ defer jctl.Restore()
+ c.Check(errtracker.JournalError(), Equals, someJournalEntry+"\n")
+ c.Check(jctl.Calls(), DeepEquals, [][]string{
+ {"journalctl", "-b", "--priority=warning..err", "--lines=1000"},
+ })
+}
+
+func (s *ErrtrackerTestSuite) TestJournalErrorSilentError(c *C) {
+ jctl := testutil.MockCommand(c, "journalctl", "kill $$")
+ defer jctl.Restore()
+ c.Check(errtracker.JournalError(), Matches, "error: signal: [Tt]erminated")
+ c.Check(jctl.Calls(), DeepEquals, [][]string{
+ {"journalctl", "-b", "--priority=warning..err", "--lines=1000"},
+ })
+}
+
+func (s *ErrtrackerTestSuite) TestJournalErrorError(c *C) {
+ jctl := testutil.MockCommand(c, "journalctl", "echo OOPS; exit 1")
+ defer jctl.Restore()
+ c.Check(errtracker.JournalError(), Equals, "OOPS\n\nerror: exit status 1")
+ c.Check(jctl.Calls(), DeepEquals, [][]string{
+ {"journalctl", "-b", "--priority=warning..err", "--lines=1000"},
+ })
+}
+
+func (s *ErrtrackerTestSuite) TestEnviron(c *C) {
+ defer errtracker.MockOsGetenv(func(s string) string {
+ switch s {
+ case "SHELL":
+ // marked as safe
+ return "/bin/sh"
+ case "GPG_AGENT_INFO":
+ // not marked as safe
+ return ".gpg-agent:0:1"
+ case "TERM":
+ // not really set
+ return ""
+ case "PATH":
+ // special handling from here down
+ return "/something/random:/sbin/:/home/ubuntu/bin:/bin:/snap/bin"
+ case "XDG_RUNTIME_DIR":
+ return "/some/thing"
+ case "LD_PRELOAD":
+ return "foo"
+ case "LD_LIBRARY_PATH":
+ return "bar"
+ }
+ return ""
+ })()
+
+ env := strings.Split(errtracker.Environ(), "\n")
+ sort.Strings(env)
+
+ c.Check(env, DeepEquals, []string{
+ "LD_LIBRARY_PATH=<set>",
+ "LD_PRELOAD=<set>",
+ // note also /sbin/ -> /sbin
+ "PATH=(custom):/sbin:(user):/bin:/snap/bin",
+ "SHELL=/bin/sh",
+ "XDG_RUNTIME_DIR=<set>",
+ })
+}
diff --git a/errtracker/export_test.go b/errtracker/export_test.go
index 3775bb84e9..a17177a46e 100644
--- a/errtracker/export_test.go
+++ b/errtracker/export_test.go
@@ -70,3 +70,52 @@ func MockReExec(f func() string) (restorer func()) {
didSnapdReExec = oldDidSnapdReExec
}
}
+
+func MockOsGetenv(f func(string) string) (restorer func()) {
+ old := osGetenv
+ osGetenv = f
+ return func() {
+ osGetenv = old
+ }
+}
+
+func MockProcCpuinfo(filename string) (restorer func()) {
+ old := procCpuinfo
+ procCpuinfo = filename
+ return func() {
+ procCpuinfo = old
+ }
+}
+
+func MockProcSelfExe(filename string) (restorer func()) {
+ old := procSelfExe
+ procSelfExe = filename
+ return func() {
+ procSelfExe = old
+ }
+}
+
+func MockProcSelfCwd(filename string) (restorer func()) {
+ old := procSelfCwd
+ procSelfCwd = filename
+ return func() {
+ procSelfCwd = old
+ }
+}
+
+func MockProcSelfCmdline(filename string) (restorer func()) {
+ old := procSelfCmdline
+ procSelfCmdline = filename
+ return func() {
+ procSelfCmdline = old
+ }
+}
+
+var (
+ ProcExe = procExe
+ ProcCwd = procCwd
+ ProcCmdline = procCmdline
+ JournalError = journalError
+ ProcCpuinfoMinimal = procCpuinfoMinimal
+ Environ = environ
+)
diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go
index 72c8ff01c5..70b0855ed7 100644
--- a/interfaces/apparmor/template.go
+++ b/interfaces/apparmor/template.go
@@ -134,6 +134,7 @@ var defaultTemplate = `
/{,usr/}bin/find ixr,
/{,usr/}bin/flock ixr,
/{,usr/}bin/fmt ixr,
+ /{,usr/}bin/getconf ixr,
/{,usr/}bin/getent ixr,
/{,usr/}bin/getopt ixr,
/{,usr/}bin/groups ixr,
diff --git a/interfaces/builtin/firewall_control.go b/interfaces/builtin/firewall_control.go
index 5e51c4ca64..7ee37fcf80 100644
--- a/interfaces/builtin/firewall_control.go
+++ b/interfaces/builtin/firewall_control.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -125,6 +125,7 @@ unix (bind) type=stream addr="@xtables",
@{PROC}/sys/net/ipv4/conf/*/log_martians w,
@{PROC}/sys/net/ipv4/tcp_syncookies w,
@{PROC}/sys/net/ipv6/conf/*/forwarding w,
+@{PROC}/sys/net/netfilter/nf_conntrack_helper rw,
`
// http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/seccomp/policygroups/ubuntu-core/16.04/firewall-control
diff --git a/interfaces/builtin/fuse_support.go b/interfaces/builtin/fuse_support.go
index bb966305c3..31338bac5d 100644
--- a/interfaces/builtin/fuse_support.go
+++ b/interfaces/builtin/fuse_support.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -54,7 +54,7 @@ capability sys_admin,
# Allow mounts to our snap-specific writable directories
# Note 1: fstype is 'fuse.<command>', eg 'fuse.sshfs'
# Note 2: due to LP: #1612393 - @{HOME} can't be used in mountpoint
-# Note 3: local fuse mounts of filesystem directories are mediated by
+# Note 3: local fuse mounts of filesystem directories are mediated by
# AppArmor. The actual underlying file in the source directory is
# mediated, not the presentation layer of the target directory, so
# we can safely allow all local mounts to our snap-specific writable
@@ -67,6 +67,10 @@ mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/@{
mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/@{SNAP_REVISION}/{,**/},
+mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/common/{,**/},
+mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /home/*/snap/@{SNAP_NAME}/common/{,**/},
+mount fstype=fuse.* options=(ro,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/common/{,**/},
+mount fstype=fuse.* options=(rw,nosuid,nodev) ** -> /var/snap/@{SNAP_NAME}/common/{,**/},
# Explicitly deny reads to /etc/fuse.conf. We do this to ensure that
# the safe defaults of fuse are used (which are enforced by our mount
diff --git a/interfaces/builtin/hostname_control.go b/interfaces/builtin/hostname_control.go
new file mode 100644
index 0000000000..b8697639e7
--- /dev/null
+++ b/interfaces/builtin/hostname_control.go
@@ -0,0 +1,89 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 builtin
+
+const hostnameControlSummary = `allows configuring the system hostname`
+
+const hostnameControlBaseDeclarationSlots = `
+ hostname-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+`
+
+const hostnameControlConnectedPlugAppArmor = `
+# Description: Can configure the system hostname.
+# /{,usr/}bin/hostname ixr, # already allowed by default
+/etc/hostname w, # read allowed by default
+
+#include <abstractions/dbus-strict>
+/{,usr/}{,s}bin/hostnamectl ixr,
+
+# Allow access to hostname system service
+dbus (send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Properties
+ member="Get{,All}"
+ peer=(label=unconfined),
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+dbus(receive, send)
+ bus=system
+ path=/org/freedesktop/hostname1
+ interface=org.freedesktop.hostname1
+ member=Set{,Pretty,Static}Hostname
+ peer=(label=unconfined),
+
+# Needed to use 'sethostname'. See man 7 capabilities
+capability sys_admin,
+# Needed to use 'hostnamectl set-hostname'
+capability sys_admin,
+`
+
+const hostnameControlConnectedPlugSecComp = `
+# Description: Can configure the system hostname.
+sethostname
+`
+
+func init() {
+ registerIface(&commonInterface{
+ name: "hostname-control",
+ summary: hostnameControlSummary,
+ implicitOnCore: true,
+ implicitOnClassic: true,
+ baseDeclarationSlots: hostnameControlBaseDeclarationSlots,
+ connectedPlugAppArmor: hostnameControlConnectedPlugAppArmor,
+ connectedPlugSecComp: hostnameControlConnectedPlugSecComp,
+ reservedForOS: true,
+ })
+
+}
diff --git a/interfaces/builtin/hostname_control_test.go b/interfaces/builtin/hostname_control_test.go
new file mode 100644
index 0000000000..4176a4517c
--- /dev/null
+++ b/interfaces/builtin/hostname_control_test.go
@@ -0,0 +1,111 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 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 builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type HostnameControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slotInfo *snap.SlotInfo
+ slot *interfaces.ConnectedSlot
+ plugInfo *snap.PlugInfo
+ plug *interfaces.ConnectedPlug
+}
+
+var _ = Suite(&HostnameControlInterfaceSuite{
+ iface: builtin.MustInterface("hostname-control"),
+})
+
+const hostnameControlConsumerYaml = `name: consumer
+version: 0
+apps:
+ app:
+ plugs: [hostname-control]
+`
+
+const hostnameControlCoreYaml = `name: core
+version: 0
+type: os
+slots:
+ hostname-control:
+`
+
+func (s *HostnameControlInterfaceSuite) SetUpTest(c *C) {
+ s.plug, s.plugInfo = MockConnectedPlug(c, hostnameControlConsumerYaml, nil, "hostname-control")
+ s.slot, s.slotInfo = MockConnectedSlot(c, hostnameControlCoreYaml, nil, "hostname-control")
+}
+
+func (s *HostnameControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "hostname-control")
+}
+
+func (s *HostnameControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
+ slot := &snap.SlotInfo{
+ Snap: &snap.Info{SuggestedName: "some-snap"},
+ Name: "hostname-control",
+ Interface: "hostname-control",
+ }
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
+ "hostname-control slots are reserved for the core snap")
+}
+
+func (s *HostnameControlInterfaceSuite) TestSanitizePlug(c *C) {
+ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+}
+
+func (s *HostnameControlInterfaceSuite) TestAppArmorSpec(c *C) {
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{,usr/}{,s}bin/hostnamectl")
+}
+
+func (s *HostnameControlInterfaceSuite) TestSecCompSpec(c *C) {
+ spec := &seccomp.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "sethostname\n")
+}
+
+func (s *HostnameControlInterfaceSuite) TestStaticInfo(c *C) {
+ si := interfaces.StaticInfoOf(s.iface)
+ c.Assert(si.ImplicitOnCore, Equals, true)
+ c.Assert(si.ImplicitOnClassic, Equals, true)
+ c.Assert(si.Summary, Equals, `allows configuring the system hostname`)
+ c.Assert(si.BaseDeclarationSlots, testutil.Contains, "hostname-control")
+}
+
+func (s *HostnameControlInterfaceSuite) TestAutoConnect(c *C) {
+ // FIXME: fix AutoConnect methods to use ConnectedPlug/Slot
+ c.Assert(s.iface.AutoConnect(&interfaces.Plug{PlugInfo: s.plugInfo}, &interfaces.Slot{SlotInfo: s.slotInfo}), Equals, true)
+}
+func (s *HostnameControlInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff --git a/interfaces/builtin/process_control.go b/interfaces/builtin/process_control.go
index 832acfbba3..1bd4936644 100644
--- a/interfaces/builtin/process_control.go
+++ b/interfaces/builtin/process_control.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -31,11 +31,13 @@ const processControlBaseDeclarationSlots = `
const processControlConnectedPlugAppArmor = `
# Description: This interface allows for controlling other processes via
-# signals and nice. This is reserved because it grants privileged access to
-# all processes under root or processes running under the same UID otherwise.
+# signals, cpu affinity and nice. This is reserved because it grants privileged
+# access to all processes under root or processes running under the same UID
+# otherwise.
-# /{,usr/}bin/nice is already in default policy, so just allow renice here
+# /{,usr/}bin/nice is already in default policy
/{,usr/}bin/renice ixr,
+/{,usr/}bin/taskset ixr,
capability sys_resource,
capability sys_nice,
@@ -45,8 +47,9 @@ signal,
const processControlConnectedPlugSecComp = `
# Description: This interface allows for controlling other processes via
-# signals and nice. This is reserved because it grants privileged access to
-# all processes under root or processes running under the same UID otherwise.
+# signals, cpu affinity and nice. This is reserved because it grants privileged
+# access to all processes under root or processes running under the same UID
+# otherwise.
# Allow setting the nice value/priority for any process
nice
diff --git a/interfaces/repo.go b/interfaces/repo.go
index 783961055c..311a2144e0 100644
--- a/interfaces/repo.go
+++ b/interfaces/repo.go
@@ -808,6 +808,9 @@ func (r *Repository) SnapSpecification(securitySystem SecuritySystem, snapName s
// Unknown interfaces and plugs/slots that don't validate are not added.
// Information about those failures are returned to the caller.
func (r *Repository) AddSnap(snapInfo *snap.Info) error {
+ if snapInfo.Broken != "" {
+ return fmt.Errorf("snap is broken: %s", snapInfo.Broken)
+ }
err := snap.Validate(snapInfo)
if err != nil {
return err
diff --git a/interfaces/system_key.go b/interfaces/system_key.go
index 0980a4366c..6fd39d9465 100644
--- a/interfaces/system_key.go
+++ b/interfaces/system_key.go
@@ -117,6 +117,7 @@ func generateSystemKey() (*systemKey, error) {
// profile.
sk.NFSHome, err = isHomeUsingNFS()
if err != nil {
+ // just log the error here
logger.Noticef("cannot determine nfs usage in generateSystemKey: %v", err)
return nil, err
}
@@ -125,6 +126,7 @@ func generateSystemKey() (*systemKey, error) {
// upperdir such that if this changes, we change our profile.
sk.OverlayRoot, err = osutil.IsRootWritableOverlay()
if err != nil {
+ // just log the error here
logger.Noticef("cannot determine root filesystem on overlay in generateSystemKey: %v", err)
return nil, err
}
diff --git a/osutil/mountentry.go b/osutil/mountentry.go
index 7d93f263f5..4d816d321a 100644
--- a/osutil/mountentry.go
+++ b/osutil/mountentry.go
@@ -127,7 +127,14 @@ func ParseMountEntry(s string) (MountEntry, error) {
var err error
var df, cpn int
fields := strings.FieldsFunc(s, func(r rune) bool { return r == ' ' || r == '\t' })
- // do all error checks before any assignments to `e'
+ // Look for any inline comments. The first field that starts with '#' is a comment.
+ for i, field := range fields {
+ if strings.HasPrefix(field, "#") {
+ fields = fields[:i]
+ break
+ }
+ }
+ // Do all error checks before any assignments to `e'
if len(fields) < 3 || len(fields) > 6 {
return e, fmt.Errorf("expected between 3 and 6 fields, found %d", len(fields))
}
diff --git a/osutil/mountentry_test.go b/osutil/mountentry_test.go
index f9b0d443bc..66e8fdff08 100644
--- a/osutil/mountentry_test.go
+++ b/osutil/mountentry_test.go
@@ -102,6 +102,18 @@ func (s *entrySuite) TestParseMountEntry1(c *C) {
c.Assert(e.CheckPassNumber, Equals, 0)
}
+// Test that hash inside a field value is supported.
+func (s *entrySuite) TestHashInFieldValue(c *C) {
+ e, err := osutil.ParseMountEntry("mhddfs#/mnt/dir1,/mnt/dir2 /mnt/dir fuse defaults,allow_other 0 0")
+ c.Assert(err, IsNil)
+ c.Assert(e.Name, Equals, "mhddfs#/mnt/dir1,/mnt/dir2")
+ c.Assert(e.Dir, Equals, "/mnt/dir")
+ c.Assert(e.Type, Equals, "fuse")
+ c.Assert(e.Options, DeepEquals, []string{"defaults", "allow_other"})
+ c.Assert(e.DumpFrequency, Equals, 0)
+ c.Assert(e.CheckPassNumber, Equals, 0)
+}
+
// Test that options are parsed correctly
func (s *entrySuite) TestParseMountEntry2(c *C) {
e, err := osutil.ParseMountEntry("name dir type options,comma,separated 0 0")
diff --git a/osutil/mountprofile.go b/osutil/mountprofile.go
index 4c692ca540..f3dc872221 100644
--- a/osutil/mountprofile.go
+++ b/osutil/mountprofile.go
@@ -66,10 +66,15 @@ func ReadMountProfile(reader io.Reader) (*MountProfile, error) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
s := scanner.Text()
- if i := strings.IndexByte(s, '#'); i != -1 {
- s = s[0:i]
- }
s = strings.TrimSpace(s)
+ // Skip lines that only contain a comment, that is, those that start
+ // with the '#' character (ignoring leading spaces). This specifically
+ // allows us to parse '#' inside individual fields, which the fstab(5)
+ // specification allows.
+ if strings.IndexByte(s, '#') == 0 {
+ continue
+ }
+ // Skip lines that are totally empty
if s == "" {
continue
}
diff --git a/osutil/mountprofile_test.go b/osutil/mountprofile_test.go
index f1bb1dc2b2..747ecd831a 100644
--- a/osutil/mountprofile_test.go
+++ b/osutil/mountprofile_test.go
@@ -58,6 +58,26 @@ func (s *profileSuite) TestLoadMountProfile2(c *C) {
})
}
+// Test that loading profile with various comments works as expected.
+func (s *profileSuite) TestLoadMountProfile3(c *C) {
+ dir := c.MkDir()
+ fname := filepath.Join(dir, "existing")
+ err := ioutil.WriteFile(fname, []byte(`
+ # comment with leading spaces
+name#-1 dir#-1 type#-1 options#-1 1 1 # inline comment
+# comment without leading spaces
+
+
+`), 0644)
+ c.Assert(err, IsNil)
+ p, err := osutil.LoadMountProfile(fname)
+ c.Assert(err, IsNil)
+ c.Assert(p.Entries, HasLen, 1)
+ c.Assert(p.Entries, DeepEquals, []osutil.MountEntry{
+ {Name: "name#-1", Dir: "dir#-1", Type: "type#-1", Options: []string{"options#-1"}, DumpFrequency: 1, CheckPassNumber: 1},
+ })
+}
+
// Test that saving a profile to a file works correctly.
func (s *profileSuite) TestSaveMountProfile1(c *C) {
dir := c.MkDir()
diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go
index 87506ded5c..c286f197fb 100644
--- a/overlord/devicestate/devicestate_test.go
+++ b/overlord/devicestate/devicestate_test.go
@@ -25,6 +25,7 @@ import (
"fmt"
"io"
"io/ioutil"
+ "net"
"net/http"
"net/http/httptest"
"os"
@@ -801,9 +802,17 @@ func (s *deviceMgrSuite) TestDoRequestSerialErrorsOnNoHost(c *C) {
c.Skip("cannot run test when http proxy is in use, the error pattern is different")
}
+ const nonexistent_host = "nowhere.nowhere.test"
+
+ // check internet access
+ _, err := net.LookupHost(nonexistent_host)
+ if netErr, ok := err.(net.Error); !ok || netErr.Temporary() {
+ c.Skip("cannot run test with no internet access, the error pattern is different")
+ }
+
privKey, _ := assertstest.GenerateKey(testKeyLength)
- nowhere := "http://nowhere.nowhere.test"
+ nowhere := "http://" + nonexistent_host
mockRequestIDURL := nowhere + requestIDURLPath
restore := devicestate.MockRequestIDURL(mockRequestIDURL)
diff --git a/overlord/devicestate/handlers.go b/overlord/devicestate/handlers.go
index 49ace1c860..6e296a8186 100644
--- a/overlord/devicestate/handlers.go
+++ b/overlord/devicestate/handlers.go
@@ -47,6 +47,9 @@ func (m *DeviceManager) doMarkSeeded(t *state.Task, _ *tomb.Tomb) error {
st.Set("seed-time", time.Now())
st.Set("seeded", true)
+ // make sure we setup a fallback model/consider the next phase
+ // (registration) timely
+ st.EnsureBefore(0)
return nil
}
diff --git a/overlord/hookstate/ctlcmd/services_test.go b/overlord/hookstate/ctlcmd/services_test.go
index 69549fbc91..b8442bcf57 100644
--- a/overlord/hookstate/ctlcmd/services_test.go
+++ b/overlord/hookstate/ctlcmd/services_test.go
@@ -47,18 +47,18 @@ type fakeStore struct {
storetest.Store
}
-func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
- return &snap.Info{
- SideInfo: snap.SideInfo{
- RealName: spec.Name,
- Revision: snap.R(2),
- },
- Publisher: "foo",
- Architectures: []string{"all"},
- }, nil
-}
+func (f *fakeStore) SnapAction(_ context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ if len(actions) == 1 && actions[0].Action == "install" {
+ return []*snap.Info{{
+ SideInfo: snap.SideInfo{
+ RealName: actions[0].Name,
+ Revision: snap.R(2),
+ },
+ Publisher: "foo",
+ Architectures: []string{"all"},
+ }}, nil
+ }
-func (f *fakeStore) ListRefresh(_ context.Context, cand []*store.RefreshCandidate, user *auth.UserState, opt *store.RefreshOptions) ([]*snap.Info, error) {
return []*snap.Info{{
SideInfo: snap.SideInfo{
RealName: "test-snap",
diff --git a/overlord/hookstate/hookmgr.go b/overlord/hookstate/hookmgr.go
index 8f331b52e8..74418a2a6b 100644
--- a/overlord/hookstate/hookmgr.go
+++ b/overlord/hookstate/hookmgr.go
@@ -26,6 +26,7 @@ import (
"regexp"
"strings"
"sync"
+ "sync/atomic"
"time"
"gopkg.in/tomb.v2"
@@ -54,6 +55,8 @@ type HookManager struct {
contexts map[string]*Context
hijackMap map[hijackKey]hijackFunc
+
+ runningHooks int32
}
// Handler is the interface a client must satify to handle hooks.
@@ -218,6 +221,25 @@ func hookSetup(task *state.Task) (*HookSetup, *snapstate.SnapState, error) {
return &hooksup, &snapst, nil
}
+// NumRunningHooks returns the number of hooks running at the moment.
+func (m *HookManager) NumRunningHooks() int {
+ return int(atomic.LoadInt32(&m.runningHooks))
+}
+
+// GracefullyWaitRunningHooks waits for currently running hooks to finish up to the default hook timeout. Returns true if there are no more running hooks on exit.
+func (m *HookManager) GracefullyWaitRunningHooks() bool {
+ toutC := time.After(defaultHookTimeout)
+ doWait := true
+ for m.NumRunningHooks() > 0 && doWait {
+ select {
+ case <-time.After(1 * time.Second):
+ case <-toutC:
+ doWait = false
+ }
+ }
+ return m.NumRunningHooks() == 0
+}
+
// doRunHook actually runs the hook that was requested.
//
// Note that this method is synchronous, as the task is already running in a
@@ -249,6 +271,18 @@ func (m *HookManager) doRunHook(task *state.Task, tomb *tomb.Tomb) error {
}
}
+ if hookExists || mustHijack {
+ // we will run something, not a noop
+ if task.State().Restarting() {
+ // don't start running a hook if we are restarting
+ return &state.Retry{}
+ }
+
+ // keep count of running hooks
+ atomic.AddInt32(&m.runningHooks, 1)
+ defer atomic.AddInt32(&m.runningHooks, -1)
+ }
+
context, err := NewContext(task, task.State(), hooksup, nil, "")
if err != nil {
return err
diff --git a/overlord/hookstate/hookstate_test.go b/overlord/hookstate/hookstate_test.go
index 0817b28e78..93a1c07114 100644
--- a/overlord/hookstate/hookstate_test.go
+++ b/overlord/hookstate/hookstate_test.go
@@ -199,7 +199,20 @@ func (s *hookManagerSuite) TestHookTask(c *C) {
}
func (s *hookManagerSuite) TestHookTaskEnsure(c *C) {
+ didRun := make(chan bool)
+ s.mockHandler.BeforeCallback = func() {
+ c.Check(s.manager.NumRunningHooks(), Equals, 1)
+ go func() {
+ didRun <- s.manager.GracefullyWaitRunningHooks()
+ }()
+ }
s.manager.Ensure()
+ select {
+ case ok := <-didRun:
+ c.Check(ok, Equals, true)
+ case <-time.After(5 * time.Second):
+ c.Fatal("hook run should have been done by now")
+ }
s.manager.Wait()
s.state.Lock()
@@ -221,6 +234,32 @@ func (s *hookManagerSuite) TestHookTaskEnsure(c *C) {
c.Check(s.task.Kind(), Equals, "run-hook")
c.Check(s.task.Status(), Equals, state.DoneStatus)
c.Check(s.change.Status(), Equals, state.DoneStatus)
+
+ c.Check(s.manager.NumRunningHooks(), Equals, 0)
+}
+
+func (s *hookManagerSuite) TestHookTaskEnsureRestarting(c *C) {
+ // we do no start new hooks runs if we are restarting
+ s.state.RequestRestart(state.RestartDaemon)
+
+ s.manager.Ensure()
+ s.manager.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ c.Assert(s.context, IsNil)
+
+ c.Check(s.command.Calls(), HasLen, 0)
+
+ c.Check(s.mockHandler.BeforeCalled, Equals, false)
+ c.Check(s.mockHandler.DoneCalled, Equals, false)
+ c.Check(s.mockHandler.ErrorCalled, Equals, false)
+
+ c.Check(s.task.Status(), Equals, state.DoingStatus)
+ c.Check(s.change.Status(), Equals, state.DoingStatus)
+
+ c.Check(s.manager.NumRunningHooks(), Equals, 0)
}
func (s *hookManagerSuite) TestHookSnapMissing(c *C) {
@@ -339,6 +378,8 @@ func (s *hookManagerSuite) TestHookTaskHandlesHookError(c *C) {
c.Check(s.task.Status(), Equals, state.ErrorStatus)
c.Check(s.change.Status(), Equals, state.ErrorStatus)
checkTaskLogContains(c, s.task, ".*failed at user request.*")
+
+ c.Check(s.manager.NumRunningHooks(), Equals, 0)
}
func (s *hookManagerSuite) TestHookTaskHandleIgnoreErrorWorks(c *C) {
@@ -506,6 +547,8 @@ func (s *hookManagerSuite) TestHookTaskCanKillHook(c *C) {
c.Check(s.task.Status(), Equals, state.ErrorStatus)
c.Check(s.change.Status(), Equals, state.ErrorStatus)
checkTaskLogContains(c, s.task, `run hook "[^"]*": <aborted>`)
+
+ c.Check(s.manager.NumRunningHooks(), Equals, 0)
}
func (s *hookManagerSuite) TestHookTaskCorrectlyIncludesContext(c *C) {
@@ -952,3 +995,38 @@ func (s *hookManagerSuite) TestCompatForConfigureSnapd(c *C) {
c.Check(chg.Status(), Equals, state.DoneStatus)
c.Check(task.Status(), Equals, state.DoneStatus)
}
+
+func (s *hookManagerSuite) TestGracefullyWaitRunningHooksTimeout(c *C) {
+ restore := hookstate.MockDefaultHookTimeout(100 * time.Millisecond)
+ defer restore()
+
+ // this works even if test-snap is not present
+ s.state.Lock()
+ snapstate.Set(s.state, "test-snap", nil)
+ s.state.Unlock()
+
+ quit := make(chan struct{})
+ defer func() {
+ quit <- struct{}{}
+ }()
+ didRun := make(chan bool)
+ s.mockHandler.BeforeCallback = func() {
+ c.Check(s.manager.NumRunningHooks(), Equals, 1)
+ go func() {
+ didRun <- s.manager.GracefullyWaitRunningHooks()
+ }()
+ }
+
+ s.manager.RegisterHijack("configure", "test-snap", func(ctx *hookstate.Context) error {
+ <-quit
+ return nil
+ })
+
+ s.manager.Ensure()
+ select {
+ case noPending := <-didRun:
+ c.Check(noPending, Equals, false)
+ case <-time.After(2 * time.Second):
+ c.Fatal("timeout should have expired")
+ }
+}
diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go
index fb4e713a51..f9d36578b3 100644
--- a/overlord/ifacestate/export_test.go
+++ b/overlord/ifacestate/export_test.go
@@ -27,7 +27,8 @@ import (
)
var (
- AddImplicitSlots = addImplicitSlots
+ AddImplicitSlots = addImplicitSlots
+ SnapsWithSecurityProfiles = snapsWithSecurityProfiles
)
// AddForeignTaskHandlers registers handlers for tasks handled outside of the
diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go
index 4a2b05b8b6..2359ff3978 100644
--- a/overlord/ifacestate/handlers.go
+++ b/overlord/ifacestate/handlers.go
@@ -119,6 +119,27 @@ func (m *InterfaceManager) doSetupProfiles(task *state.Task, tomb *tomb.Tomb) er
return fmt.Errorf("cannot finish core installation, there was a rollback across reboot")
}
}
+
+ // Compatibility with old snapd: check if we have auto-connect task and if not, inject it after self (setup-profiles).
+ // Inject it for core after the 2nd setup-profiles - same placement as done in doInstall.
+ // In the older snapd versions interfaces were auto-connected as part of setupProfilesForSnap.
+ var hasAutoConnect bool
+ for _, t := range task.Change().Tasks() {
+ if t.Kind() == "auto-connect" {
+ otherSnapsup, err := snapstate.TaskSnapSetup(t)
+ if err != nil {
+ return err
+ }
+ // Check if this is auto-connect task for same snap
+ if snapsup.Name() == otherSnapsup.Name() {
+ hasAutoConnect = true
+ break
+ }
+ }
+ }
+ if !hasAutoConnect {
+ snapstate.InjectAutoConnect(task, snapsup)
+ }
}
opts := confinementOptions(snapsup.Flags)
diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go
index 19e95640e5..cb94bd0507 100644
--- a/overlord/ifacestate/helpers.go
+++ b/overlord/ifacestate/helpers.go
@@ -96,14 +96,14 @@ func (m *InterfaceManager) addBackends(extra []interfaces.SecurityBackend) error
}
func (m *InterfaceManager) addSnaps() error {
- snaps, err := snapstate.ActiveInfos(m.state)
+ snaps, err := snapsWithSecurityProfiles(m.state)
if err != nil {
return err
}
for _, snapInfo := range snaps {
addImplicitSlots(snapInfo)
if err := m.repo.AddSnap(snapInfo); err != nil {
- logger.Noticef("%s", err)
+ logger.Noticef("cannot add snap %q to interface repository: %s", snapInfo.Name(), err)
}
}
return nil
@@ -124,7 +124,7 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error {
securityBackends := m.repo.Backends()
// Get all the snap infos
- snaps, err := snapstate.ActiveInfos(m.state)
+ snaps, err := snapsWithSecurityProfiles(m.state)
if err != nil {
return err
}
@@ -159,7 +159,10 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error {
}
}
- return interfaces.WriteSystemKey()
+ if err := interfaces.WriteSystemKey(); err != nil {
+ logger.Noticef("cannot write system key: %v", err)
+ }
+ return nil
}
// renameCorePlugConnection renames one connection from "core-support" plug to
@@ -358,3 +361,57 @@ func getConns(st *state.State) (map[string]connState, error) {
func setConns(st *state.State, conns map[string]connState) {
st.Set("conns", conns)
}
+
+// snapsWithSecurityProfiles returns all snaps that have active
+// security profiles: these are either snaps that are active, or about
+// to be active (pending link-snap) with a done setup-profiles
+func snapsWithSecurityProfiles(st *state.State) ([]*snap.Info, error) {
+ infos, err := snapstate.ActiveInfos(st)
+ if err != nil {
+ return nil, err
+ }
+ seen := make(map[string]bool, len(infos))
+ for _, info := range infos {
+ seen[info.Name()] = true
+ }
+ for _, t := range st.Tasks() {
+ if t.Kind() != "link-snap" || t.Status().Ready() {
+ continue
+ }
+ snapsup, err := snapstate.TaskSnapSetup(t)
+ if err != nil {
+ return nil, err
+ }
+ snapName := snapsup.Name()
+ if seen[snapName] {
+ continue
+ }
+
+ doneProfiles := false
+ for _, t1 := range t.WaitTasks() {
+ if t1.Kind() == "setup-profiles" && t1.Status() == state.DoneStatus {
+ snapsup1, err := snapstate.TaskSnapSetup(t)
+ if err != nil {
+ return nil, err
+ }
+ if snapsup1.Name() == snapName {
+ doneProfiles = true
+ break
+ }
+ }
+ }
+ if !doneProfiles {
+ continue
+ }
+
+ seen[snapName] = true
+ snapInfo, err := snap.ReadInfo(snapName, snapsup.SideInfo)
+ if err != nil {
+ logger.Noticef("cannot retrieve info for snap %q: %s", snapName, err)
+ continue
+ }
+ infos = append(infos, snapInfo)
+ }
+
+ return infos, nil
+}
diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go
index 58a7aec18f..b49b43ddcd 100644
--- a/overlord/ifacestate/ifacestate_test.go
+++ b/overlord/ifacestate/ifacestate_test.go
@@ -2521,3 +2521,186 @@ slots:
defer s.state.Unlock()
c.Check(tConnectPlug.Status(), Equals, state.DoneStatus)
}
+
+/*
+func (s *interfaceManagerSuite) TestSetupProfilesInjectsAutoConnectIfMissing(c *C) {
+ mgr := s.manager(c)
+
+ si1 := &snap.SideInfo{
+ RealName: "snap1",
+ Revision: snap.R(1),
+ }
+ sup1 := &snapstate.SnapSetup{SideInfo: si1}
+ _ = snaptest.MockSnap(c, sampleSnapYaml, si1)
+
+ si2 := &snap.SideInfo{
+ RealName: "snap2",
+ Revision: snap.R(1),
+ }
+ sup2 := &snapstate.SnapSetup{SideInfo: si2}
+ _ = snaptest.MockSnap(c, consumerYaml, si2)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task1 := s.state.NewTask("setup-profiles", "")
+ task1.Set("snap-setup", sup1)
+
+ task2 := s.state.NewTask("setup-profiles", "")
+ task2.Set("snap-setup", sup2)
+
+ chg := s.state.NewChange("test", "")
+ chg.AddTask(task1)
+ task2.WaitFor(task1)
+ chg.AddTask(task2)
+
+ s.state.Unlock()
+
+ defer mgr.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ // ensure all our tasks ran
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Tasks(), HasLen, 4)
+
+ // sanity checks
+ t := chg.Tasks()[0]
+ c.Assert(t.Kind(), Equals, "setup-profiles")
+ t = chg.Tasks()[1]
+ c.Assert(t.Kind(), Equals, "setup-profiles")
+
+ // check that auto-connect tasks were added and have snap-setup
+ var autoconnectSup snapstate.SnapSetup
+ t = chg.Tasks()[2]
+ c.Assert(t.Kind(), Equals, "auto-connect")
+ c.Assert(t.Get("snap-setup", &autoconnectSup), IsNil)
+ c.Assert(autoconnectSup.Name(), Equals, "snap1")
+
+ t = chg.Tasks()[3]
+ c.Assert(t.Kind(), Equals, "auto-connect")
+ c.Assert(t.Get("snap-setup", &autoconnectSup), IsNil)
+ c.Assert(autoconnectSup.Name(), Equals, "snap2")
+}
+*/
+
+func (s *interfaceManagerSuite) TestSetupProfilesInjectsAutoConnectIfCore(c *C) {
+ mgr := s.manager(c)
+
+ si1 := &snap.SideInfo{
+ RealName: "core",
+ Revision: snap.R(1),
+ }
+ sup1 := &snapstate.SnapSetup{SideInfo: si1}
+ _ = snaptest.MockSnap(c, ubuntuCoreSnapYaml, si1)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task1 := s.state.NewTask("setup-profiles", "")
+ task1.Set("snap-setup", sup1)
+
+ task2 := s.state.NewTask("setup-profiles", "")
+ task2.Set("snap-setup", sup1)
+ task2.Set("core-phase-2", true)
+ task2.WaitFor(task1)
+
+ chg := s.state.NewChange("test", "")
+ chg.AddTask(task1)
+ chg.AddTask(task2)
+
+ s.state.Unlock()
+
+ defer mgr.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ // ensure all our tasks ran
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Tasks(), HasLen, 3)
+
+ // sanity checks
+ t := chg.Tasks()[0]
+ c.Assert(t.Kind(), Equals, "setup-profiles")
+ t = chg.Tasks()[1]
+ c.Assert(t.Kind(), Equals, "setup-profiles")
+ var phase2 bool
+ c.Assert(t.Get("core-phase-2", &phase2), IsNil)
+ c.Assert(t.HaltTasks(), HasLen, 1)
+
+ // check that auto-connect task was added after phase2
+ var autoconnectSup snapstate.SnapSetup
+ t = chg.Tasks()[2]
+ c.Assert(t.Kind(), Equals, "auto-connect")
+ c.Assert(t.Get("snap-setup", &autoconnectSup), IsNil)
+ c.Assert(autoconnectSup.Name(), Equals, "core")
+}
+
+func (s *interfaceManagerSuite) TestSnapsWithSecurityProfiles(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ si0 := &snap.SideInfo{
+ RealName: "snap0",
+ Revision: snap.R(10),
+ }
+ snaptest.MockSnap(c, `name: snap0`, si0)
+ snapstate.Set(s.state, "snap0", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si0},
+ Current: si0.Revision,
+ })
+
+ snaps := []struct {
+ name string
+ setupStatus state.Status
+ linkStatus state.Status
+ }{
+ {"snap0", state.DoneStatus, state.DoneStatus},
+ {"snap1", state.DoneStatus, state.DoStatus},
+ {"snap2", state.DoneStatus, state.ErrorStatus},
+ {"snap3", state.DoneStatus, state.UndoingStatus},
+ {"snap4", state.DoingStatus, state.DoStatus},
+ {"snap6", state.DoStatus, state.DoStatus},
+ }
+
+ for i, snp := range snaps {
+ var si *snap.SideInfo
+
+ if snp.name != "snap0" {
+ si = &snap.SideInfo{
+ RealName: snp.name,
+ Revision: snap.R(i),
+ }
+ snaptest.MockSnap(c, "name: "+snp.name, si)
+ }
+
+ chg := s.state.NewChange("linking", "linking 1")
+ t1 := s.state.NewTask("setup-profiles", "setup profiles 1")
+ t1.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ })
+ t1.SetStatus(snp.setupStatus)
+ t2 := s.state.NewTask("link-snap", "link snap 1")
+ t2.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ })
+ t2.WaitFor(t1)
+ t2.SetStatus(snp.linkStatus)
+ chg.AddTask(t1)
+ chg.AddTask(t2)
+ }
+
+ infos, err := ifacestate.SnapsWithSecurityProfiles(s.state)
+ c.Assert(err, IsNil)
+ c.Check(infos, HasLen, 3)
+ got := make(map[string]snap.Revision)
+ for _, info := range infos {
+ got[info.Name()] = info.Revision
+ }
+ c.Check(got, DeepEquals, map[string]snap.Revision{
+ "snap0": snap.R(10),
+ "snap1": snap.R(1),
+ "snap3": snap.R(3),
+ })
+}
diff --git a/overlord/managers_test.go b/overlord/managers_test.go
index e70955dcfa..dda907a3ba 100644
--- a/overlord/managers_test.go
+++ b/overlord/managers_test.go
@@ -360,6 +360,29 @@ const (
"summary": "Foo",
"version": "@VERSION@"
}`
+
+ snapV2 = `{
+ "architectures": [
+ "all"
+ ],
+ "download": {
+ "url": "@URL@"
+ },
+ "type": "app",
+ "name": "@NAME@",
+ "revision": @REVISION@,
+ "snap-id": "@SNAPID@",
+ "summary": "Foo",
+ "description": "this is a description",
+ "version": "@VERSION@",
+ "publisher": {
+ "id": "devdevdev",
+ "name": "bar"
+ },
+ "media": [
+ {"type": "icon", "url": "@ICON@"}
+ ]
+}`
)
var fooSnapID = fakeSnapID("foo")
@@ -416,7 +439,7 @@ func (ms *mgrsSuite) makeStoreTestSnap(c *C, snapYaml string, revno string) (pat
func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
var baseURL *url.URL
- fillHit := func(name string) string {
+ fillHit := func(hitTemplate, name string) string {
snapf, err := snap.Open(ms.serveSnapPath[name])
if err != nil {
panic(err)
@@ -425,7 +448,7 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
if err != nil {
panic(err)
}
- hit := strings.Replace(searchHit, "@URL@", baseURL.String()+"/api/v1/snaps/download/"+name, -1)
+ hit := strings.Replace(hitTemplate, "@URL@", baseURL.String()+"/api/v1/snaps/download/"+name, -1)
hit = strings.Replace(hit, "@NAME@", name, -1)
hit = strings.Replace(hit, "@SNAPID@", fakeSnapID(name), -1)
hit = strings.Replace(hit, "@ICON@", baseURL.String()+"/icon", -1)
@@ -435,13 +458,25 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
}
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // all URLS are /api/v1/snaps/... so check the url is sane and discard
- // the common prefix to simplify indexing into the comps slice.
+ // all URLS are /api/v1/snaps/... or /v2/snaps/... so
+ // check the url is sane and discard the common prefix
+ // to simplify indexing into the comps slice.
comps := strings.Split(r.URL.Path, "/")
- if len(comps) <= 4 {
+ if len(comps) < 2 {
panic("unexpected url path: " + r.URL.Path)
}
- comps = comps[4:]
+ if comps[1] == "api" { //v1
+ if len(comps) <= 4 {
+ panic("unexpected url path: " + r.URL.Path)
+ }
+ comps = comps[4:]
+ } else { // v2
+ if len(comps) <= 3 {
+ panic("unexpected url path: " + r.URL.Path)
+ }
+ comps = comps[3:]
+ comps[0] = "v2:" + comps[0]
+ }
switch comps[0] {
case "assertions":
@@ -465,7 +500,7 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
return
case "details":
w.WriteHeader(200)
- io.WriteString(w, fillHit(comps[1]))
+ io.WriteString(w, fillHit(searchHit, comps[1]))
case "metadata":
dec := json.NewDecoder(r.Body)
var input struct {
@@ -484,7 +519,7 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
if snap.R(s.Revision) == snap.R(ms.serveRevision[name]) {
continue
}
- hits = append(hits, json.RawMessage(fillHit(name)))
+ hits = append(hits, json.RawMessage(fillHit(searchHit, name)))
}
w.WriteHeader(200)
output, err := json.Marshal(map[string]interface{}{
@@ -506,6 +541,50 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server {
panic(err)
}
io.Copy(w, snapR)
+ case "v2:refresh":
+ dec := json.NewDecoder(r.Body)
+ var input struct {
+ Actions []struct {
+ Action string `json:"action"`
+ SnapID string `json:"snap-id"`
+ Name string `json:"name"`
+ } `json:"actions"`
+ }
+ if err := dec.Decode(&input); err != nil {
+ panic(err)
+ }
+ type resultJSON struct {
+ Result string `json:"result"`
+ SnapID string `json:"snap-id"`
+ Name string `json:"name"`
+ Snap json.RawMessage `json:"snap"`
+ }
+ var results []resultJSON
+ for _, a := range input.Actions {
+ name := ms.serveIDtoName[a.SnapID]
+ if a.Action == "install" {
+ name = a.Name
+ }
+ if ms.serveSnapPath[name] == "" {
+ // no match
+ continue
+ }
+ results = append(results, resultJSON{
+ Result: a.Action,
+ SnapID: a.SnapID,
+ Name: name,
+ Snap: json.RawMessage(fillHit(snapV2, name)),
+ })
+ }
+ w.WriteHeader(200)
+ output, err := json.Marshal(map[string]interface{}{
+ "results": results,
+ })
+ if err != nil {
+ panic(err)
+ }
+ w.Write(output)
+
default:
panic("unexpected url path: " + r.URL.Path)
}
diff --git a/overlord/overlord_test.go b/overlord/overlord_test.go
index f54e7c0d19..0f4f3a32f8 100644
--- a/overlord/overlord_test.go
+++ b/overlord/overlord_test.go
@@ -207,6 +207,9 @@ func (ovs *overlordSuite) TestTrivialRunAndStop(c *C) {
c.Assert(err, IsNil)
markSeeded(o)
+ // make sure we don't try to talk to the store
+ snapstate.CanAutoRefresh = nil
+
o.Loop()
err = o.Stop()
diff --git a/overlord/snapstate/autorefresh_test.go b/overlord/snapstate/autorefresh_test.go
index b25f7841e7..00b50384a7 100644
--- a/overlord/snapstate/autorefresh_test.go
+++ b/overlord/snapstate/autorefresh_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-2018 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
@@ -57,6 +57,22 @@ func (r *autoRefreshStore) ListRefresh(ctx context.Context, cands []*store.Refre
return nil, r.listRefreshErr
}
+func (r *autoRefreshStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ if ctx == nil || !auth.IsEnsureContext(ctx) {
+ panic("Ensure marked context required")
+ }
+ if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
+ panic("expected in test one action for each current snaps, and at least one snap")
+ }
+ for _, a := range actions {
+ if a.Action != "refresh" {
+ panic("expected refresh actions")
+ }
+ }
+ r.ops = append(r.ops, "list-refresh")
+ return nil, r.listRefreshErr
+}
+
type autoRefreshTestSuite struct {
state *state.State
diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go
index 0088995c73..4a1b743299 100644
--- a/overlord/snapstate/backend.go
+++ b/overlord/snapstate/backend.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -37,9 +37,13 @@ type StoreService interface {
SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error)
Find(search *store.Search, user *auth.UserState) ([]*snap.Info, error)
LookupRefresh(*store.RefreshCandidate, *auth.UserState) (*snap.Info, error)
+
ListRefresh(context.Context, []*store.RefreshCandidate, *auth.UserState, *store.RefreshOptions) ([]*snap.Info, error)
+ SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error)
+
Sections(ctx context.Context, user *auth.UserState) ([]string, error)
WriteCatalogs(ctx context.Context, names io.Writer, adder store.SnapAdder) error
+
Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error
Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go
index 1d4180434a..f51190454f 100644
--- a/overlord/snapstate/backend_test.go
+++ b/overlord/snapstate/backend_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -47,7 +47,9 @@ type fakeOp struct {
revno snap.Revision
sinfo snap.SideInfo
stype snap.Type
- cand store.RefreshCandidate
+
+ curSnaps []store.CurrentSnap
+ action store.SnapAction
old string
@@ -93,6 +95,29 @@ type fakeDownload struct {
macaroon string
}
+type byName []store.CurrentSnap
+
+func (bna byName) Len() int { return len(bna) }
+func (bna byName) Swap(i, j int) { bna[i], bna[j] = bna[j], bna[i] }
+func (bna byName) Less(i, j int) bool {
+ return bna[i].Name < bna[j].Name
+}
+
+type byAction []*store.SnapAction
+
+func (ba byAction) Len() int { return len(ba) }
+func (ba byAction) Swap(i, j int) { ba[i], ba[j] = ba[j], ba[i] }
+func (ba byAction) Less(i, j int) bool {
+ if ba[i].Action == ba[j].Action {
+ if ba[i].Action == "refresh" {
+ return ba[i].SnapID < ba[j].SnapID
+ } else {
+ return ba[i].Name < ba[j].Name
+ }
+ }
+ return ba[i].Action < ba[j].Action
+}
+
type fakeStore struct {
storetest.Store
@@ -114,6 +139,18 @@ func (f *fakeStore) pokeStateLock() {
func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
f.pokeStateLock()
+ info, err := f.snapInfo(spec, user)
+
+ userID := 0
+ if user != nil {
+ userID = user.ID
+ }
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: info.Revision, userID: userID})
+
+ return info, err
+}
+
+func (f *fakeStore) snapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
if spec.Revision.Unset() {
spec.Revision = snap.R(11)
if spec.Channel == "channel-for-7" {
@@ -128,6 +165,10 @@ func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.I
typ = snap.TypeOS
}
+ if spec.Name == "snap-unknown" {
+ return nil, store.ErrSnapNotFound
+ }
+
info := &snap.Info{
Architectures: []string{"all"},
SideInfo: snap.SideInfo{
@@ -163,27 +204,23 @@ func (f *fakeStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.I
}
}
- userID := 0
- if user != nil {
- userID = user.ID
- }
- f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap", name: spec.Name, revno: spec.Revision, userID: userID})
-
return info, nil
}
-func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserState) (*snap.Info, error) {
- f.pokeStateLock()
-
- if cand == nil {
- panic("LookupRefresh called with no candidate")
- }
+type refreshCand struct {
+ snapID string
+ channel string
+ revision snap.Revision
+ block []snap.Revision
+ ignoreValidation bool
+}
+func (f *fakeStore) lookupRefresh(cand refreshCand) (*snap.Info, error) {
var name string
- switch cand.SnapID {
+ switch cand.snapID {
case "":
- return nil, store.ErrLocalSnap
+ panic("store refresh APIs expect snap-ids")
case "other-snap-id":
return nil, store.ErrNoUpdateAvailable
case "fakestore-please-error-on-refresh":
@@ -196,16 +233,20 @@ func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserS
name = "core"
case "snap-with-snapd-control-id":
name = "snap-with-snapd-control"
+ case "producer-id":
+ name = "producer"
+ case "consumer-id":
+ name = "consumer"
default:
- panic(fmt.Sprintf("ListRefresh: unknown snap-id: %s", cand.SnapID))
+ panic(fmt.Sprintf("refresh: unknown snap-id: %s", cand.snapID))
}
revno := snap.R(11)
- if r := f.refreshRevnos[cand.SnapID]; !r.Unset() {
+ if r := f.refreshRevnos[cand.snapID]; !r.Unset() {
revno = r
}
confinement := snap.StrictConfinement
- switch cand.Channel {
+ switch cand.channel {
case "channel-for-7":
revno = snap.R(7)
case "channel-for-classic":
@@ -217,8 +258,8 @@ func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserS
info := &snap.Info{
SideInfo: snap.SideInfo{
RealName: name,
- Channel: cand.Channel,
- SnapID: cand.SnapID,
+ Channel: cand.channel,
+ SnapID: cand.snapID,
Revision: revno,
},
Version: name,
@@ -228,7 +269,7 @@ func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserS
Confinement: confinement,
Architectures: []string{"all"},
}
- switch cand.Channel {
+ switch cand.channel {
case "channel-for-layout":
info.Layout = map[string]*snap.Layout{
"/usr": {
@@ -240,23 +281,16 @@ func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserS
}
var hit snap.Revision
- if cand.Revision != revno {
+ if cand.revision != revno {
hit = revno
}
- for _, blocked := range cand.Block {
+ for _, blocked := range cand.block {
if blocked == revno {
hit = snap.Revision{}
break
}
}
- userID := 0
- if user != nil {
- userID = user.ID
- }
- // TODO: move this back to ListRefresh
- f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-list-refresh", cand: *cand, revno: hit, userID: userID})
-
if !hit.Unset() {
return info, nil
}
@@ -264,28 +298,136 @@ func (f *fakeStore) LookupRefresh(cand *store.RefreshCandidate, user *auth.UserS
return nil, store.ErrNoUpdateAvailable
}
-func (f *fakeStore) ListRefresh(ctx context.Context, cands []*store.RefreshCandidate, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, error) {
+func (f *fakeStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
if ctx == nil {
panic("context required")
}
f.pokeStateLock()
- if len(cands) == 0 {
+ if len(currentSnaps) == 0 && len(actions) == 0 {
return nil, nil
}
- if len(cands) > 3 {
- panic("fake ListRefresh unexpectedly called with more than 3 candidates")
+ if len(actions) > 3 {
+ panic("fake SnapAction unexpectedly called with more than 3 actions")
+ }
+
+ curByID := make(map[string]*store.CurrentSnap, len(currentSnaps))
+ curSnaps := make(byName, len(currentSnaps))
+ for i, cur := range currentSnaps {
+ if cur.Name == "" || cur.SnapID == "" || cur.Revision.Unset() {
+ return nil, fmt.Errorf("internal error: incomplete current snap info")
+ }
+ curByID[cur.SnapID] = cur
+ curSnaps[i] = *cur
+ }
+ sort.Sort(curSnaps)
+
+ userID := 0
+ if user != nil {
+ userID = user.ID
+ }
+ if len(curSnaps) == 0 {
+ curSnaps = nil
}
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{op: "storesvc-snap-action", curSnaps: curSnaps, userID: userID})
+
+ sorted := make(byAction, len(actions))
+ copy(sorted, actions)
+ sort.Sort(sorted)
+ refreshErrors := make(map[string]error)
+ installErrors := make(map[string]error)
var res []*snap.Info
- for _, cand := range cands {
- info, err := f.LookupRefresh(cand, user)
- if err == store.ErrLocalSnap || err == store.ErrNoUpdateAvailable {
+ for _, a := range sorted {
+ if a.Action != "install" && a.Action != "refresh" {
+ panic("not supported")
+ }
+
+ if a.Action == "install" {
+ spec := store.SnapSpec{
+ Name: a.Name,
+ Channel: a.Channel,
+ Revision: a.Revision,
+ }
+ info, err := f.snapInfo(spec, user)
+ if err != nil {
+ installErrors[a.Name] = err
+ continue
+ }
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{
+ op: "storesvc-snap-action:action",
+ action: *a,
+ revno: info.Revision,
+ userID: userID,
+ })
+ if !a.Revision.Unset() {
+ info.Channel = ""
+ }
+ res = append(res, info)
continue
}
+
+ // refresh
+
+ cur := curByID[a.SnapID]
+ channel := a.Channel
+ if channel == "" {
+ channel = cur.TrackingChannel
+ }
+ ignoreValidation := cur.IgnoreValidation
+ if a.Flags&store.SnapActionIgnoreValidation != 0 {
+ ignoreValidation = true
+ } else if a.Flags&store.SnapActionEnforceValidation != 0 {
+ ignoreValidation = false
+ }
+ cand := refreshCand{
+ snapID: a.SnapID,
+ channel: channel,
+ revision: cur.Revision,
+ block: cur.Block,
+ ignoreValidation: ignoreValidation,
+ }
+ info, err := f.lookupRefresh(cand)
+ var hit snap.Revision
+ if info != nil {
+ if !a.Revision.Unset() {
+ info.Revision = a.Revision
+ }
+ hit = info.Revision
+ }
+ f.fakeBackend.ops = append(f.fakeBackend.ops, fakeOp{
+ op: "storesvc-snap-action:action",
+ action: *a,
+ revno: hit,
+ userID: userID,
+ })
+ if err == store.ErrNoUpdateAvailable {
+ refreshErrors[cur.Name] = err
+ continue
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !a.Revision.Unset() {
+ info.Channel = ""
+ }
res = append(res, info)
}
+ if len(refreshErrors)+len(installErrors) > 0 || len(res) == 0 {
+ if len(refreshErrors) == 0 {
+ refreshErrors = nil
+ }
+ if len(installErrors) == 0 {
+ installErrors = nil
+ }
+ return res, &store.SnapActionError{
+ NoResults: len(refreshErrors)+len(installErrors)+len(res) == 0,
+ Refresh: refreshErrors,
+ Install: installErrors,
+ }
+ }
+
return res, nil
}
diff --git a/overlord/snapstate/catalogrefresh_test.go b/overlord/snapstate/catalogrefresh_test.go
index 474521c2da..7180f823f6 100644
--- a/overlord/snapstate/catalogrefresh_test.go
+++ b/overlord/snapstate/catalogrefresh_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-2018 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
@@ -50,8 +50,8 @@ func (r *catalogStore) WriteCatalogs(ctx context.Context, w io.Writer, a store.S
}
r.ops = append(r.ops, "write-catalog")
w.Write([]byte("pkg1\npkg2"))
- a.AddSnap("foo", "foo summary", []string{"foo", "meh"})
- a.AddSnap("bar", "bar summray", []string{"bar", "meh"})
+ a.AddSnap("foo", "1.0", "foo summary", []string{"foo", "meh"})
+ a.AddSnap("bar", "2.0", "bar summray", []string{"bar", "meh"})
return nil
}
@@ -105,10 +105,10 @@ func (s *catalogRefreshTestSuite) TestCatalogRefresh(c *C) {
c.Check(osutil.FileExists(dirs.SnapCommandsDB), Equals, true)
dump, err := advisor.DumpCommands()
c.Assert(err, IsNil)
- c.Check(dump, DeepEquals, map[string][]string{
- "foo": {"foo"},
- "bar": {"bar"},
- "meh": {"foo", "bar"},
+ c.Check(dump, DeepEquals, map[string]string{
+ "foo": `[{"snap":"foo","version":"1.0"}]`,
+ "bar": `[{"snap":"bar","version":"2.0"}]`,
+ "meh": `[{"snap":"foo","version":"1.0"},{"snap":"bar","version":"2.0"}]`,
})
}
diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go
index 210fb480fc..7237b814c2 100644
--- a/overlord/snapstate/export_test.go
+++ b/overlord/snapstate/export_test.go
@@ -89,6 +89,15 @@ func MockReadInfo(mock func(name string, si *snap.SideInfo) (*snap.Info, error))
return func() { readInfo = old }
}
+func MockRevisionDate(mock func(info *snap.Info) time.Time) (restore func()) {
+ old := revisionDate
+ if mock == nil {
+ mock = revisionDateImpl
+ }
+ revisionDate = mock
+ return func() { revisionDate = old }
+}
+
func MockOpenSnapFile(mock func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error)) (restore func()) {
prevOpenSnapFile := openSnapFile
openSnapFile = mock
diff --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go
index cfd1b57d04..120dc5ca08 100644
--- a/overlord/snapstate/handlers.go
+++ b/overlord/snapstate/handlers.go
@@ -29,6 +29,7 @@ import (
"gopkg.in/tomb.v2"
"github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/configstate/config"
@@ -36,7 +37,6 @@ import (
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/store"
)
// TaskSnapSetup returns the SnapSetup with task params hold by or referred to by the the task.
@@ -362,6 +362,12 @@ func (m *SnapManager) undoPrepareSnap(t *state.Task, _ *tomb.Tomb) error {
return nil
}
+func installInfoUnlocked(st *state.State, snapsup *SnapSetup) (*snap.Info, error) {
+ st.Lock()
+ defer st.Unlock()
+ return installInfo(st, snapsup.Name(), snapsup.Channel, snapsup.Revision(), snapsup.UserID)
+}
+
func (m *SnapManager) doDownloadSnap(t *state.Task, tomb *tomb.Tomb) error {
st := t.State()
st.Lock()
@@ -386,12 +392,7 @@ func (m *SnapManager) doDownloadSnap(t *state.Task, tomb *tomb.Tomb) error {
// COMPATIBILITY - this task was created from an older version
// of snapd that did not store the DownloadInfo in the state
// yet.
- spec := store.SnapSpec{
- Name: snapsup.Name(),
- Channel: snapsup.Channel,
- Revision: snapsup.Revision(),
- }
- storeInfo, err = theStore.SnapInfo(spec, user)
+ storeInfo, err = installInfoUnlocked(st, snapsup)
if err != nil {
return err
}
@@ -719,6 +720,32 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error {
t.Set("old-candidate-index", oldCandidateIndex)
// Do at the end so we only preserve the new state if it worked.
Set(st, snapsup.Name(), snapst)
+
+ // Compatibility with old snapd: check if we have auto-connect task and
+ // if not, inject it after self (link-snap) for snaps that are not core
+ if newInfo.Type != snap.TypeOS {
+ var hasAutoConnect, hasSetupProfiles bool
+ for _, other := range t.Change().Tasks() {
+ // Check if this is auto-connect task for same snap and we it's part of the change with setup-profiles task
+ if other.Kind() == "auto-connect" || other.Kind() == "setup-profiles" {
+ otherSnapsup, err := TaskSnapSetup(other)
+ if err != nil {
+ return err
+ }
+ if snapsup.Name() == otherSnapsup.Name() {
+ if other.Kind() == "auto-connect" {
+ hasAutoConnect = true
+ } else {
+ hasSetupProfiles = true
+ }
+ }
+ }
+ }
+ if !hasAutoConnect && hasSetupProfiles {
+ InjectAutoConnect(t, snapsup)
+ }
+ }
+
// Make sure if state commits and snapst is mutated we won't be rerun
t.SetStatus(state.DoneStatus)
@@ -1640,3 +1667,39 @@ func (m *SnapManager) doPreferAliases(t *state.Task, _ *tomb.Tomb) error {
Set(st, snapName, snapst)
return nil
}
+
+// InjectTasks makes all the halt tasks of the mainTask wait for extraTasks;
+// extraTasks join the same lane and change as the mainTask.
+func InjectTasks(mainTask *state.Task, extraTasks *state.TaskSet) {
+ lanes := mainTask.Lanes()
+ if len(lanes) == 1 && lanes[0] == 0 {
+ lanes = nil
+ }
+ for _, l := range lanes {
+ extraTasks.JoinLane(l)
+ }
+
+ chg := mainTask.Change()
+ // Change shouldn't normally be nil, except for cases where
+ // this helper is used before tasks are added to a change.
+ if chg != nil {
+ chg.AddAll(extraTasks)
+ }
+
+ // make all halt tasks of the mainTask wait on extraTasks
+ ht := mainTask.HaltTasks()
+ for _, t := range ht {
+ t.WaitAll(extraTasks)
+ }
+
+ // make the extra tasks wait for main task
+ extraTasks.WaitFor(mainTask)
+}
+
+func InjectAutoConnect(mainTask *state.Task, snapsup *SnapSetup) {
+ st := mainTask.State()
+ autoConnect := st.NewTask("auto-connect", fmt.Sprintf(i18n.G("Automatically connect eligible plugs and slots of snap %q"), snapsup.Name()))
+ autoConnect.Set("snap-setup", snapsup)
+ InjectTasks(mainTask, state.NewTaskSet(autoConnect))
+ mainTask.Logf("added auto-connect task")
+}
diff --git a/overlord/snapstate/handlers_download_test.go b/overlord/snapstate/handlers_download_test.go
index 2a581a51c7..e3b9f5e262 100644
--- a/overlord/snapstate/handlers_download_test.go
+++ b/overlord/snapstate/handlers_download_test.go
@@ -25,6 +25,7 @@ import (
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
)
type downloadSnapSuite struct {
@@ -90,8 +91,15 @@ func (s *downloadSnapSuite) TestDoDownloadSnapCompatbility(c *C) {
// the compat code called the store "Snap" endpoint
c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{
{
- op: "storesvc-snap",
- name: "foo",
+ op: "storesvc-snap-action",
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "foo",
+ Channel: "some-channel",
+ },
revno: snap.R(11),
},
{
diff --git a/overlord/snapstate/handlers_link_test.go b/overlord/snapstate/handlers_link_test.go
index 9a1007e5f7..b2e1501ffb 100644
--- a/overlord/snapstate/handlers_link_test.go
+++ b/overlord/snapstate/handlers_link_test.go
@@ -266,7 +266,7 @@ func (s *linkSnapSuite) TestDoUndoLinkSnap(c *C) {
s.state.Unlock()
- for i := 0; i < 3; i++ {
+ for i := 0; i < 6; i++ {
s.snapmgr.Ensure()
s.snapmgr.Wait()
}
@@ -388,7 +388,7 @@ func (s *linkSnapSuite) TestDoUndoLinkSnapSequenceDidNotHaveCandidate(c *C) {
s.state.Unlock()
- for i := 0; i < 3; i++ {
+ for i := 0; i < 6; i++ {
s.snapmgr.Ensure()
s.snapmgr.Wait()
}
@@ -432,7 +432,7 @@ func (s *linkSnapSuite) TestDoUndoLinkSnapSequenceHadCandidate(c *C) {
s.state.Unlock()
- for i := 0; i < 3; i++ {
+ for i := 0; i < 6; i++ {
s.snapmgr.Ensure()
s.snapmgr.Wait()
}
@@ -538,3 +538,69 @@ func (s *linkSnapSuite) TestDoUndoLinkSnapCoreClassic(c *C) {
c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon, state.RestartDaemon})
}
+
+func (s *linkSnapSuite) TestLinkSnapInjectsAutoConnectIfMissing(c *C) {
+ si1 := &snap.SideInfo{
+ RealName: "snap1",
+ Revision: snap.R(1),
+ }
+ sup1 := &snapstate.SnapSetup{SideInfo: si1}
+ si2 := &snap.SideInfo{
+ RealName: "snap2",
+ Revision: snap.R(1),
+ }
+ sup2 := &snapstate.SnapSetup{SideInfo: si2}
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ task0 := s.state.NewTask("setup-profiles", "")
+ task1 := s.state.NewTask("link-snap", "")
+ task1.WaitFor(task0)
+ task0.Set("snap-setup", sup1)
+ task1.Set("snap-setup", sup1)
+
+ task2 := s.state.NewTask("setup-profiles", "")
+ task3 := s.state.NewTask("link-snap", "")
+ task2.WaitFor(task1)
+ task3.WaitFor(task2)
+ task2.Set("snap-setup", sup2)
+ task3.Set("snap-setup", sup2)
+
+ chg := s.state.NewChange("test", "")
+ chg.AddTask(task0)
+ chg.AddTask(task1)
+ chg.AddTask(task2)
+ chg.AddTask(task3)
+
+ s.state.Unlock()
+
+ for i := 0; i < 10; i++ {
+ s.snapmgr.Ensure()
+ s.snapmgr.Wait()
+ }
+
+ s.state.Lock()
+
+ // ensure all our tasks ran
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.Tasks(), HasLen, 6)
+
+ // sanity checks
+ t := chg.Tasks()[1]
+ c.Assert(t.Kind(), Equals, "link-snap")
+ t = chg.Tasks()[3]
+ c.Assert(t.Kind(), Equals, "link-snap")
+
+ // check that auto-connect tasks were added and have snap-setup
+ var autoconnectSup snapstate.SnapSetup
+ t = chg.Tasks()[4]
+ c.Assert(t.Kind(), Equals, "auto-connect")
+ c.Assert(t.Get("snap-setup", &autoconnectSup), IsNil)
+ c.Assert(autoconnectSup.Name(), Equals, "snap1")
+
+ t = chg.Tasks()[5]
+ c.Assert(t.Kind(), Equals, "auto-connect")
+ c.Assert(t.Get("snap-setup", &autoconnectSup), IsNil)
+ c.Assert(autoconnectSup.Name(), Equals, "snap2")
+}
diff --git a/overlord/snapstate/handlers_prereq_test.go b/overlord/snapstate/handlers_prereq_test.go
index e7e0c7aed0..765894a04a 100644
--- a/overlord/snapstate/handlers_prereq_test.go
+++ b/overlord/snapstate/handlers_prereq_test.go
@@ -29,6 +29,7 @@ import (
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
)
type prereqSuite struct {
@@ -123,18 +124,39 @@ func (s *prereqSuite) TestDoPrereqTalksToStoreAndQueues(c *C) {
defer s.state.Unlock()
c.Assert(s.fakeBackend.ops, DeepEquals, fakeOps{
{
- op: "storesvc-snap",
- name: "prereq1",
+ op: "storesvc-snap-action",
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "prereq1",
+ Channel: "stable",
+ },
revno: snap.R(11),
},
{
- op: "storesvc-snap",
- name: "prereq2",
+ op: "storesvc-snap-action",
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "prereq2",
+ Channel: "stable",
+ },
revno: snap.R(11),
},
{
- op: "storesvc-snap",
- name: "some-base",
+ op: "storesvc-snap-action",
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "some-base",
+ Channel: "stable",
+ },
revno: snap.R(11),
},
})
diff --git a/overlord/snapstate/refreshhints_test.go b/overlord/snapstate/refreshhints_test.go
index 94d40104b1..e292e8123e 100644
--- a/overlord/snapstate/refreshhints_test.go
+++ b/overlord/snapstate/refreshhints_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-2018 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
@@ -49,6 +49,22 @@ func (r *recordingStore) ListRefresh(ctx context.Context, cands []*store.Refresh
return nil, nil
}
+func (r *recordingStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ if ctx == nil || !auth.IsEnsureContext(ctx) {
+ panic("Ensure marked context required")
+ }
+ if len(currentSnaps) != len(actions) || len(currentSnaps) == 0 {
+ panic("expected in test one action for each current snaps, and at least one snap")
+ }
+ for _, a := range actions {
+ if a.Action != "refresh" {
+ panic("expected refresh actions")
+ }
+ }
+ r.ops = append(r.ops, "list-refresh")
+ return nil, nil
+}
+
type refreshHintsTestSuite struct {
state *state.State
diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go
index 4abccfe753..ea006e745b 100644
--- a/overlord/snapstate/snapmgr.go
+++ b/overlord/snapstate/snapmgr.go
@@ -30,6 +30,7 @@ import (
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/errtracker"
"github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
@@ -215,6 +216,9 @@ var readInfo = readInfoAnyway
func readInfoAnyway(name string, si *snap.SideInfo) (*snap.Info, error) {
info, err := snap.ReadInfo(name, si)
+ if err != nil {
+ logger.Noticef("cannot read snap info of snap %q at revision %s: %s", name, si.Revision, err)
+ }
if _, ok := err.(*snap.NotFoundError); ok {
reason := fmt.Sprintf("cannot read snap %q: %s", name, err)
info := &snap.Info{
@@ -230,6 +234,17 @@ func readInfoAnyway(name string, si *snap.SideInfo) (*snap.Info, error) {
return info, err
}
+var revisionDate = revisionDateImpl
+
+// revisionDate returns a good approximation of when a revision reached the system.
+func revisionDateImpl(info *snap.Info) time.Time {
+ fi, err := os.Lstat(info.MountFile())
+ if err != nil {
+ return time.Time{}
+ }
+ return fi.ModTime()
+}
+
// CurrentInfo returns the information about the current active revision or the last active revision (if the snap is inactive). It returns the ErrNoCurrent error if snapst.Current is unset.
func (snapst *SnapState) CurrentInfo() (*snap.Info, error) {
cur := snapst.CurrentSideInfo()
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index ffb11bc524..612f4a48d9 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -610,7 +610,7 @@ func Install(st *state.State, name, channel string, revision snap.Revision, user
return nil, &snap.AlreadyInstalledError{Snap: name}
}
- info, err := snapInfo(st, name, channel, revision, userID)
+ info, err := installInfo(st, name, channel, revision, userID)
if err != nil {
return nil, err
}
@@ -640,6 +640,7 @@ func Install(st *state.State, name, channel string, revision snap.Revision, user
func InstallMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
installed := make([]string, 0, len(names))
tasksets := make([]*state.TaskSet, 0, len(names))
+ // TODO: this could be reorged to do one single store call
for _, name := range names {
ts, err := Install(st, name, "", snap.R(0), userID, Flags{})
// FIXME: is this expected behavior?
@@ -1080,7 +1081,7 @@ func infoForUpdate(st *state.State, snapst *SnapState, name, channel string, rev
}
if sideInfo == nil {
// refresh from given revision from store
- return updateToRevisionInfo(st, snapst, channel, revision, userID)
+ return updateToRevisionInfo(st, snapst, revision, userID)
}
// refresh-to-local
@@ -1517,12 +1518,6 @@ func TransitionCore(st *state.State, oldName, newName string) ([]*state.TaskSet,
return nil, fmt.Errorf("cannot transition snap %q: not installed", oldName)
}
- var userID int
- newInfo, err := snapInfo(st, newName, oldSnapst.Channel, snap.R(0), userID)
- if err != nil {
- return nil, err
- }
-
var all []*state.TaskSet
// install new core (if not already installed)
err = Get(st, newName, &newSnapst)
@@ -1530,6 +1525,12 @@ func TransitionCore(st *state.State, oldName, newName string) ([]*state.TaskSet,
return nil, err
}
if !newSnapst.IsInstalled() {
+ var userID int
+ newInfo, err := installInfo(st, newName, oldSnapst.Channel, snap.R(0), userID)
+ if err != nil {
+ return nil, err
+ }
+
// start by instaling the new snap
tsInst, err := doInstall(st, &newSnapst, &SnapSetup{
Channel: oldSnapst.Channel,
diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go
index a153100247..cf03e33fcc 100644
--- a/overlord/snapstate/snapstate_test.go
+++ b/overlord/snapstate/snapstate_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -78,6 +78,8 @@ func (s *snapmgrTestSuite) settle(c *C) {
var _ = Suite(&snapmgrTestSuite{})
+var fakeRevDateEpoch = time.Date(2018, 1, 0, 0, 0, 0, 0, time.UTC)
+
func (s *snapmgrTestSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
@@ -116,6 +118,14 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) {
s.BaseTest.AddCleanup(snapstate.MockReadInfo(s.fakeBackend.ReadInfo))
s.BaseTest.AddCleanup(snapstate.MockOpenSnapFile(s.fakeBackend.OpenSnapFile))
+ revDate := func(info *snap.Info) time.Time {
+ if info.Revision.Local() {
+ panic("no local revision should reach revisionDate")
+ }
+ // for convenience a date derived from the revision
+ return fakeRevDateEpoch.AddDate(0, 0, info.Revision.N)
+ }
+ s.BaseTest.AddCleanup(snapstate.MockRevisionDate(revDate))
s.BaseTest.AddCleanup(func() {
snapstate.SetupInstallHook = oldSetupInstallHook
@@ -1134,7 +1144,7 @@ type sneakyStore struct {
state *state.State
}
-func (s sneakyStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
+func (s sneakyStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
s.state.Lock()
snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
Active: true,
@@ -1144,7 +1154,7 @@ func (s sneakyStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.
SnapType: "app",
})
s.state.Unlock()
- return s.fakeStore.SnapInfo(spec, user)
+ return s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts)
}
func (s *snapmgrTestSuite) TestInstallStateConflict(c *C) {
@@ -1574,6 +1584,163 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
defer s.state.Unlock()
chg := s.state.NewChange("install", "install a snap")
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.snapmgr.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ // ensure all our tasks ran
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Check(snapstate.Installing(s.state), Equals, false)
+ c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{
+ macaroon: s.user.StoreMacaroon,
+ name: "some-snap",
+ }})
+ expected := fakeOps{
+ {
+ op: "storesvc-snap-action",
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "some-snap",
+ Channel: "some-channel",
+ },
+ revno: snap.R(11),
+ userID: 1,
+ },
+ {
+ op: "storesvc-download",
+ name: "some-snap",
+ },
+ {
+ op: "validate-snap:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "current",
+ old: "<no-current>",
+ },
+ {
+ op: "open-snap-file",
+ name: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "setup-snap",
+ name: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
+ revno: snap.R(11),
+ },
+ {
+ op: "copy-data",
+ name: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
+ old: "<no-old>",
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ },
+ },
+ {
+ op: "link-snap",
+ name: filepath.Join(dirs.SnapMountDir, "some-snap/11"),
+ },
+ {
+ op: "auto-connect:Doing",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "cleanup-trash",
+ name: "some-snap",
+ revno: snap.R(11),
+ },
+ }
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Assert(s.fakeBackend.ops, DeepEquals, expected)
+
+ // check progress
+ ta := ts.Tasks()
+ task := ta[1]
+ _, cur, total := task.Progress()
+ c.Assert(cur, Equals, s.fakeStore.fakeCurrentProgress)
+ c.Assert(total, Equals, s.fakeStore.fakeTotalProgress)
+ c.Check(task.Summary(), Equals, `Download snap "some-snap" (11) from channel "some-channel"`)
+
+ // check link/start snap summary
+ linkTask := ta[len(ta)-7]
+ c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (11) available to the system`)
+ startTask := ta[len(ta)-2]
+ c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (11) services`)
+
+ // verify snap-setup in the task state
+ var snapsup snapstate.SnapSetup
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Assert(snapsup, DeepEquals, snapstate.SnapSetup{
+ Channel: "some-channel",
+ UserID: s.user.ID,
+ SnapPath: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"),
+ DownloadInfo: &snap.DownloadInfo{
+ DownloadURL: "https://some-server.com/some/path.snap",
+ },
+ SideInfo: snapsup.SideInfo,
+ })
+ c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ SnapID: "some-snap-id",
+ })
+
+ // verify snaps in the system state
+ var snaps map[string]*snapstate.SnapState
+ err = s.state.Get("snaps", &snaps)
+ c.Assert(err, IsNil)
+
+ snapst := snaps["some-snap"]
+ c.Assert(snapst.Active, Equals, true)
+ c.Assert(snapst.Channel, Equals, "some-channel")
+ c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Revision: snap.R(11),
+ })
+ c.Assert(snapst.Required, Equals, false)
+}
+
+func (s *snapmgrTestSuite) TestInstallWithRevisionRunThrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ chg := s.state.NewChange("install", "install a snap")
ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -1593,8 +1760,16 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
}})
expected := fakeOps{
{
- op: "storesvc-snap",
- name: "some-snap",
+ op: "storesvc-snap-action",
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "some-snap",
+ Revision: snap.R(42),
+ },
revno: snap.R(42),
userID: 1,
},
@@ -1616,7 +1791,6 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
name: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"),
sinfo: snap.SideInfo{
RealName: "some-snap",
- Channel: "some-channel",
SnapID: "some-snap-id",
Revision: snap.R(42),
},
@@ -1640,7 +1814,6 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
op: "candidate",
sinfo: snap.SideInfo{
RealName: "some-snap",
- Channel: "some-channel",
SnapID: "some-snap-id",
Revision: snap.R(42),
},
@@ -1697,7 +1870,6 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{
RealName: "some-snap",
Revision: snap.R(42),
- Channel: "some-channel",
SnapID: "some-snap-id",
})
@@ -1711,7 +1883,6 @@ func (s *snapmgrTestSuite) TestInstallRunThrough(c *C) {
c.Assert(snapst.Channel, Equals, "some-channel")
c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
RealName: "some-snap",
- Channel: "some-channel",
SnapID: "some-snap-id",
Revision: snap.R(42),
})
@@ -1739,6 +1910,13 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
Revision: snap.R(7),
SnapID: "services-snap-id",
}
+ snaptest.MockSnap(c, `name: services-snap`, &si)
+ fi, err := os.Stat(snap.MountFile("services-snap", si.Revision))
+ c.Assert(err, IsNil)
+ refreshedDate := fi.ModTime()
+ // look at disk
+ r := snapstate.MockRevisionDate(nil)
+ defer r()
s.state.Lock()
defer s.state.Unlock()
@@ -1748,6 +1926,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
Sequence: []*snap.SideInfo{&si},
Current: si.Revision,
SnapType: "app",
+ Channel: "stable",
})
chg := s.state.NewChange("refresh", "refresh a snap")
@@ -1762,11 +1941,23 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) {
expected := fakeOps{
{
- op: "storesvc-list-refresh",
- cand: store.RefreshCandidate{
- Channel: "some-channel",
- SnapID: "services-snap-id",
- Revision: snap.R(7),
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "services-snap",
+ SnapID: "services-snap-id",
+ Revision: snap.R(7),
+ TrackingChannel: "stable",
+ RefreshedDate: refreshedDate,
+ }},
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "services-snap-id",
+ Channel: "some-channel",
+ Flags: store.SnapActionEnforceValidation,
},
revno: snap.R(11),
userID: 1,
@@ -1947,7 +2138,7 @@ func (s *snapmgrTestSuite) TestUpdateRememberedUserRunThrough(c *C) {
for _, op := range s.fakeBackend.ops {
switch op.op {
- case "storesvc-list-refresh":
+ case "storesvc-snap-action":
c.Check(op.userID, Equals, 1)
case "storesvc-download":
snapName := op.name
@@ -1988,7 +2179,7 @@ func (s *snapmgrTestSuite) TestUpdateToRevisionRememberedUserRunThrough(c *C) {
for _, op := range s.fakeBackend.ops {
switch op.op {
- case "storesvc-snap":
+ case "storesvc-snap-action:action":
c.Check(op.userID, Equals, 1)
case "storesvc-download":
snapName := op.name
@@ -2059,11 +2250,19 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsNoUserRunThrough(c *C) {
userID int
}
seen := make(map[snapIDuserID]bool)
+ ir := 0
di := 0
for _, op := range s.fakeBackend.ops {
switch op.op {
- case "storesvc-list-refresh":
- snapID := op.cand.SnapID
+ case "storesvc-snap-action":
+ ir++
+ c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{
+ {Name: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)},
+ {Name: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)},
+ {Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)},
+ })
+ case "storesvc-snap-action:action":
+ snapID := op.action.SnapID
seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true
case "storesvc-download":
snapName := op.name
@@ -2074,13 +2273,11 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsNoUserRunThrough(c *C) {
di++
}
}
+ c.Check(ir, Equals, 3)
// we check all snaps with each user
c.Check(seen, DeepEquals, map[snapIDuserID]bool{
- {snapID: "core-snap-id", userID: 1}: true,
+ {snapID: "core-snap-id", userID: 0}: true,
{snapID: "some-snap-id", userID: 1}: true,
- {snapID: "services-snap-id", userID: 1}: true,
- {snapID: "core-snap-id", userID: 2}: true,
- {snapID: "some-snap-id", userID: 2}: true,
{snapID: "services-snap-id", userID: 2}: true,
})
}
@@ -2144,11 +2341,19 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserRunThrough(c *C) {
userID int
}
seen := make(map[snapIDuserID]bool)
+ ir := 0
di := 0
for _, op := range s.fakeBackend.ops {
switch op.op {
- case "storesvc-list-refresh":
- snapID := op.cand.SnapID
+ case "storesvc-snap-action":
+ ir++
+ c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{
+ {Name: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)},
+ {Name: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)},
+ {Name: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)},
+ })
+ case "storesvc-snap-action:action":
+ snapID := op.action.SnapID
seen[snapIDuserID{snapID: snapID, userID: op.userID}] = true
case "storesvc-download":
snapName := op.name
@@ -2159,13 +2364,11 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserRunThrough(c *C) {
di++
}
}
+ c.Check(ir, Equals, 2)
// we check all snaps with each user
c.Check(seen, DeepEquals, map[snapIDuserID]bool{
- {snapID: "core-snap-id", userID: 1}: true,
- {snapID: "some-snap-id", userID: 1}: true,
- {snapID: "services-snap-id", userID: 1}: true,
{snapID: "core-snap-id", userID: 2}: true,
- {snapID: "some-snap-id", userID: 2}: true,
+ {snapID: "some-snap-id", userID: 1}: true,
{snapID: "services-snap-id", userID: 2}: true,
})
@@ -2215,11 +2418,22 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) {
expected := fakeOps{
{
- op: "storesvc-list-refresh",
- cand: store.RefreshCandidate{
- Channel: "some-channel",
- SnapID: "some-snap-id",
- Revision: snap.R(7),
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Flags: store.SnapActionEnforceValidation,
},
revno: snap.R(11),
userID: 1,
@@ -2375,11 +2589,23 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) {
expected := fakeOps{
{
- op: "storesvc-list-refresh",
- cand: store.RefreshCandidate{
- Channel: "some-channel",
- SnapID: "some-snap-id",
- Revision: snap.R(7),
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ TrackingChannel: "stable",
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Channel: "some-channel",
+ Flags: store.SnapActionEnforceValidation,
},
revno: snap.R(11),
userID: 1,
@@ -2530,6 +2756,41 @@ func (s *snapmgrTestSuite) TestUpdateSameRevision(c *C) {
c.Assert(err, Equals, store.ErrNoUpdateAvailable)
}
+// A noResultsStore returns no results for install/refresh requests
+type noResultsStore struct {
+ *fakeStore
+}
+
+func (n noResultsStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ return nil, &store.SnapActionError{NoResults: true}
+}
+
+func (s *snapmgrTestSuite) TestUpdateNoStoreResults(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.ReplaceStore(s.state, noResultsStore{fakeStore: s.fakeStore})
+
+ // this is an atypical case in which the store didn't return
+ // an error nor a result, we are defensive and return
+ // a reasonable error
+ si := snap.SideInfo{
+ RealName: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ }
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Channel: "channel-for-7",
+ Current: si.Revision,
+ })
+
+ _, err := snapstate.Update(s.state, "some-snap", "channel-for-7", snap.R(0), s.user.ID, snapstate.Flags{})
+ c.Assert(err, Equals, store.ErrNoUpdateAvailable)
+}
+
func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchesChannel(c *C) {
si := snap.SideInfo{
RealName: "some-snap",
@@ -2608,15 +2869,28 @@ func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchChannelRunThrough(c *C) {
s.state.Lock()
expected := fakeOps{
- // we just expect the "storesvc-list-refresh" op, we
+ // we just expect the "storesvc-snap-action" ops, we
// don't have a fakeOp for switchChannel because it has
// not a backend method, it just manipulates the state
{
- op: "storesvc-list-refresh",
- cand: store.RefreshCandidate{
- Channel: "channel-for-7",
- SnapID: "some-snap-id",
- Revision: snap.R(7),
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ TrackingChannel: "other-channel",
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
+ userID: 1,
+ },
+
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Channel: "channel-for-7",
+ Flags: store.SnapActionEnforceValidation,
},
userID: 1,
},
@@ -2871,13 +3145,24 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) {
c.Assert(err, IsNil)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- revno: snap.R(11),
- cand: store.RefreshCandidate{
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
SnapID: "some-snap-id",
Revision: snap.R(7),
- Channel: "stable",
- IgnoreValidation: true,
+ IgnoreValidation: false,
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
+ userID: 1,
+ })
+ c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
+ op: "storesvc-snap-action:action",
+ revno: snap.R(11),
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Channel: "stable",
+ Flags: store.SnapActionIgnoreValidation,
},
userID: 1,
})
@@ -2906,13 +3191,24 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) {
c.Check(tts, HasLen, 1)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- revno: snap.R(12),
- cand: store.RefreshCandidate{
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
SnapID: "some-snap-id",
Revision: snap.R(11),
- Channel: "stable",
+ TrackingChannel: "stable",
IgnoreValidation: true,
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 11),
+ }},
+ userID: 1,
+ })
+ c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
+ op: "storesvc-snap-action:action",
+ revno: snap.R(12),
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Flags: 0,
},
userID: 1,
})
@@ -2946,13 +3242,25 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) {
c.Assert(err, IsNil)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- revno: snap.R(11),
- cand: store.RefreshCandidate{
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
SnapID: "some-snap-id",
Revision: snap.R(12),
- Channel: "stable",
- IgnoreValidation: false,
+ TrackingChannel: "stable",
+ IgnoreValidation: true,
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 12),
+ }},
+ userID: 1,
+ })
+ c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{
+ op: "storesvc-snap-action:action",
+ revno: snap.R(11),
+ action: store.SnapAction{
+ Action: "refresh",
+ SnapID: "some-snap-id",
+ Channel: "stable",
+ Flags: store.SnapActionEnforceValidation,
},
userID: 1,
})
@@ -3019,7 +3327,26 @@ func (s *snapmgrTestSuite) TestUpdateAmend(c *C) {
err = tasks[1].Get("snap-setup", &snapsup)
c.Assert(err, IsNil)
c.Check(snapsup.Revision(), Equals, snap.R(7))
+}
+
+func (s *snapmgrTestSuite) TestUpdateAmendSnapNotFound(c *C) {
+ si := snap.SideInfo{
+ RealName: "snap-unknown",
+ Revision: snap.R("x1"),
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "snap-unknown", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{&si},
+ Channel: "stable",
+ Current: si.Revision,
+ })
+ _, err := snapstate.Update(s.state, "snap-unknown", "stable", snap.R(0), s.user.ID, snapstate.Flags{Amend: true})
+ c.Assert(err, Equals, store.ErrSnapNotFound)
}
func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) {
@@ -3048,18 +3375,17 @@ func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) {
_, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
- c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Assert(s.fakeBackend.ops, HasLen, 2)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- revno: snap.R(11),
- cand: store.RefreshCandidate{
- SnapID: "some-snap-id",
- Revision: snap.R(7),
- Channel: "some-channel",
- },
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
userID: 1,
})
-
}
func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) {
@@ -3089,17 +3415,17 @@ func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) {
c.Assert(err, IsNil)
c.Check(updates, DeepEquals, []string{"some-snap"})
- c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Assert(s.fakeBackend.ops, HasLen, 2)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- revno: snap.R(11),
- cand: store.RefreshCandidate{
- SnapID: "some-snap-id",
- Revision: snap.R(7),
- },
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ }},
userID: 1,
})
-
}
func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) {
@@ -3128,17 +3454,18 @@ func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) {
c.Check(err, IsNil)
c.Check(updates, HasLen, 0)
- c.Assert(s.fakeBackend.ops, HasLen, 1)
+ c.Assert(s.fakeBackend.ops, HasLen, 2)
c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{
- op: "storesvc-list-refresh",
- cand: store.RefreshCandidate{
- SnapID: "some-snap-id",
- Revision: snap.R(7),
- Block: []snap.Revision{snap.R(11)},
- },
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{{
+ Name: "some-snap",
+ SnapID: "some-snap-id",
+ Revision: snap.R(7),
+ RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7),
+ Block: []snap.Revision{snap.R(11)},
+ }},
userID: 1,
})
-
}
var orthogonalAutoAliasesScenarios = []struct {
@@ -5199,6 +5526,11 @@ func (s *snapmgrTestSuite) TestEnableRunThrough(c *C) {
name: filepath.Join(dirs.SnapMountDir, "some-snap/7"),
},
{
+ op: "auto-connect:Doing",
+ name: "some-snap",
+ revno: snap.R(7),
+ },
+ {
op: "update-aliases",
},
}
@@ -5353,8 +5685,16 @@ func (s *snapmgrTestSuite) TestUndoMountSnapFailsInCopyData(c *C) {
expected := fakeOps{
{
- op: "storesvc-snap",
- name: "some-snap",
+ op: "storesvc-snap-action",
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "some-snap",
+ Channel: "some-channel",
+ },
revno: snap.R(11),
userID: 1,
},
@@ -7404,6 +7744,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
Sequence: []*snap.SideInfo{{RealName: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1)}},
Current: snap.R(1),
SnapType: "os",
+ Channel: "beta",
})
chg := s.state.NewChange("transition-ubuntu-core", "...")
@@ -7428,8 +7769,18 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
}})
expected := fakeOps{
{
- op: "storesvc-snap",
- name: "core",
+ op: "storesvc-snap-action",
+ curSnaps: []store.CurrentSnap{
+ {Name: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1), TrackingChannel: "beta", RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)},
+ },
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "core",
+ Channel: "beta",
+ },
revno: snap.R(11),
},
{
@@ -7451,6 +7802,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
sinfo: snap.SideInfo{
RealName: "core",
SnapID: "core-id",
+ Channel: "beta",
Revision: snap.R(11),
},
},
@@ -7474,6 +7826,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
sinfo: snap.SideInfo{
RealName: "core",
SnapID: "core-id",
+ Channel: "beta",
Revision: snap.R(11),
},
},
@@ -7542,6 +7895,7 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) {
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Assert(s.fakeBackend.ops, DeepEquals, expected)
}
+
func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -7551,12 +7905,14 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) {
Sequence: []*snap.SideInfo{{RealName: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1)}},
Current: snap.R(1),
SnapType: "os",
+ Channel: "stable",
})
snapstate.Set(s.state, "core", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{{RealName: "core", SnapID: "core-snap-id", Revision: snap.R(1)}},
Current: snap.R(1),
SnapType: "os",
+ Channel: "stable",
})
chg := s.state.NewChange("transition-ubuntu-core", "...")
@@ -7577,11 +7933,6 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) {
c.Check(s.fakeStore.downloads, HasLen, 0)
expected := fakeOps{
{
- op: "storesvc-snap",
- name: "core",
- revno: snap.R(11),
- },
- {
op: "transition-ubuntu-core:Doing",
name: "ubuntu-core",
},
@@ -7623,7 +7974,6 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThroughWithCore(c *C) {
// start with an easier-to-read error if this fails:
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Assert(s.fakeBackend.ops, DeepEquals, expected)
-
}
func (s *snapmgrTestSuite) TestTransitionCoreStartsAutomatically(c *C) {
@@ -8055,16 +8405,32 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) {
expected := fakeOps{
// we check the snap
{
- op: "storesvc-snap",
- name: "some-snap",
+ op: "storesvc-snap-action",
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "some-snap",
+ Revision: snap.R(42),
+ },
revno: snap.R(42),
userID: 1,
},
// then we check core because its not installed already
// and continue with that
{
- op: "storesvc-snap",
- name: "core",
+ op: "storesvc-snap-action",
+ userID: 1,
+ },
+ {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "core",
+ Channel: "stable",
+ },
revno: snap.R(11),
userID: 1,
},
@@ -8146,7 +8512,6 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) {
name: filepath.Join(dirs.SnapBlobDir, "some-snap_42.snap"),
sinfo: snap.SideInfo{
RealName: "some-snap",
- Channel: "some-channel",
SnapID: "some-snap-id",
Revision: snap.R(42),
},
@@ -8170,7 +8535,6 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreRunThrough1(c *C) {
op: "candidate",
sinfo: snap.SideInfo{
RealName: "some-snap",
- Channel: "some-channel",
SnapID: "some-snap-id",
Revision: snap.R(42),
},
@@ -8322,7 +8686,6 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreTwoSnapsWithFailureRunThrough(c
// ensure we have both core and snap2
var snapst snapstate.SnapState
-
err = snapstate.Get(s.state, "core", &snapst)
c.Assert(err, IsNil)
c.Assert(snapst.Active, Equals, true)
@@ -8334,14 +8697,15 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreTwoSnapsWithFailureRunThrough(c
Revision: snap.R(11),
})
- err = snapstate.Get(s.state, "snap2", &snapst)
+ var snapst2 snapstate.SnapState
+ err = snapstate.Get(s.state, "snap2", &snapst2)
c.Assert(err, IsNil)
- c.Assert(snapst.Active, Equals, true)
- c.Assert(snapst.Sequence, HasLen, 1)
- c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{
+ c.Assert(snapst2.Active, Equals, true)
+ c.Assert(snapst2.Sequence, HasLen, 1)
+ c.Assert(snapst2.Sequence[0], DeepEquals, &snap.SideInfo{
RealName: "snap2",
SnapID: "snap2-id",
- Channel: "some-other-channel",
+ Channel: "",
Revision: snap.R(21),
})
@@ -8357,8 +8721,8 @@ type behindYourBackStore struct {
chg *state.Change
}
-func (s behindYourBackStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
- if spec.Name == "core" {
+func (s behindYourBackStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ if len(actions) == 1 && actions[0].Action == "install" && actions[0].Name == "core" {
s.state.Lock()
if !s.coreInstallRequested {
s.coreInstallRequested = true
@@ -8392,7 +8756,7 @@ func (s behindYourBackStore) SnapInfo(spec store.SnapSpec, user *auth.UserState)
s.state.Unlock()
}
- return s.fakeStore.SnapInfo(spec, user)
+ return s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts)
}
// this test the scenario that some-snap gets installed and during the
@@ -8414,7 +8778,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreConflictingInstall(c *C) {
// now install a snap that will pull in core
chg := s.state.NewChange("install", "install a snap on a system without core")
- ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -8471,7 +8835,7 @@ func (s *snapmgrTestSuite) TestInstallWithoutCoreConflictingInstall(c *C) {
RealName: "some-snap",
SnapID: "some-snap-id",
Channel: "some-channel",
- Revision: snap.R(42),
+ Revision: snap.R(11),
})
}
@@ -8480,9 +8844,13 @@ type contentStore struct {
state *state.State
}
-func (s contentStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) {
- info, err := s.fakeStore.SnapInfo(spec, user)
- switch spec.Name {
+func (s contentStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+ snaps, err := s.fakeStore.SnapAction(ctx, currentSnaps, actions, user, opts)
+ if len(snaps) != 1 {
+ panic("expected to be queried for install of only one snap at a time")
+ }
+ info := snaps[0]
+ switch info.Name() {
case "snap-content-plug":
info.Plugs = map[string]*snap.PlugInfo{
"some-plug": {
@@ -8564,7 +8932,7 @@ func (s contentStore) SnapInfo(spec store.SnapSpec, user *auth.UserState) (*snap
}
}
- return info, err
+ return []*snap.Info{info}, err
}
func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
@@ -8577,7 +8945,7 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
ifacerepo.Replace(s.state, repo)
chg := s.state.NewChange("install", "install a snap")
- ts, err := snapstate.Install(s.state, "snap-content-plug", "some-channel", snap.R(42), s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(s.state, "snap-content-plug", "stable", snap.R(42), s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -8590,13 +8958,27 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
c.Assert(chg.Err(), IsNil)
c.Assert(chg.IsReady(), Equals, true)
expected := fakeOps{{
- op: "storesvc-snap",
- name: "snap-content-plug",
+ op: "storesvc-snap-action",
+ userID: 1,
+ }, {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "snap-content-plug",
+ Revision: snap.R(42),
+ },
revno: snap.R(42),
userID: 1,
}, {
- op: "storesvc-snap",
- name: "snap-content-slot",
+ op: "storesvc-snap-action",
+ userID: 1,
+ }, {
+ op: "storesvc-snap-action:action",
+ action: store.SnapAction{
+ Action: "install",
+ Name: "snap-content-slot",
+ Channel: "stable",
+ },
revno: snap.R(11),
userID: 1,
}, {
@@ -8662,7 +9044,6 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
name: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_42.snap"),
sinfo: snap.SideInfo{
RealName: "snap-content-plug",
- Channel: "some-channel",
SnapID: "snap-content-plug-id",
Revision: snap.R(42),
},
@@ -8682,7 +9063,6 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
op: "candidate",
sinfo: snap.SideInfo{
RealName: "snap-content-plug",
- Channel: "some-channel",
SnapID: "snap-content-plug-id",
Revision: snap.R(42),
},
@@ -8709,7 +9089,7 @@ func (s *snapmgrTestSuite) TestInstallDefaultProviderRunThrough(c *C) {
// do a simple c.Check(ops, DeepEquals, fakeOps{...})
c.Check(len(s.fakeBackend.ops), Equals, len(expected))
for _, op := range expected {
- c.Check(s.fakeBackend.ops, testutil.DeepContains, op)
+ c.Assert(s.fakeBackend.ops, testutil.DeepContains, op)
}
}
@@ -8996,6 +9376,76 @@ func (s *snapmgrTestSuite) TestUpdateManyLayoutsChecksFeatureFlag(c *C) {
c.Assert(refreshes, DeepEquals, []string{"some-snap"})
}
+func (s *snapmgrTestSuite) TestInjectTasks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ lane := s.state.NewLane()
+
+ // setup main task and two tasks waiting for it; all part of same change
+ chg := s.state.NewChange("change", "")
+ t0 := s.state.NewTask("task1", "")
+ chg.AddTask(t0)
+ t0.JoinLane(lane)
+ t01 := s.state.NewTask("task1-1", "")
+ t01.WaitFor(t0)
+ chg.AddTask(t01)
+ t02 := s.state.NewTask("task1-2", "")
+ t02.WaitFor(t0)
+ chg.AddTask(t02)
+
+ // setup extra tasks
+ t1 := s.state.NewTask("task2", "")
+ t2 := s.state.NewTask("task3", "")
+ ts := state.NewTaskSet(t1, t2)
+
+ snapstate.InjectTasks(t0, ts)
+
+ // verify that extra tasks are now part of same change
+ c.Assert(t1.Change().ID(), Equals, t0.Change().ID())
+ c.Assert(t2.Change().ID(), Equals, t0.Change().ID())
+ c.Assert(t1.Change().ID(), Equals, chg.ID())
+
+ c.Assert(t1.Lanes(), DeepEquals, []int{lane})
+
+ // verify that halt tasks of the main task now wait for extra tasks
+ c.Assert(t1.HaltTasks(), HasLen, 2)
+ c.Assert(t2.HaltTasks(), HasLen, 2)
+ c.Assert(t1.HaltTasks(), DeepEquals, t2.HaltTasks())
+
+ ids := []string{t1.HaltTasks()[0].Kind(), t2.HaltTasks()[1].Kind()}
+ sort.Strings(ids)
+ c.Assert(ids, DeepEquals, []string{"task1-1", "task1-2"})
+
+ // verify that extra tasks wait for the main task
+ c.Assert(t1.WaitTasks(), HasLen, 1)
+ c.Assert(t1.WaitTasks()[0].Kind(), Equals, "task1")
+ c.Assert(t2.WaitTasks(), HasLen, 1)
+ c.Assert(t2.WaitTasks()[0].Kind(), Equals, "task1")
+}
+
+func (s *snapmgrTestSuite) TestInjectTasksWithNullChange(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // setup main task
+ t0 := s.state.NewTask("task1", "")
+ t01 := s.state.NewTask("task1-1", "")
+ t01.WaitFor(t0)
+
+ // setup extra task
+ t1 := s.state.NewTask("task2", "")
+ ts := state.NewTaskSet(t1)
+
+ snapstate.InjectTasks(t0, ts)
+
+ c.Assert(t1.Lanes(), DeepEquals, []int{0})
+
+ // verify that halt tasks of the main task now wait for extra tasks
+ c.Assert(t1.HaltTasks(), HasLen, 1)
+ c.Assert(t1.HaltTasks()[0].Kind(), Equals, "task1-1")
+}
+
type canDisableSuite struct{}
var _ = Suite(&canDisableSuite{})
diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go
index 12319bbbcf..76fef4c336 100644
--- a/overlord/snapstate/storehelpers.go
+++ b/overlord/snapstate/storehelpers.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -25,6 +25,7 @@ import (
"golang.org/x/net/context"
+ "github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
@@ -90,29 +91,39 @@ func userFromUserIDOrFallback(st *state.State, userID int, fallbackUser *auth.Us
return fallbackUser, nil
}
-func snapNameToID(st *state.State, name string, user *auth.UserState) (string, error) {
- theStore := Store(st)
- st.Unlock()
- info, err := theStore.SnapInfo(store.SnapSpec{Name: name}, user)
- st.Lock()
- return info.SnapID, err
-}
+func installInfo(st *state.State, name, channel string, revision snap.Revision, userID int) (*snap.Info, error) {
+ // TODO: support ignore-validation?
+
+ curSnaps, err := currentSnaps(st)
+ if err != nil {
+ return nil, err
+ }
-func snapInfo(st *state.State, name, channel string, revision snap.Revision, userID int) (*snap.Info, error) {
user, err := userFromUserID(st, userID)
if err != nil {
return nil, err
}
- theStore := Store(st)
- st.Unlock() // calls to the store should be done without holding the state lock
- spec := store.SnapSpec{
- Name: name,
- Channel: channel,
+
+ // cannot specify both with the API
+ if !revision.Unset() {
+ channel = ""
+ }
+
+ action := &store.SnapAction{
+ Action: "install",
+ Name: name,
+ // the desired channel
+ Channel: channel,
+ // the desired revision
Revision: revision,
}
- snap, err := theStore.SnapInfo(spec, user)
+
+ theStore := Store(st)
+ st.Unlock() // calls to the store should be done without holding the state lock
+ res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil)
st.Lock()
- return snap, err
+
+ return singleActionResult(name, action.Action, res, err)
}
func updateInfo(st *state.State, snapst *SnapState, opts *updateInfoOpts, userID int) (*snap.Info, error) {
@@ -120,26 +131,42 @@ func updateInfo(st *state.State, snapst *SnapState, opts *updateInfoOpts, userID
opts = &updateInfoOpts{}
}
+ curSnaps, err := currentSnaps(st)
+ if err != nil {
+ return nil, err
+ }
+
curInfo, user, err := preUpdateInfo(st, snapst, opts.amend, userID)
if err != nil {
return nil, err
}
- refreshCand := &store.RefreshCandidate{
+ var flags store.SnapActionFlags
+ if opts.ignoreValidation {
+ flags = store.SnapActionIgnoreValidation
+ } else {
+ flags = store.SnapActionEnforceValidation
+ }
+
+ action := &store.SnapAction{
+ Action: "refresh",
+ SnapID: curInfo.SnapID,
// the desired channel
- Channel: opts.channel,
- SnapID: curInfo.SnapID,
- Revision: curInfo.Revision,
- Epoch: curInfo.Epoch,
- IgnoreValidation: opts.ignoreValidation,
- Amend: opts.amend,
+ Channel: opts.channel,
+ Flags: flags,
+ }
+
+ if curInfo.SnapID == "" { // amend
+ action.Action = "install"
+ action.Name = curInfo.Name()
}
theStore := Store(st)
st.Unlock() // calls to the store should be done without holding the state lock
- res, err := theStore.LookupRefresh(refreshCand, user)
+ res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil)
st.Lock()
- return res, err
+
+ return singleActionResult(curInfo.Name(), action.Action, res, err)
}
func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (*snap.Info, *auth.UserState, error) {
@@ -157,39 +184,130 @@ func preUpdateInfo(st *state.State, snapst *SnapState, amend bool, userID int) (
if !amend {
return nil, nil, store.ErrLocalSnap
}
+ }
- // in amend mode we need to move to the store rev
- id, err := snapNameToID(st, curInfo.Name(), user)
- if err != nil {
- return nil, nil, fmt.Errorf("cannot get snap ID for %q: %v", curInfo.Name(), err)
+ return curInfo, user, nil
+}
+
+func singleActionResult(name, action string, results []*snap.Info, e error) (info *snap.Info, err error) {
+ if len(results) > 1 {
+ return nil, fmt.Errorf("internal error: multiple store results for a single snap op")
+ }
+ if len(results) > 0 {
+ // TODO: if we also have an error log/warn about it
+ return results[0], nil
+ }
+
+ if saErr, ok := e.(*store.SnapActionError); ok {
+ if len(saErr.Other) != 0 {
+ return nil, saErr
+ }
+
+ var snapErr error
+ switch action {
+ case "refresh":
+ snapErr = saErr.Refresh[name]
+ case "install":
+ snapErr = saErr.Install[name]
+ }
+ if snapErr != nil {
+ return nil, snapErr
+ }
+
+ // no result, atypical case
+ if saErr.NoResults {
+ switch action {
+ case "refresh":
+ return nil, store.ErrNoUpdateAvailable
+ case "install":
+ return nil, store.ErrSnapNotFound
+ }
}
- curInfo.SnapID = id
- // set revision to "unknown"
- curInfo.Revision = snap.R(0)
}
- return curInfo, user, nil
+ return nil, e
}
-func updateToRevisionInfo(st *state.State, snapst *SnapState, channel string, revision snap.Revision, userID int) (*snap.Info, error) {
+func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int) (*snap.Info, error) {
+ // TODO: support ignore-validation?
+
+ curSnaps, err := currentSnaps(st)
+ if err != nil {
+ return nil, err
+ }
+
curInfo, user, err := preUpdateInfo(st, snapst, false, userID)
if err != nil {
return nil, err
}
- theStore := Store(st)
- st.Unlock() // calls to the store should be done without holding the state lock
- spec := store.SnapSpec{
- Name: curInfo.Name(),
- Channel: channel,
+ action := &store.SnapAction{
+ Action: "refresh",
+ SnapID: curInfo.SnapID,
+ // the desired revision
Revision: revision,
}
- snap, err := theStore.SnapInfo(spec, user)
+
+ theStore := Store(st)
+ st.Unlock() // calls to the store should be done without holding the state lock
+ res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, nil)
st.Lock()
- return snap, err
+
+ return singleActionResult(curInfo.Name(), action.Action, res, err)
}
-func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, flags *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) {
+func currentSnaps(st *state.State) ([]*store.CurrentSnap, error) {
+ snapStates, err := All(st)
+ if err != nil {
+ return nil, err
+ }
+
+ curSnaps := collectCurrentSnaps(snapStates, nil)
+ return curSnaps, nil
+}
+
+func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store.CurrentSnap, *SnapState)) (curSnaps []*store.CurrentSnap) {
+ curSnaps = make([]*store.CurrentSnap, 0, len(snapStates))
+
+ for snapName, snapst := range snapStates {
+ if snapst.TryMode {
+ // try mode snaps are completely local and
+ // irrelevant for the operation
+ continue
+ }
+
+ snapInfo, err := snapst.CurrentInfo()
+ if err != nil {
+ continue
+ }
+
+ if snapInfo.SnapID == "" {
+ // the store won't be able to tell what this
+ // is and so cannot include it in the
+ // operation
+ continue
+ }
+
+ installed := &store.CurrentSnap{
+ Name: snapName,
+ SnapID: snapInfo.SnapID,
+ // the desired channel (not snapInfo.Channel!)
+ TrackingChannel: snapst.Channel,
+ Revision: snapInfo.Revision,
+ RefreshedDate: revisionDate(snapInfo),
+ IgnoreValidation: snapst.IgnoreValidation,
+ }
+ curSnaps = append(curSnaps, installed)
+
+ if consider != nil {
+ consider(installed, snapst)
+ }
+ }
+
+ return curSnaps
+}
+
+func refreshCandidates(ctx context.Context, st *state.State, names []string, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, map[string]*SnapState, map[string]bool, error) {
snapStates, err := All(st)
if err != nil {
return nil, nil, nil, err
@@ -204,93 +322,69 @@ func refreshCandidates(ctx context.Context, st *state.State, names []string, use
sort.Strings(names)
+ actionsByUserID := make(map[int][]*store.SnapAction)
stateByID := make(map[string]*SnapState, len(snapStates))
- candidatesInfo := make([]*store.RefreshCandidate, 0, len(snapStates))
ignoreValidation := make(map[string]bool)
- userIDs := make(map[int]bool)
- for _, snapst := range snapStates {
- if len(names) == 0 && (snapst.TryMode || snapst.DevMode) {
- // no auto-refresh for trymode nor devmode
- continue
- }
+ fallbackID := idForUser(user)
+ nCands := 0
+ addCand := func(installed *store.CurrentSnap, snapst *SnapState) {
// FIXME: snaps that are not active are skipped for now
// until we know what we want to do
if !snapst.Active {
- continue
+ return
}
- snapInfo, err := snapst.CurrentInfo()
- if err != nil {
- // log something maybe?
- continue
- }
-
- if snapInfo.SnapID == "" {
- // no refresh for sideloaded
- continue
+ if len(names) == 0 && snapst.DevMode {
+ // no auto-refresh for devmode
+ return
}
- if len(names) > 0 && !strutil.SortedListContains(names, snapInfo.Name()) {
- continue
+ if len(names) > 0 && !strutil.SortedListContains(names, installed.Name) {
+ return
}
- stateByID[snapInfo.SnapID] = snapst
-
- // get confinement preference from the snapstate
- candidateInfo := &store.RefreshCandidate{
- // the desired channel (not info.Channel!)
- Channel: snapst.Channel,
- SnapID: snapInfo.SnapID,
- Revision: snapInfo.Revision,
- Epoch: snapInfo.Epoch,
- IgnoreValidation: snapst.IgnoreValidation,
- }
+ stateByID[installed.SnapID] = snapst
if len(names) == 0 {
- candidateInfo.Block = snapst.Block()
+ installed.Block = snapst.Block()
}
- candidatesInfo = append(candidatesInfo, candidateInfo)
- if snapst.UserID != 0 {
- userIDs[snapst.UserID] = true
+ userID := snapst.UserID
+ if userID == 0 {
+ userID = fallbackID
}
+ actionsByUserID[userID] = append(actionsByUserID[userID], &store.SnapAction{
+ Action: "refresh",
+ SnapID: installed.SnapID,
+ })
if snapst.IgnoreValidation {
- ignoreValidation[snapInfo.SnapID] = true
+ ignoreValidation[installed.SnapID] = true
}
+ nCands++
}
+ // determine current snaps and collect candidates for refresh
+ curSnaps := collectCurrentSnaps(snapStates, addCand)
theStore := Store(st)
- // TODO: we query for all snaps for each user so that the
- // store can take into account validation constraints, we can
- // do better with coming APIs
- updatesInfo := make(map[string]*snap.Info, len(candidatesInfo))
- fallbackUsed := false
- fallbackID := idForUser(user)
- if len(userIDs) == 0 {
- // none of the snaps had an installed user set, just
- // use the fallbackID
- userIDs[fallbackID] = true
- }
- for userID := range userIDs {
+ updatesInfo := make(map[string]*snap.Info, nCands)
+ for userID, actions := range actionsByUserID {
u, err := userFromUserIDOrFallback(st, userID, user)
if err != nil {
return nil, nil, nil, err
}
- // consider the fallback user at most once
- if idForUser(u) == fallbackID {
- if fallbackUsed {
- continue
- }
- fallbackUsed = true
- }
st.Unlock()
- updatesForUser, err := theStore.ListRefresh(ctx, candidatesInfo, u, flags)
+ updatesForUser, err := theStore.SnapAction(ctx, curSnaps, actions, u, opts)
st.Lock()
if err != nil {
- return nil, nil, nil, err
+ saErr, ok := err.(*store.SnapActionError)
+ if !ok {
+ return nil, nil, nil, err
+ }
+ // TODO: use the warning infra here when we have it
+ logger.Noticef("%v", saErr)
}
for _, snapInfo := range updatesForUser {
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
index 3e5f352fc3..d0493fcde9 100644
--- a/packaging/arch/PKGBUILD
+++ b/packaging/arch/PKGBUILD
@@ -7,7 +7,7 @@ pkgname=snapd
pkgdesc="Service and tools for management of snap packages."
depends=('squashfs-tools' 'libseccomp' 'libsystemd')
optdepends=('bash-completion: bash completion support')
-pkgver=2.32.2.r619.g3c932a4c5
+pkgver=2.32.3.r619.g3c932a4c5
pkgrel=1
arch=('x86_64')
url="https://github.com/snapcore/snapd"
diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec
index 16adcb5a46..0a59b1e12e 100644
--- a/packaging/fedora/snapd.spec
+++ b/packaging/fedora/snapd.spec
@@ -70,7 +70,7 @@
%endif
Name: snapd
-Version: 2.32.2
+Version: 2.32.3
Release: 0%{?dist}
Summary: A transactional software package manager
Group: System Environment/Base
@@ -722,6 +722,21 @@ fi
%changelog
+* Thu Apr 05 2018 Michael Vogt <mvo@ubuntu.com>
+- New upstream release 2.32.3
+ - ifacestate: add to the repo also snaps that are pending being
+ activated but have a done setup-profiles
+ - snapstate: inject autoconnect tasks in doLinkSnap for regular
+ snaps
+ - cmd/snap-confine: allow creating missing gl32, gl, vulkan dirs
+ - errtracker: add more fields to aid debugging
+ - interfaces: make system-key more robust against invalid fstab
+ entries
+ - cmd/snap-mgmt: remove timers, udev rules, dbus policy files
+ - overlord,interfaces: be more vocal about broken snaps and read
+ errors
+ - osutil: fix fstab parser to allow for # in field values
+
* Sat Mar 31 2018 Michael Vogt <mvo@ubuntu.com>
- New upstream release 2.32.2
- interfaces/content: add rule so slot can access writable files at
diff --git a/packaging/opensuse-42.2/snapd.changes b/packaging/opensuse-42.2/snapd.changes
index c6b310bec5..24cddfed4c 100644
--- a/packaging/opensuse-42.2/snapd.changes
+++ b/packaging/opensuse-42.2/snapd.changes
@@ -1,4 +1,9 @@
-------------------------------------------------------------------
+Thu Apr 05 22:35:35 UTC 2018 - mvo@fastmail.fm
+
+- Update to upstream release 2.32.3
+
+-------------------------------------------------------------------
Sat Mar 31 21:09:29 UTC 2018 - mvo@fastmail.fm
- Update to upstream release 2.32.2
diff --git a/packaging/opensuse-42.2/snapd.spec b/packaging/opensuse-42.2/snapd.spec
index fbe79d4093..b014e30aed 100644
--- a/packaging/opensuse-42.2/snapd.spec
+++ b/packaging/opensuse-42.2/snapd.spec
@@ -32,7 +32,7 @@
%define systemd_services_list snapd.socket snapd.service
Name: snapd
-Version: 2.32.2
+Version: 2.32.3
Release: 0
Summary: Tools enabling systems to work with .snap files
License: GPL-3.0
diff --git a/packaging/ubuntu-14.04/changelog b/packaging/ubuntu-14.04/changelog
index 962b986628..805a845787 100644
--- a/packaging/ubuntu-14.04/changelog
+++ b/packaging/ubuntu-14.04/changelog
@@ -1,3 +1,21 @@
+snapd (2.32.3~14.04) trusty; urgency=medium
+
+ * New upstream release, LP: #1756173
+ - ifacestate: add to the repo also snaps that are pending being
+ activated but have a done setup-profiles
+ - snapstate: inject autoconnect tasks in doLinkSnap for regular
+ snaps
+ - cmd/snap-confine: allow creating missing gl32, gl, vulkan dirs
+ - errtracker: add more fields to aid debugging
+ - interfaces: make system-key more robust against invalid fstab
+ entries
+ - cmd/snap-mgmt: remove timers, udev rules, dbus policy files
+ - overlord,interfaces: be more vocal about broken snaps and read
+ errors
+ - osutil: fix fstab parser to allow for # in field values
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 05 Apr 2018 22:35:35 +0200
+
snapd (2.32.2~14.04) trusty; urgency=medium
* New upstream release, LP: #1756173
diff --git a/packaging/ubuntu-14.04/snapd.postrm b/packaging/ubuntu-14.04/snapd.postrm
index ff324c4fb3..c2f685a720 100644
--- a/packaging/ubuntu-14.04/snapd.postrm
+++ b/packaging/ubuntu-14.04/snapd.postrm
@@ -65,6 +65,15 @@ if [ "$1" = "purge" ]; then
rmdir --ignore-fail-on-non-empty "$d" || true
fi
done
+ # udev rules
+ find /etc/udev/rules.d -name "*-snap.${snap}.rules" -execdir rm -f "{}" \;
+ # dbus policy files
+ find /etc/dbus-1/system.d -name "snap.${snap}.*.conf" -execdir rm -f "{}" \;
+ # timer files
+ find /etc/systemd/system -name "snap.${snap}.*.timer" | while read -r f; do
+ systemctl_stop "$(basename $f)"
+ rm -f "$f"
+ done
fi
echo "Removing $unit"
diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog
index c4bc059792..f04d6b9bb6 100644
--- a/packaging/ubuntu-16.04/changelog
+++ b/packaging/ubuntu-16.04/changelog
@@ -1,3 +1,21 @@
+snapd (2.32.3) xenial; urgency=medium
+
+ * New upstream release, LP: #1756173
+ - ifacestate: add to the repo also snaps that are pending being
+ activated but have a done setup-profiles
+ - snapstate: inject autoconnect tasks in doLinkSnap for regular
+ snaps
+ - cmd/snap-confine: allow creating missing gl32, gl, vulkan dirs
+ - errtracker: add more fields to aid debugging
+ - interfaces: make system-key more robust against invalid fstab
+ entries
+ - cmd/snap-mgmt: remove timers, udev rules, dbus policy files
+ - overlord,interfaces: be more vocal about broken snaps and read
+ errors
+ - osutil: fix fstab parser to allow for # in field values
+
+ -- Michael Vogt <michael.vogt@ubuntu.com> Thu, 05 Apr 2018 22:35:35 +0200
+
snapd (2.32.2) xenial; urgency=medium
* New upstream release, LP: #1756173
diff --git a/packaging/ubuntu-16.04/gbp.conf b/packaging/ubuntu-16.04/gbp.conf
new file mode 100644
index 0000000000..7966999e11
--- /dev/null
+++ b/packaging/ubuntu-16.04/gbp.conf
@@ -0,0 +1,7 @@
+[buildpackage]
+# use a build area relative to the git repository
+export-dir = ../build-area
+# disable the since the sources are being exported first
+cleaner =
+# post export script that gets the vendored dependencies
+postexport = ./get-deps.sh
diff --git a/packaging/ubuntu-16.04/snapd.postrm b/packaging/ubuntu-16.04/snapd.postrm
index 429ed13e5e..7a4d7234ed 100644
--- a/packaging/ubuntu-16.04/snapd.postrm
+++ b/packaging/ubuntu-16.04/snapd.postrm
@@ -68,6 +68,15 @@ if [ "$1" = "purge" ]; then
rmdir --ignore-fail-on-non-empty "$d" || true
fi
done
+ # udev rules
+ find /etc/udev/rules.d -name "*-snap.${snap}.rules" -execdir rm -f "{}" \;
+ # dbus policy files
+ find /etc/dbus-1/system.d -name "snap.${snap}.*.conf" -execdir rm -f "{}" \;
+ # timer files
+ find /etc/systemd/system -name "snap.${snap}.*.timer" | while read -r f; do
+ systemctl_stop "$(basename $f)"
+ rm -f "$f"
+ done
fi
echo "Removing $unit"
diff --git a/release-tools/repack-debian-tarball.sh b/release-tools/repack-debian-tarball.sh
new file mode 100755
index 0000000000..2b7dce1cef
--- /dev/null
+++ b/release-tools/repack-debian-tarball.sh
@@ -0,0 +1,87 @@
+#!/bin/sh
+# This script is used to re-pack the "orig" tarball from the Debian package
+# into a suitable upstream release. There are two changes applied: The Debian
+# tarball contains the directory snapd.upstream/ which needs to become
+# snapd-$VERSION. The Debian tarball contains the vendor/ directory which must
+# be removed from one of those.
+#
+# Example usage, using tarball from the archive or from the image ppa:
+#
+# $ wget https://launchpad.net/ubuntu/+archive/primary/+files/snapd_2.31.2.tar.xz
+# $ wget https://launchpad.net/~snappy-dev/+archive/ubuntu/image/+files/snapd_2.32.1.tar.xz
+#
+# $ repack-debian-tarball.sh snapd_2.31.2.tar.xz
+#
+# This will produce three files that need to be added to the github release page:
+#
+# - snapd_2.31.2.no-vendor.tar.xz
+# - snapd_2.31.2.vendor.tar.xz
+# - snapd_2.31.2.only-vendor.xz
+set -ue
+
+# Get the filename from argv[1]
+debian_tarball="${1:-}"
+if [ "$debian_tarball" = "" ]; then
+ echo "Usage: repack-debian-tarball.sh <snapd-debian-tarball>"
+ exit 1
+fi
+
+if [ ! -f "$debian_tarball" ]; then
+ echo "cannot operate on $debian_tarball, no such file"
+ exit 1
+fi
+
+# Extract the upstream version from the filename.
+# For example: snapd_2.31.2.tar.xz => 2.32.2
+# NOTE: There is no dash (-) in the version because snapd is a native Debian package.
+upstream_version="$(echo "$debian_tarball" | cut -d _ -f 2 | sed -e 's/\.tar\..*//')"
+
+# Scratch directory is where the original tarball is unpacked.
+scratch_dir="$(mktemp -d)"
+cleanup() {
+ rm -rf "$scratch_dir"
+}
+trap cleanup EXIT
+
+# Unpack the original with fakeroot (to preserve ownership of files).
+fakeroot tar \
+ --auto-compress \
+ --extract \
+ --file="$debian_tarball" \
+ --directory="$scratch_dir/"
+
+# Top-level directory may be either snappy.upstream or snapd.upstream, because
+# of small differences between the release manager's laptop and desktop machines.
+if [ -d "$scratch_dir/snapd.upstream" ]; then
+ top_dir=snapd.upstream
+elif [ -d "$scratch_dir/snappy.upstream" ]; then
+ top_dir=snappy.upstream
+else
+ echo "Unexpected contents of given tarball, expected snap{py,d}.upstream/"
+ exit 1
+fi
+
+# Pack a fully copy with vendor tree
+fakeroot tar \
+ --create \
+ --transform="s/$top_dir/snapd-$upstream_version/" \
+ --file=snapd_"$upstream_version".vendor.tar.xz \
+ --auto-compress \
+ --directory="$scratch_dir/" "$top_dir"
+
+# Pack a copy without vendor tree
+fakeroot tar \
+ --create \
+ --transform="s/$top_dir/snapd-$upstream_version/" \
+ --exclude='snapd*/vendor/*' \
+ --file=snapd_"$upstream_version".no-vendor.tar.xz \
+ --auto-compress \
+ --directory="$scratch_dir/" "$top_dir"
+
+# Pack a copy of the vendor tree
+fakeroot tar \
+ --create \
+ --transform="s/$top_dir/snapd-$upstream_version/" \
+ --file=snapd_"$upstream_version".only-vendor.tar.xz \
+ --auto-compress \
+ --directory="$scratch_dir/" "$top_dir"/vendor/
diff --git a/store/errors.go b/store/errors.go
index 0d626eba1f..f2153326b3 100644
--- a/store/errors.go
+++ b/store/errors.go
@@ -182,9 +182,6 @@ var (
func translateSnapActionError(action, code, message string) error {
switch code {
case "revision-not-found":
- if action == "refresh" {
- return ErrNoUpdateAvailable
- }
return ErrRevisionNotAvailable
case "id-not-found", "name-not-found":
return ErrSnapNotFound
diff --git a/store/store.go b/store/store.go
index b43482af9c..d3cca61840 100644
--- a/store/store.go
+++ b/store/store.go
@@ -675,13 +675,14 @@ type alias struct {
type catalogItem struct {
Name string `json:"package_name"`
+ Version string `json:"version"`
Summary string `json:"summary"`
Aliases []alias `json:"aliases"`
Apps []string `json:"apps"`
}
type SnapAdder interface {
- AddSnap(snapName, summary string, commands []string) error
+ AddSnap(snapName, version, summary string, commands []string) error
}
func decodeCatalog(resp *http.Response, names io.Writer, db SnapAdder) error {
@@ -722,7 +723,7 @@ func decodeCatalog(resp *http.Response, names io.Writer, db SnapAdder) error {
commands = append(commands, snap.JoinSnapApp(v.Name, app))
}
- if err := db.AddSnap(v.Name, v.Summary, commands); err != nil {
+ if err := db.AddSnap(v.Name, v.Version, v.Summary, commands); err != nil {
return err
}
}
diff --git a/store/store_test.go b/store/store_test.go
index c0d4cec2d7..364b941daa 100644
--- a/store/store_test.go
+++ b/store/store_test.go
@@ -2855,12 +2855,14 @@ const mockNamesJSON = `
"apps": ["baz"],
"title": "a title",
"summary": "oneary plus twoary",
- "package_name": "bar"
+ "package_name": "bar",
+ "version": "2.0"
},
{
"aliases": [{"name": "meh", "target": "foo"}],
"apps": ["foo"],
- "package_name": "foo"
+ "package_name": "foo",
+ "version": "1.0"
}
]
}
@@ -2921,11 +2923,11 @@ func (s *storeTestSuite) testSnapCommands(c *C, onClassic bool) {
dump, err := advisor.DumpCommands()
c.Assert(err, IsNil)
- c.Check(dump, DeepEquals, map[string][]string{
- "foo": {"foo"},
- "bar.baz": {"bar"},
- "potato": {"bar"},
- "meh": {"bar", "foo"},
+ c.Check(dump, DeepEquals, map[string]string{
+ "foo": `[{"snap":"foo","version":"1.0"}]`,
+ "bar.baz": `[{"snap":"bar","version":"2.0"}]`,
+ "potato": `[{"snap":"bar","version":"2.0"}]`,
+ "meh": `[{"snap":"bar","version":"2.0"},{"snap":"foo","version":"1.0"}]`,
})
}
@@ -6547,7 +6549,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailable(c *C) {
c.Assert(results, HasLen, 0)
c.Check(err, DeepEquals, &SnapActionError{
Refresh: map[string]error{
- "hello-world": ErrNoUpdateAvailable,
+ "hello-world": ErrRevisionNotAvailable,
},
Install: map[string]error{
"foo": ErrRevisionNotAvailable,
diff --git a/store/storetest/storetest.go b/store/storetest/storetest.go
index 4a1fb1c93e..4ad3d1535c 100644
--- a/store/storetest/storetest.go
+++ b/store/storetest/storetest.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2017 Canonical Ltd
+ * Copyright (C) 2014-2018 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
@@ -57,6 +57,10 @@ func (Store) ListRefresh(context.Context, []*store.RefreshCandidate, *auth.UserS
panic("Store.ListRefresh not expected")
}
+func (Store) SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, *auth.UserState, *store.RefreshOptions) ([]*snap.Info, error) {
+ panic("Store.SnapAction not expected")
+}
+
func (Store) Download(context.Context, string, string, *snap.DownloadInfo, progress.Meter, *auth.UserState) error {
panic("Store.Download not expected")
}
diff --git a/tests/lib/fakestore/store/store.go b/tests/lib/fakestore/store/store.go
index 4edde0b32b..baf67acaf8 100644
--- a/tests/lib/fakestore/store/store.go
+++ b/tests/lib/fakestore/store/store.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2018 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
@@ -99,6 +99,8 @@ func NewStore(topDir, addr string, assertFallback bool) *Store {
mux.HandleFunc("/api/v1/snaps/metadata", store.bulkEndpoint)
mux.Handle("/download/", http.StripPrefix("/download/", http.FileServer(http.Dir(topDir))))
mux.HandleFunc("/api/v1/snaps/assertions/", store.assertionsEndpoint)
+ // v2
+ mux.HandleFunc("/v2/snaps/refresh", store.snapActionEndpoint)
return store
}
@@ -185,19 +187,19 @@ func snapEssentialInfo(w http.ResponseWriter, fn, snapID string, bs asserts.Back
info, err := snap.ReadInfoFromSnapFile(snapFile, nil)
if err != nil {
- http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), 400)
+ http.Error(w, fmt.Sprintf("cannot get info for: %v: %v", fn, err), 400)
return nil, errInfo
}
snapDigest, size, err := asserts.SnapFileSHA3_384(fn)
if err != nil {
- http.Error(w, fmt.Sprintf("can get digest for: %v: %v", fn, err), 400)
+ http.Error(w, fmt.Sprintf("cannot get digest for: %v: %v", fn, err), 400)
return nil, errInfo
}
snapRev, devAcct, err := findSnapRevision(snapDigest, bs)
if err != nil && !asserts.IsNotFound(err) {
- http.Error(w, fmt.Sprintf("can get info for: %v: %v", fn, err), 400)
+ http.Error(w, fmt.Sprintf("cannot get info for: %v: %v", fn, err), 400)
return nil, errInfo
}
@@ -407,7 +409,7 @@ func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) {
name := snapIDtoName[pkg.SnapID]
if name == "" {
- http.Error(w, fmt.Sprintf("unknown snapid: %q", pkg.SnapID), 400)
+ http.Error(w, fmt.Sprintf("unknown snap-id: %q", pkg.SnapID), 400)
return
}
@@ -439,7 +441,7 @@ func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) {
// should look nice
out, err := json.MarshalIndent(replyData, "", " ")
if err != nil {
- http.Error(w, fmt.Sprintf("can marshal: %v: %v", replyData, err), 400)
+ http.Error(w, fmt.Sprintf("cannot marshal: %v: %v", replyData, err), 400)
return
}
w.Write(out)
@@ -482,6 +484,153 @@ func (s *Store) collectAssertions() (asserts.Backstore, error) {
return bs, nil
}
+type currentSnap struct {
+ SnapID string `json:"snap-id"`
+ InstanceKey string `json:"instance-key"`
+}
+
+type snapAction struct {
+ Action string `json:"action"`
+ InstanceKey string `json:"instance-key"`
+ SnapID string `json:"snap-id"`
+ Name string `json:"name"`
+}
+
+type snapActionRequest struct {
+ Context []currentSnap `json:"context"`
+ Fields []string `json:"fields"`
+ Actions []snapAction `json:"actions"`
+}
+
+type snapActionResult struct {
+ Result string `json:"result"`
+ InstanceKey string `json:"instance-key"`
+ SnapID string `json:"snap-id"`
+ Name string `json:"name"`
+ Snap detailsResultV2 `json:"snap"`
+}
+
+type snapActionResultList struct {
+ Results []*snapActionResult `json:"results"`
+}
+
+type detailsResultV2 struct {
+ Architectures []string `json:"architectures"`
+ SnapID string `json:"snap-id"`
+ Name string `json:"name"`
+ Publisher struct {
+ ID string `json:"id"`
+ Username string `json:"username"`
+ } `json:"publisher"`
+ Download struct {
+ URL string `json:"url"`
+ Sha3_384 string `json:"sha3-384"`
+ Size uint64 `json:"size"`
+ } `json:"download"`
+ Version string `json:"version"`
+ Revision int `json:"revision"`
+}
+
+func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) {
+ var reqData snapActionRequest
+ var replyData snapActionResultList
+
+ decoder := json.NewDecoder(req.Body)
+ if err := decoder.Decode(&reqData); err != nil {
+ http.Error(w, fmt.Sprintf("cannot decode request body: %v", err), 400)
+ return
+ }
+
+ bs, err := s.collectAssertions()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting assertions: %v", err), 500)
+ return
+ }
+
+ var remoteStore string
+ if osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") {
+ remoteStore = "staging"
+ } else {
+ remoteStore = "production"
+ }
+ snapIDtoName, err := addSnapIDs(bs, someSnapIDtoName[remoteStore])
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting snapIDs: %v", err), 500)
+ return
+ }
+
+ snaps, err := s.collectSnaps()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("internal error collecting snaps: %v", err), 500)
+ return
+ }
+
+ actions := reqData.Actions
+ if len(actions) == 1 && actions[0].Action == "refresh-all" {
+ actions = make([]snapAction, len(reqData.Context))
+ for i, s := range reqData.Context {
+ actions[i] = snapAction{
+ Action: "refresh",
+ SnapID: s.SnapID,
+ InstanceKey: s.InstanceKey,
+ }
+ }
+ }
+
+ // check if we have downloadable snap of the given SnapID or name
+ for _, a := range actions {
+ name := a.Name
+ snapID := a.SnapID
+ if a.Action == "refresh" {
+ name = snapIDtoName[snapID]
+ }
+
+ if name == "" {
+ http.Error(w, fmt.Sprintf("unknown snap-id: %q", snapID), 400)
+ return
+ }
+
+ if fn, ok := snaps[name]; ok {
+ essInfo, err := snapEssentialInfo(w, fn, snapID, bs)
+ if essInfo == nil {
+ if err != errInfo {
+ panic(err)
+ }
+ return
+ }
+
+ res := &snapActionResult{
+ Result: a.Action,
+ InstanceKey: a.InstanceKey,
+ SnapID: essInfo.SnapID,
+ Name: essInfo.Name,
+ Snap: detailsResultV2{
+ Architectures: []string{"all"},
+ SnapID: essInfo.SnapID,
+ Name: essInfo.Name,
+ Version: essInfo.Version,
+ Revision: essInfo.Revision,
+ },
+ }
+ res.Snap.Publisher.ID = essInfo.DeveloperID
+ res.Snap.Publisher.Username = essInfo.DevelName
+ res.Snap.Download.URL = fmt.Sprintf("%s/download/%s", s.URL(), filepath.Base(fn))
+ res.Snap.Download.Sha3_384 = hexify(essInfo.Digest)
+ res.Snap.Download.Size = essInfo.Size
+ replyData.Results = append(replyData.Results, res)
+ }
+ }
+
+ // use indent because this is a development tool, output
+ // should look nice
+ out, err := json.MarshalIndent(replyData, "", " ")
+ if err != nil {
+ http.Error(w, fmt.Sprintf("cannot marshal: %v: %v", replyData, err), 400)
+ return
+ }
+ w.Write(out)
+}
+
func (s *Store) retrieveAssertion(bs asserts.Backstore, assertType *asserts.AssertionType, primaryKey []string) (asserts.Assertion, error) {
a, err := bs.Get(assertType, primaryKey, assertType.MaxSupportedFormat())
if asserts.IsNotFound(err) && s.assertFallback {
diff --git a/tests/lib/fakestore/store/store_test.go b/tests/lib/fakestore/store/store_test.go
index 32c37f211e..e4334f1e69 100644
--- a/tests/lib/fakestore/store/store_test.go
+++ b/tests/lib/fakestore/store/store_test.go
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2015 Canonical Ltd
+ * Copyright (C) 2014-2018 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
@@ -49,12 +49,12 @@ var _ = Suite(&storeTestSuite{})
var defaultAddr = "localhost:23321"
-func getSha(fn string) string {
- snapDigest, _, err := asserts.SnapFileSHA3_384(fn)
+func getSha(fn string) (string, uint64) {
+ snapDigest, size, err := asserts.SnapFileSHA3_384(fn)
if err != nil {
panic(err)
}
- return hexify(snapDigest)
+ return hexify(snapDigest), size
}
func (s *storeTestSuite) SetUpTest(c *C) {
@@ -127,6 +127,7 @@ func (s *storeTestSuite) TestDetailsEndpointWithAssertions(c *C) {
var body map[string]interface{}
c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ sha3_384, _ := getSha(snapFn)
c.Check(body, DeepEquals, map[string]interface{}{
"architecture": []interface{}{"all"},
"snap_id": "xidididididididididididididididid",
@@ -137,7 +138,7 @@ func (s *storeTestSuite) TestDetailsEndpointWithAssertions(c *C) {
"download_url": s.store.URL() + "/download/foo_7_all.snap",
"version": "7",
"revision": float64(77),
- "download_sha3_384": getSha(snapFn),
+ "download_sha3_384": sha3_384,
})
}
@@ -151,6 +152,7 @@ func (s *storeTestSuite) TestDetailsEndpoint(c *C) {
var body map[string]interface{}
c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ sha3_384, _ := getSha(snapFn)
c.Check(body, DeepEquals, map[string]interface{}{
"architecture": []interface{}{"all"},
"snap_id": "",
@@ -161,7 +163,7 @@ func (s *storeTestSuite) TestDetailsEndpoint(c *C) {
"download_url": s.store.URL() + "/download/foo_1_all.snap",
"version": "1",
"revision": float64(424242),
- "download_sha3_384": getSha(snapFn),
+ "download_sha3_384": sha3_384,
})
}
@@ -183,6 +185,7 @@ func (s *storeTestSuite) TestBulkEndpoint(c *C) {
} `json:"_embedded"`
}
c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ sha3_384, _ := getSha(snapFn)
c.Check(body.Top.Cat, DeepEquals, []map[string]interface{}{{
"architecture": []interface{}{"all"},
"snap_id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
@@ -193,7 +196,7 @@ func (s *storeTestSuite) TestBulkEndpoint(c *C) {
"download_url": s.store.URL() + "/download/test-snapd-tools_1_all.snap",
"version": "1",
"revision": float64(424242),
- "download_sha3_384": getSha(snapFn),
+ "download_sha3_384": sha3_384,
}})
}
@@ -214,6 +217,7 @@ func (s *storeTestSuite) TestBulkEndpointWithAssertions(c *C) {
} `json:"_embedded"`
}
c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ sha3_384, _ := getSha(snapFn)
c.Check(body.Top.Cat, DeepEquals, []map[string]interface{}{{
"architecture": []interface{}{"all"},
"snap_id": "xidididididididididididididididid",
@@ -224,7 +228,7 @@ func (s *storeTestSuite) TestBulkEndpointWithAssertions(c *C) {
"download_url": s.store.URL() + "/download/foo_10_all.snap",
"version": "10",
"revision": float64(99),
- "download_sha3_384": getSha(snapFn),
+ "download_sha3_384": sha3_384,
}})
}
@@ -390,3 +394,169 @@ func (s *storeTestSuite) TestAssertionsEndpointNotFound(c *C) {
c.Assert(err, IsNil)
c.Check(respObj["status"], Equals, float64(404))
}
+
+func (s *storeTestSuite) TestSnapActionEndpoint(c *C) {
+ snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
+
+ resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
+"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"stable","revision":1}],
+"actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw"}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ var body struct {
+ Results []map[string]interface{}
+ }
+ c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ c.Check(body.Results, HasLen, 1)
+ sha3_384, size := getSha(snapFn)
+ c.Check(body.Results[0], DeepEquals, map[string]interface{}{
+ "result": "refresh",
+ "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "name": "test-snapd-tools",
+ "snap": map[string]interface{}{
+ "architectures": []interface{}{"all"},
+ "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "name": "test-snapd-tools",
+ "publisher": map[string]interface{}{
+ "username": "canonical",
+ "id": "canonical",
+ },
+ "download": map[string]interface{}{
+ "url": s.store.URL() + "/download/test-snapd-tools_1_all.snap",
+ "sha3-384": sha3_384,
+ "size": float64(size),
+ },
+ "version": "1",
+ "revision": float64(424242),
+ },
+ })
+}
+
+func (s *storeTestSuite) TestSnapActionEndpointWithAssertions(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 10")
+ s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99)
+
+ resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
+"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"xidididididididididididididididid","tracking-channel":"stable","revision":1}],
+"actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"xidididididididididididididididid"}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ var body struct {
+ Results []map[string]interface{}
+ }
+ c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ c.Check(body.Results, HasLen, 1)
+ sha3_384, size := getSha(snapFn)
+ c.Check(body.Results[0], DeepEquals, map[string]interface{}{
+ "result": "refresh",
+ "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "snap-id": "xidididididididididididididididid",
+ "name": "foo",
+ "snap": map[string]interface{}{
+ "architectures": []interface{}{"all"},
+ "snap-id": "xidididididididididididididididid",
+ "name": "foo",
+ "publisher": map[string]interface{}{
+ "username": "foo-devel",
+ "id": "foo-devel-id",
+ },
+ "download": map[string]interface{}{
+ "url": s.store.URL() + "/download/foo_10_all.snap",
+ "sha3-384": sha3_384,
+ "size": float64(size),
+ },
+ "version": "10",
+ "revision": float64(99),
+ },
+ })
+}
+
+func (s *storeTestSuite) TestSnapActionEndpointRefreshAll(c *C) {
+ snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
+
+ resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
+"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"stable","revision":1}],
+"actions": [{"action":"refresh-all"}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ var body struct {
+ Results []map[string]interface{}
+ }
+ c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ c.Check(body.Results, HasLen, 1)
+ sha3_384, size := getSha(snapFn)
+ c.Check(body.Results[0], DeepEquals, map[string]interface{}{
+ "result": "refresh",
+ "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "name": "test-snapd-tools",
+ "snap": map[string]interface{}{
+ "architectures": []interface{}{"all"},
+ "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw",
+ "name": "test-snapd-tools",
+ "publisher": map[string]interface{}{
+ "username": "canonical",
+ "id": "canonical",
+ },
+ "download": map[string]interface{}{
+ "url": s.store.URL() + "/download/test-snapd-tools_1_all.snap",
+ "sha3-384": sha3_384,
+ "size": float64(size),
+ },
+ "version": "1",
+ "revision": float64(424242),
+ },
+ })
+}
+
+func (s *storeTestSuite) TestSnapActionEndpointWithAssertionsInstall(c *C) {
+ snapFn := s.makeTestSnap(c, "name: foo\nversion: 10")
+ s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99)
+
+ resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
+"context": [],
+"actions": [{"action":"install","instance-key":"foo","name":"foo"}]
+}`))
+ c.Assert(err, IsNil)
+ defer resp.Body.Close()
+
+ c.Assert(resp.StatusCode, Equals, 200)
+ var body struct {
+ Results []map[string]interface{}
+ }
+ c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil)
+ c.Check(body.Results, HasLen, 1)
+ sha3_384, size := getSha(snapFn)
+ c.Check(body.Results[0], DeepEquals, map[string]interface{}{
+ "result": "install",
+ "instance-key": "foo",
+ "snap-id": "xidididididididididididididididid",
+ "name": "foo",
+ "snap": map[string]interface{}{
+ "architectures": []interface{}{"all"},
+ "snap-id": "xidididididididididididididididid",
+ "name": "foo",
+ "publisher": map[string]interface{}{
+ "username": "foo-devel",
+ "id": "foo-devel-id",
+ },
+ "download": map[string]interface{}{
+ "url": s.store.URL() + "/download/foo_10_all.snap",
+ "sha3-384": sha3_384,
+ "size": float64(size),
+ },
+ "version": "10",
+ "revision": float64(99),
+ },
+ })
+}
diff --git a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml
index 7c08bda41f..aeab6f965f 100644
--- a/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml
+++ b/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml
@@ -113,6 +113,9 @@ apps:
home:
command: bin/run
plugs: [ home ]
+ hostname-control:
+ command: bin/run
+ plugs: [ hostname-control ]
io-ports-control:
command: bin/run
plugs: [ io-ports-control ]
diff --git a/tests/main/interfaces-opengl-nvidia/task.yaml b/tests/main/interfaces-opengl-nvidia/task.yaml
index bce0bbf642..e037f6b19a 100644
--- a/tests/main/interfaces-opengl-nvidia/task.yaml
+++ b/tests/main/interfaces-opengl-nvidia/task.yaml
@@ -22,6 +22,14 @@ prepare: |
echo "canary-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libnvidia-glcore.so.123.456
echo "canary-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/tls/libnvidia-tls.so.123.456
echo "canary-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libnvidia-tls.so.123.456
+ if [[ "$(uname -m)" == x86_64 ]]; then
+ mkdir -p /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/tls
+ echo "canary-32-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libGLX.so.0.0.1
+ echo "canary-32-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libGLX_nvidia.so.0.0.1
+ echo "canary-32-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libnvidia-glcore.so.123.456
+ echo "canary-32-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/tls/libnvidia-tls.so.123.456
+ echo "canary-32-triplet" >> /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libnvidia-tls.so.123.456
+ fi
fi
mkdir -p /usr/lib/nvidia-123/tls
echo "canary-legacy" >> /usr/lib/nvidia-123/libGLX.so.0.0.1
@@ -29,6 +37,14 @@ prepare: |
echo "canary-legacy" >> /usr/lib/nvidia-123/libnvidia-glcore.so.123.456
echo "canary-legacy" >> /usr/lib/nvidia-123/tls/libnvidia-tls.so.123.456
echo "canary-legacy" >> /usr/lib/nvidia-123/libnvidia-tls.so.123.456
+ if [[ "$(uname -m)" == x86_64 ]]; then
+ mkdir -p /usr/lib32/nvidia-123/tls
+ echo "canary-32-legacy" >> /usr/lib32/nvidia-123/libGLX.so.0.0.1
+ echo "canary-32-legacy" >> /usr/lib32/nvidia-123/libGLX_nvidia.so.0.0.1
+ echo "canary-32-legacy" >> /usr/lib32/nvidia-123/libnvidia-glcore.so.123.456
+ echo "canary-32-legacy" >> /usr/lib32/nvidia-123/tls/libnvidia-tls.so.123.456
+ echo "canary-32-legacy" >> /usr/lib32/nvidia-123/libnvidia-tls.so.123.456
+ fi
execute: |
. $TESTSLIB/dirs.sh
@@ -49,6 +65,16 @@ execute: |
snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/gl/$f" | MATCH "$expected"
done
+ if [[ "$(uname -m)" == x86_64 ]]; then
+ expected32="canary-32-legacy"
+ if [[ "$SPREAD_SYSTEM" == ubuntu-18.04-* ]]; then
+ expected32="canary-32-triplet"
+ fi
+ for f in $files; do
+ snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/gl32/$f" | MATCH "$expected32"
+ done
+ fi
+
echo "And vulkan ICD file"
snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/vulkan/icd.d/nvidia_icd.json" | MATCH canary-vulkan
@@ -63,5 +89,14 @@ restore: |
rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libnvidia-glcore.so.123.456
rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/tls/libnvidia-tls.so.123.456
rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/libnvidia-tls.so.123.456
+ if [[ "$(uname -m)" == x86_64 ]]; then
+ rm -rf /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/tls
+ rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libGLX.so.0.0.1
+ rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libGLX_nvidia.so.0.0.1
+ rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libnvidia-glcore.so.123.456
+ rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/tls/libnvidia-tls.so.123.456
+ rm -f /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH -ai386)/libnvidia-tls.so.123.456
+ fi
fi
rm -rf /usr/lib/nvidia-123
+ rm -rf /usr/lib32/nvidia-123
diff --git a/tests/main/snap-mgmt/task.yaml b/tests/main/snap-mgmt/task.yaml
index e95a03b78f..03976ed858 100644
--- a/tests/main/snap-mgmt/task.yaml
+++ b/tests/main/snap-mgmt/task.yaml
@@ -10,9 +10,18 @@ prepare: |
snap install test-snapd-tools
snap list | MATCH test-snapd-tools
+ # a snap with services
install_local test-snapd-service
snap list | MATCH test-snapd-service
+ # a snap with timers
+ install_local test-snapd-timer-service
+ snap list | MATCH test-snapd-timer-service
+
+ # a snap with DBus policy files
+ snap install test-snapd-network-status-provider
+ snap list | MATCH test-snapd-network-status-provider
+
before=$(find ${SNAP_MOUNT_DIR} -type d | wc -l)
if [ "$before" -lt 2 ]; then
echo "${SNAP_MOUNT_DIR} empty - test setup broken"
@@ -22,6 +31,11 @@ prepare: |
echo "test service is known to systemd and enabled"
systemctl list-unit-files --type service --no-legend | MATCH 'snap.test-snapd-service\..*\.service\s+enabled'
+ # expecting to find various files that snap installation produced
+ test $(find /etc/udev/rules.d -name '*-snap.*.rules' | wc -l) -gt 0
+ test $(find /etc/dbus-1/system.d -name 'snap.*.conf' | wc -l) -gt 0
+ test $(find /etc/systemd/system -name 'snap.*.timer' | wc -l) -gt 0
+
execute: |
echo "Stop snapd before purging"
systemctl stop snapd.service snapd.socket
@@ -62,3 +76,7 @@ execute: |
echo "No dangling service symlinks are left behind"
test -z "$(find /etc/systemd/system/multi-user.target.wants/ -name 'snap.test-snapd-service.*')"
+
+ test $(find /etc/udev/rules.d -name '*-snap.*.rules' | wc -l) -eq 0
+ test $(find /etc/dbus-1/system.d -name 'snap.*.conf' | wc -l) -eq 0
+ test $(find /etc/systemd/system -name 'snap.*.timer' | wc -l) -eq 0
diff --git a/tests/main/snap-system-key/task.yaml b/tests/main/snap-system-key/task.yaml
index 2fc28534a9..ae4d4fc9e9 100644
--- a/tests/main/snap-system-key/task.yaml
+++ b/tests/main/snap-system-key/task.yaml
@@ -1,5 +1,11 @@
summary: Ensure security profile re-generation works with system-key
-
+prepare: |
+ echo "Make backup of fstab"
+ cp /etc/fstab /tmp/fstab.save
+restore: |
+ echo "Restore fstab copy"
+ cp /tmp/fstab.save /etc/fstab
+ rm -f /tmp/fstab.save
execute: |
stop_snapd() {
systemctl stop snapd.service snapd.socket
@@ -64,3 +70,9 @@ execute: |
echo "system-key *not* rewriten test broken"
exit 1
fi
+
+ echo "Ensure snapd works even with invalid /etc/fstab (LP: #1760841)"
+ echo "invalid so very invalid so invalid" >> /etc/fstab
+ restart_snapd
+ echo "Ensure snap commands still work"
+ snap list