diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2018-04-10 09:15:58 +0200 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2018-04-10 09:15:58 +0200 |
| commit | ac83fbddce5c22391e5f11ce519e1bcd2ecad0f7 (patch) | |
| tree | 3aedf1c744d3b941cd2a4d2f76c60478b7136a09 | |
| parent | 2940f1c4d33a0abb5d1cf4770b8f23ce34859b55 (diff) | |
| parent | c25b7318ad3a60fdcfe1b18d1f245ebf0161503c (diff) | |
Merge remote-tracking branch 'upstream/master' into whoopsie-dupsiewhoopsie-dupsie
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 |
