From 114c84007fe146b0c246453872997e71c7cb24eb Mon Sep 17 00:00:00 2001 From: Samuele Pedroni Date: Thu, 23 Aug 2018 10:15:21 +0200 Subject: [PATCH 001/580] make InstallMany work like UpdateMany by issuing a single store request to get the installation candidates --- overlord/hookstate/ctlcmd/services_test.go | 27 ++++++----- overlord/snapstate/snapstate.go | 55 ++++++++++++++++++---- overlord/snapstate/storehelpers.go | 22 +++++++++ 3 files changed, 85 insertions(+), 19 deletions(-) diff --git a/overlord/hookstate/ctlcmd/services_test.go b/overlord/hookstate/ctlcmd/services_test.go index 18b20c3070f..a3ad444ecc9 100644 --- a/overlord/hookstate/ctlcmd/services_test.go +++ b/overlord/hookstate/ctlcmd/services_test.go @@ -48,19 +48,24 @@ type fakeStore struct { } 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" { - snapName, instanceKey := snap.SplitInstanceName(actions[0].InstanceName) - if instanceKey != "" { - panic(fmt.Sprintf("unexpected instance name %q in snap install action", actions[0].InstanceName)) + if actions[0].Action == "install" { + installs := make([]*snap.Info, 0, len(actions)) + for _, a := range actions { + snapName, instanceKey := snap.SplitInstanceName(a.InstanceName) + if instanceKey != "" { + panic(fmt.Sprintf("unexpected instance name %q in snap install action", a.InstanceName)) + } + + installs = append(installs, &snap.Info{ + SideInfo: snap.SideInfo{ + RealName: snapName, + Revision: snap.R(2), + }, + Architectures: []string{"all"}, + }) } - return []*snap.Info{{ - SideInfo: snap.SideInfo{ - RealName: snapName, - Revision: snap.R(2), - }, - Architectures: []string{"all"}, - }}, nil + return installs, nil } return []*snap.Info{{ diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 488d3f9d8f0..a9bbe00054e 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -629,24 +629,63 @@ func Install(st *state.State, name, channel string, revision snap.Revision, user // InstallMany installs everything from the given list of names. // Note that the state must be locked by the caller. 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 + toInstall := make([]string, 0, len(names)) for _, name := range names { - ts, err := Install(st, name, "", snap.R(0), userID, Flags{}) - // FIXME: is this expected behavior? - if _, ok := err.(*snap.AlreadyInstalledError); ok { + var snapst SnapState + err := Get(st, name, &snapst) + if err != nil && err != state.ErrNoState { + return nil, nil, err + } + if snapst.IsInstalled() { continue } + toInstall = append(toInstall, name) + } + + user, err := userFromUserID(st, userID) + if err != nil { + return nil, nil, err + } + + installs, err := installCandidates(st, toInstall, "stable", user) + if err != nil { + return nil, nil, err + } + + tasksets := make([]*state.TaskSet, 0, len(installs)) + for _, info := range installs { + var snapst SnapState + var flags Flags + + if err := validateInfoAndFlags(info, &snapst, flags); err != nil { + return nil, nil, err + } + if err := validateFeatureFlags(st, info); err != nil { + return nil, nil, err + } + + snapsup := &SnapSetup{ + Channel: "stable", + Base: info.Base, + Prereq: defaultContentPlugProviders(st, info), + UserID: userID, + Flags: flags.ForSnapSetup(), + DownloadInfo: &info.DownloadInfo, + SideInfo: &info.SideInfo, + Type: info.Type, + PlugsOnly: len(info.Slots) == 0, + InstanceKey: info.InstanceKey, + } + + ts, err := doInstall(st, &snapst, snapsup, 0) if err != nil { return nil, nil, err } - installed = append(installed, name) ts.JoinLane(st.NewLane()) tasksets = append(tasksets, ts) } - return installed, tasksets, nil + return toInstall, tasksets, nil } // RefreshCandidates gets a list of candidates for update diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index 2e4a81764a5..f1d0405fb66 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -403,3 +403,25 @@ func refreshCandidates(ctx context.Context, st *state.State, names []string, use return updates, stateByID, ignoreValidation, nil } + +func installCandidates(st *state.State, names []string, channel string, user *auth.UserState) ([]*snap.Info, error) { + curSnaps, err := currentSnaps(st) + if err != nil { + return nil, err + } + + actions := make([]*store.SnapAction, len(names)) + for i, name := range names { + actions[i] = &store.SnapAction{ + Action: "install", + InstanceName: name, + // the desired channel + Channel: channel, + } + } + + theStore := Store(st) + st.Unlock() // calls to the store should be done without holding the state lock + defer st.Lock() + return theStore.SnapAction(context.TODO(), curSnaps, actions, user, nil) +} From 847acef7c41ac6cfeee8fd2d4ccc06553eefb451 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 19 Sep 2018 22:04:23 +0200 Subject: [PATCH 002/580] interface: add new dotfiles interface This is a new interface called "dotfiles" (name is a strawman). It allows to get access to specific files or dirs in the users home directory. An application might for example use: ``` name: fluxctl version: 1.0 plugs: dotfiles: files [.kube/config] apps: app: command: fluxctl plugs: [dotfiles] ``` Which will allow the fluxctl snap to control the .kube config. This was requested in: https://forum.snapcraft.io/t/6948 https://forum.snapcraft.io/t/7062 This gives read/write access right now. This could be split into read/write if desired. E.g. `read-files: [.kube/config]`. --- interfaces/builtin/dotfiles.go | 92 +++++++++++++++++ interfaces/builtin/dotfiles_test.go | 151 ++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+) create mode 100644 interfaces/builtin/dotfiles.go create mode 100644 interfaces/builtin/dotfiles_test.go diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go new file mode 100644 index 00000000000..4be753a03a6 --- /dev/null +++ b/interfaces/builtin/dotfiles.go @@ -0,0 +1,92 @@ +// -*- 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 . + * + */ + +package builtin + +import ( + "fmt" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +const dotfilesSummary = `allows access to hidden files in the home directory` + +const dotfilesBaseDeclarationSlots = ` + dotfiles: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` + +const dotfilesConnectedPlugAppArmor = ` +# Description: Can access hidden files in user's $HOME. This is restricted +# because it gives file access to some of the user's $HOME. + +# Note, @{HOME} is the user's $HOME, not the snap's $HOME +` + +type dotfilesInterface struct { + commonInterface +} + +func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + hasValidAttr := false + var sl []interface{} + for _, s := range []string{"files", "dirs"} { + if err := plug.Attr(s, &sl); err == nil { + hasValidAttr = true + } + } + if !hasValidAttr { + return fmt.Errorf(`cannot add dotfiles plug without valid "files" or "dirs" attribute`) + } + + return nil +} + +func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + var files, dirs []interface{} + _ = plug.Attr("files", &files) + _ = plug.Attr("dirs", &dirs) + + spec.AddSnippet(dotfilesConnectedPlugAppArmor) + for _, file := range files { + s := fmt.Sprintf("owner @${HOME}/%s rwklix,", file) + spec.AddSnippet(s) + } + for _, dir := range dirs { + s := fmt.Sprintf("owner @${HOME}/%s/** rwklix,", dir) + spec.AddSnippet(s) + } + + return nil +} + +func init() { + registerIface(&dotfilesInterface{commonInterface{ + name: "dotfiles", + summary: dotfilesSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: dotfilesBaseDeclarationSlots, + reservedForOS: true, + }}) +} diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go new file mode 100644 index 00000000000..9e936b72f4a --- /dev/null +++ b/interfaces/builtin/dotfiles_test.go @@ -0,0 +1,151 @@ +// -*- 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 . + * + */ + +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/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type dotfilesInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.ConnectedSlot + slotInfo *snap.SlotInfo + plug *interfaces.ConnectedPlug + plugInfo *snap.PlugInfo +} + +var _ = Suite(&dotfilesInterfaceSuite{ + iface: builtin.MustInterface("dotfiles"), +}) + +func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { + const mockPlugSnapInfo = `name: other +version: 1.0 +plugs: + dotfiles: + dirs: [.dir1] + files: [.file1] +apps: + app: + command: foo + plugs: [dotfiles] +` + s.slotInfo = &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "dotfiles", + Interface: "dotfiles", + } + s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) + plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) + s.plugInfo = plugSnap.Plugs["dotfiles"] + s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) +} + +func (s *dotfilesInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "dotfiles") +} + +func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) + slot := &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "dotfiles", + Interface: "dotfiles", + } + c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, + "dotfiles slots are reserved for the core snap") +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugHappy(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [".file1"] + dirs: [".dir1"] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyFilesAttrib(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: "" +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug without valid "files" or "dirs" attribute`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyDirsAttrib(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + dirs: "" +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug without valid "files" or "dirs" attribute`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugWithBadAttrib(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + foo: bar +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug without valid "files" or "dirs" attribute`) +} + +func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { + apparmorSpec := &apparmor.Specification{} + err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.file1 rwklix,`) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.dir1/** rwklix,`) + +} + +func (s *dotfilesInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} From d5c9b62a609543faf08eb5217d11c00bfe32555d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 21 Sep 2018 19:25:16 +0200 Subject: [PATCH 003/580] tests: add dotfiles to test-snapd-policy-app-consumer --- tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml | 3 +++ 1 file changed, 3 insertions(+) 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 62ae720b44e..84a69989137 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 @@ -83,6 +83,9 @@ apps: docker-support: command: bin/run plugs: [ docker-support ] + dotfiles: + command: bin/run + plugs: [ dotfiles ] dvb: command: bin/run plugs: [ dvb ] From e4bd9e3cf7ddd33a3c0cbce177df10467f7057ae Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 21 Sep 2018 19:55:56 +0200 Subject: [PATCH 004/580] interfaces: add extra checks for dotfiles path validation --- interfaces/builtin/daemon_notify.go | 2 +- interfaces/builtin/dotfiles.go | 45 +++++++++++++++++++---- interfaces/builtin/dotfiles_test.go | 56 +++++++++++++++++++++++++++-- interfaces/builtin/utils.go | 2 ++ 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/interfaces/builtin/daemon_notify.go b/interfaces/builtin/daemon_notify.go index 02c19002805..bfc6a875127 100644 --- a/interfaces/builtin/daemon_notify.go +++ b/interfaces/builtin/daemon_notify.go @@ -64,7 +64,7 @@ func (iface *daemoNotifyInterface) AppArmorConnectedPlug(spec *apparmor.Specific // must be an absolute path or an abstract socket path return fmt.Errorf("cannot use %q as notify socket path: not absolute", notifySocket) } - illegalChars := `?*[]{}^"` + illegalChars := apparmorAARE if strings.ContainsAny(notifySocket, illegalChars) { // must not contain any AppArmor regular expression (AARE) // characters or double quotes diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 4be753a03a6..63ea4eae5ea 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -21,6 +21,9 @@ package builtin import ( "fmt" + "path/filepath" + "strings" + "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/snap" @@ -39,21 +42,51 @@ const dotfilesBaseDeclarationSlots = ` const dotfilesConnectedPlugAppArmor = ` # Description: Can access hidden files in user's $HOME. This is restricted # because it gives file access to some of the user's $HOME. - -# Note, @{HOME} is the user's $HOME, not the snap's $HOME ` type dotfilesInterface struct { commonInterface } +func validatePaths(attrName string, paths []interface{}) error { + for _, npp := range paths { + np, ok := npp.(string) + if !ok { + return fmt.Errorf("%q must be a list of strings", attrName) + } + p := filepath.Clean(np) + if p != np { + return fmt.Errorf("%q must be clean", np) + } + + if strings.Contains(p, "..") { + return fmt.Errorf(`%q contains invalid ".."`, p) + } + illegalChars := apparmorAARE + if strings.ContainsAny(p, illegalChars) { + // must not contain any AppArmor regular expression (AARE) + // characters or double quotes + return fmt.Errorf("%q contains one of %s", p, illegalChars) + } + + } + return nil +} + func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { hasValidAttr := false - var sl []interface{} - for _, s := range []string{"files", "dirs"} { - if err := plug.Attr(s, &sl); err == nil { - hasValidAttr = true + for _, att := range []string{"files", "dirs"} { + if _, ok := plug.Attrs[att]; !ok { + continue + } + il, ok := plug.Attrs[att].([]interface{}) + if !ok { + return fmt.Errorf("cannot add dotfiles plug: %q must be a list of strings", att) + } + if err := validatePaths(att, il); err != nil { + return fmt.Errorf("cannot add dotfiles plug: %s", err) } + hasValidAttr = true } if !hasValidAttr { return fmt.Errorf(`cannot add dotfiles plug without valid "files" or "dirs" attribute`) diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 9e936b72f4a..912969777c6 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -107,7 +107,59 @@ plugs: info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug without valid "files" or "dirs" attribute`) + `cannot add dotfiles plug: "files" must be a list of strings`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugWithWrongFileAttrType(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ 121 ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "files" must be a list of strings`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugWithUncleanPath(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ "./foo/./bar" ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "./foo/./bar" must be clean`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugDots(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ "../foo" ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "../foo" contains invalid ".."`) +} + +func (s *dotfilesInterfaceSuite) TestSanitizePlugAARE(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ "foo[" ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "foo\[" contains one of \?\*\[\]\{\}\^\"`) } func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyDirsAttrib(c *C) { @@ -120,7 +172,7 @@ plugs: info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug without valid "files" or "dirs" attribute`) + `cannot add dotfiles plug: "dirs" must be a list of strings`) } func (s *dotfilesInterfaceSuite) TestSanitizePlugWithBadAttrib(c *C) { diff --git a/interfaces/builtin/utils.go b/interfaces/builtin/utils.go index fd7a849af77..3bb1086ac9c 100644 --- a/interfaces/builtin/utils.go +++ b/interfaces/builtin/utils.go @@ -31,6 +31,8 @@ import ( // The maximum number of Usb bInterfaceNumber. const UsbMaxInterfaces = 32 +const apparmorAARE = `?*[]{}^"` + // AppLabelExpr returns the specification of the apparmor label describing // all the apps bound to a given slot. The result has one of three forms, // depending on how apps are bound to the slot: From bc05d9a9bc39c02c89b7247f97836e2be5a55cd3 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 24 Sep 2018 08:19:32 +0200 Subject: [PATCH 005/580] tests: split dotfiles in the test to dotfiles-{dirs,files} --- run-checks | 2 +- .../test-snapd-policy-app-consumer/meta/snap.yaml | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/run-checks b/run-checks index d2c11230600..1aff2fa5afa 100755 --- a/run-checks +++ b/run-checks @@ -105,7 +105,7 @@ missing_interface_spread_test() { # skip gadget provided interfaces for now continue ;; - dbus|content) + dbus|content|dotfiles) search="interface: $iface" ;; autopilot) 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 84a69989137..cace9b96468 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 @@ -83,9 +83,12 @@ apps: docker-support: command: bin/run plugs: [ docker-support ] - dotfiles: + dotfiles-files: command: bin/run - plugs: [ dotfiles ] + plugs: [ dotfiles-files ] + dotfiles-dirs: + command: bin/run + plugs: [ dotfiles-dirs ] dvb: command: bin/run plugs: [ dvb ] @@ -361,5 +364,11 @@ plugs: interface: dbus bus: system name: test.system + dotfiles-files: + interface: dotfiles + files: [.file1] + dotfiles-dirs: + interface: dotfiles + dirs: [.dir1] dummy: interface: dummy From 1fd26c6386febd1c56fcb02afe7ffac538ebcb64 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 25 Sep 2018 10:34:36 +0200 Subject: [PATCH 006/580] interfaces: add new apparmor.ValidateFreeFromAARE() helper --- interfaces/apparmor/apparmor.go | 11 +++++++++++ interfaces/builtin/daemon_notify.go | 7 ++----- interfaces/builtin/daemon_notify_test.go | 4 ++-- interfaces/builtin/dotfiles.go | 8 ++------ interfaces/builtin/dotfiles_test.go | 2 +- interfaces/builtin/utils.go | 2 -- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/interfaces/apparmor/apparmor.go b/interfaces/apparmor/apparmor.go index b5221f9838c..7debaf7150f 100644 --- a/interfaces/apparmor/apparmor.go +++ b/interfaces/apparmor/apparmor.go @@ -37,6 +37,17 @@ import ( "github.com/snapcore/snapd/osutil" ) +// ValidateFreeFromAARE will check that the given string does not +// contain AppArmor regular expressions (AARE) or double quotes +func ValidateFreeFromAARE(s string) error { + const AARE = `?*[]{}^"` + + if strings.ContainsAny(s, AARE) { + return fmt.Errorf("%q contains a reserved apparmor char from %s ", s, AARE) + } + return nil +} + // LoadProfile loads an apparmor profile from the given file. // // If no such profile was previously loaded then it is simply added to the kernel. diff --git a/interfaces/builtin/daemon_notify.go b/interfaces/builtin/daemon_notify.go index bfc6a875127..2376772e8c3 100644 --- a/interfaces/builtin/daemon_notify.go +++ b/interfaces/builtin/daemon_notify.go @@ -64,11 +64,8 @@ func (iface *daemoNotifyInterface) AppArmorConnectedPlug(spec *apparmor.Specific // must be an absolute path or an abstract socket path return fmt.Errorf("cannot use %q as notify socket path: not absolute", notifySocket) } - illegalChars := apparmorAARE - if strings.ContainsAny(notifySocket, illegalChars) { - // must not contain any AppArmor regular expression (AARE) - // characters or double quotes - return fmt.Errorf("cannot use %q as notify socket path: contains one of %s", notifySocket, illegalChars) + if err := apparmor.ValidateFreeFromAARE(notifySocket); err != nil { + return fmt.Errorf("cannot use %q as notify socket path: %s", notifySocket, err) } var rule string diff --git a/interfaces/builtin/daemon_notify_test.go b/interfaces/builtin/daemon_notify_test.go index a1e87f5eac6..811b8c92d38 100644 --- a/interfaces/builtin/daemon_notify_test.go +++ b/interfaces/builtin/daemon_notify_test.go @@ -158,8 +158,8 @@ func (s *daemoNotifySuite) TestAppArmorConnectedPlugNotifySocketEnvBadFormat(c * }{ {"foo/bar", `cannot use \".*\" as notify socket path: not absolute`}, {"[", `cannot use ".*" as notify socket path: not absolute`}, - {"@^", `cannot use \".*\" as notify socket path: contains one of .*`}, - {`/foo/bar"[]`, `cannot use \".*\" as notify socket path: contains one of .*`}, + {"@^", `cannot use \".*\" as notify socket path: \".*\" contains a reserved apparmor char from .*`}, + {`/foo/bar"[]`, `cannot use \".*\" as notify socket path: \".*\" contains a reserved apparmor char from .*`}, } { c.Logf("trying %d: %v", idx, tc) socketPath = tc.format diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 63ea4eae5ea..3fc9d09c140 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -62,13 +62,9 @@ func validatePaths(attrName string, paths []interface{}) error { if strings.Contains(p, "..") { return fmt.Errorf(`%q contains invalid ".."`, p) } - illegalChars := apparmorAARE - if strings.ContainsAny(p, illegalChars) { - // must not contain any AppArmor regular expression (AARE) - // characters or double quotes - return fmt.Errorf("%q contains one of %s", p, illegalChars) + if err := apparmor.ValidateFreeFromAARE(p); err != nil { + return err } - } return nil } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 912969777c6..01c7a9b42bd 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -159,7 +159,7 @@ plugs: info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "foo\[" contains one of \?\*\[\]\{\}\^\"`) + `cannot add dotfiles plug: "foo\[" contains a reserved apparmor char from .*`) } func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyDirsAttrib(c *C) { diff --git a/interfaces/builtin/utils.go b/interfaces/builtin/utils.go index 3bb1086ac9c..fd7a849af77 100644 --- a/interfaces/builtin/utils.go +++ b/interfaces/builtin/utils.go @@ -31,8 +31,6 @@ import ( // The maximum number of Usb bInterfaceNumber. const UsbMaxInterfaces = 32 -const apparmorAARE = `?*[]{}^"` - // AppLabelExpr returns the specification of the apparmor label describing // all the apps bound to a given slot. The result has one of three forms, // depending on how apps are bound to the slot: From aaa0252793185b99649d800154821abdc8a0e2a5 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 25 Sep 2018 11:11:07 +0200 Subject: [PATCH 007/580] interfaces: deal with trailing /" in the dirs attr correctly --- interfaces/builtin/dotfiles.go | 16 ++++++++++++---- interfaces/builtin/dotfiles_test.go | 17 +++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 3fc9d09c140..4c5c1b704ad 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -29,7 +29,7 @@ import ( "github.com/snapcore/snapd/snap" ) -const dotfilesSummary = `allows access to hidden files in the home directory` +const dotfilesSummary = `allows access to specific hidden files in the home directory` const dotfilesBaseDeclarationSlots = ` dotfiles: @@ -40,7 +40,7 @@ const dotfilesBaseDeclarationSlots = ` ` const dotfilesConnectedPlugAppArmor = ` -# Description: Can access hidden files in user's $HOME. This is restricted +# Description: Can access specific hidden files in user's $HOME. This is restricted # because it gives file access to some of the user's $HOME. ` @@ -54,6 +54,14 @@ func validatePaths(attrName string, paths []interface{}) error { if !ok { return fmt.Errorf("%q must be a list of strings", attrName) } + // filepath.Clean() will remove trailing "/" but we allow this + // for "paths" + if attrName == "dirs" { + last := len(np) + if np[last-1] == '/' { + np = np[:last-1] + } + } p := filepath.Clean(np) if p != np { return fmt.Errorf("%q must be clean", np) @@ -98,11 +106,11 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati spec.AddSnippet(dotfilesConnectedPlugAppArmor) for _, file := range files { - s := fmt.Sprintf("owner @${HOME}/%s rwklix,", file) + s := fmt.Sprintf("owner @${HOME}/%s rwklix,", filepath.Clean(file.(string))) spec.AddSnippet(s) } for _, dir := range dirs { - s := fmt.Sprintf("owner @${HOME}/%s/** rwklix,", dir) + s := fmt.Sprintf("owner @${HOME}/%s/** rwklix,", filepath.Clean(dir.(string))) spec.AddSnippet(s) } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 01c7a9b42bd..7b61884469d 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -47,7 +47,7 @@ func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { version: 1.0 plugs: dotfiles: - dirs: [.dir1] + dirs: [.dir1/] files: [.file1] apps: app: @@ -90,7 +90,7 @@ version: 1.0 plugs: dotfiles: files: [".file1"] - dirs: [".dir1"] + dirs: [".dir1/"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] @@ -149,6 +149,19 @@ plugs: `cannot add dotfiles plug: "../foo" contains invalid ".."`) } +func (s *dotfilesInterfaceSuite) TestSanitizePlugFilesWithTrailingSlash(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ "foo/" ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "foo/" must be clean`) +} + func (s *dotfilesInterfaceSuite) TestSanitizePlugAARE(c *C) { const mockSnapYaml = `name: dotfiles-plug-snap version: 1.0 From 9951154684a9d9bf60dfcd2526e481d7183643ee Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 25 Sep 2018 11:20:01 +0200 Subject: [PATCH 008/580] dotfiles: deal with "~" --- interfaces/builtin/dotfiles.go | 3 +++ interfaces/builtin/dotfiles_test.go | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 4c5c1b704ad..3737d038191 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -70,6 +70,9 @@ func validatePaths(attrName string, paths []interface{}) error { if strings.Contains(p, "..") { return fmt.Errorf(`%q contains invalid ".."`, p) } + if strings.Contains(p, "~") { + return fmt.Errorf(`%q contains invalid "~"`, p) + } if err := apparmor.ValidateFreeFromAARE(p); err != nil { return err } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 7b61884469d..affba9114ba 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -201,6 +201,19 @@ plugs: `cannot add dotfiles plug without valid "files" or "dirs" attribute`) } +func (s *dotfilesInterfaceSuite) TestSanitizePlugFilesWithTilde(c *C) { + const mockSnapYaml = `name: dotfiles-plug-snap +version: 1.0 +plugs: + dotfiles: + files: [ "~/foo" ] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["dotfiles"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, + `cannot add dotfiles plug: "~/foo" contains invalid "~"`) +} + func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) From e1076649471e91dd60c62138f9bab8bcecfcdbed Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 26 Sep 2018 12:38:52 +0200 Subject: [PATCH 009/580] apparmor: add \x00 check in ValidateFreeFromAARE() and tests --- interfaces/apparmor/apparmor.go | 2 +- interfaces/apparmor/apparmor_test.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/interfaces/apparmor/apparmor.go b/interfaces/apparmor/apparmor.go index 7debaf7150f..e503f105943 100644 --- a/interfaces/apparmor/apparmor.go +++ b/interfaces/apparmor/apparmor.go @@ -40,7 +40,7 @@ import ( // ValidateFreeFromAARE will check that the given string does not // contain AppArmor regular expressions (AARE) or double quotes func ValidateFreeFromAARE(s string) error { - const AARE = `?*[]{}^"` + const AARE = `?*[]{}^"` + "\x00" if strings.ContainsAny(s, AARE) { return fmt.Errorf("%q contains a reserved apparmor char from %s ", s, AARE) diff --git a/interfaces/apparmor/apparmor_test.go b/interfaces/apparmor/apparmor_test.go index fb487887bab..b71ea70e272 100644 --- a/interfaces/apparmor/apparmor_test.go +++ b/interfaces/apparmor/apparmor_test.go @@ -183,3 +183,11 @@ func (s *appArmorSuite) TestLoadedApparmorProfilesHandlesParsingErrors(c *C) { c.Assert(err, ErrorMatches, `syntax error, expected: name \(mode\)`) c.Check(profiles, IsNil) } + +func (s *appArmorSuite) TestValidateFreeFromAAREUnhappy(c *C) { + var testCases = []string{"a?", "*b", "c[c", "dd]", "e{", "f}", "g^", `h"`, "f\000", "g\x00"} + + for _, s := range testCases { + c.Check(apparmor.ValidateFreeFromAARE(s), ErrorMatches, ".* contains a reserved apparmor char from .*", Commentf("%q is not raising an error", s)) + } +} From ea0acbcc4d0ac7298ff26e0a7877612871b8f04f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 26 Sep 2018 13:13:32 +0200 Subject: [PATCH 010/580] interfaces: rework dotfiles interface to allow only strings starting with "/" and "$HOME" --- interfaces/builtin/dotfiles.go | 29 ++++- interfaces/builtin/dotfiles_test.go | 169 +++++++++------------------- 2 files changed, 82 insertions(+), 116 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 3737d038191..a88413309cd 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -62,6 +62,11 @@ func validatePaths(attrName string, paths []interface{}) error { np = np[:last-1] } } + + // FIXME: should we ensure that no other "$" is part of the string? + if !strings.HasPrefix(np, "/") && !strings.HasPrefix(np, "$HOME/") { + return fmt.Errorf(`%q must start with "/" or "$HOME"`, np) + } p := filepath.Clean(np) if p != np { return fmt.Errorf("%q must be clean", np) @@ -96,24 +101,42 @@ func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { hasValidAttr = true } if !hasValidAttr { - return fmt.Errorf(`cannot add dotfiles plug without valid "files" or "dirs" attribute`) + return fmt.Errorf(`cannot add dotfiles plug: needs valid "files" or "dirs" attribute`) } return nil } +func formatPath(ip interface{}) (string, error) { + p, ok := ip.(string) + if !ok { + return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) + } + p = strings.Replace(p, "$HOME", "@${HOME}", 1) + return filepath.Clean(p), nil +} + func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { var files, dirs []interface{} _ = plug.Attr("files", &files) _ = plug.Attr("dirs", &dirs) + errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) spec.AddSnippet(dotfilesConnectedPlugAppArmor) for _, file := range files { - s := fmt.Sprintf("owner @${HOME}/%s rwklix,", filepath.Clean(file.(string))) + f, err := formatPath(file) + if err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + s := fmt.Sprintf("owner %s rwklix,", f) spec.AddSnippet(s) } for _, dir := range dirs { - s := fmt.Sprintf("owner @${HOME}/%s/** rwklix,", filepath.Clean(dir.(string))) + d, err := formatPath(dir) + if err != nil { + return fmt.Errorf(errPrefix+"%v", err) + } + s := fmt.Sprintf("owner %s/** rwklix,", d) spec.AddSnippet(s) } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index affba9114ba..82f7af310bc 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -20,6 +20,8 @@ package builtin_test import ( + "strings" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/interfaces" @@ -47,8 +49,8 @@ func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { version: 1.0 plugs: dotfiles: - dirs: [.dir1/] - files: [.file1] + dirs: [$HOME/.dir1/, /etc/dir2] + files: [$HOME/.file1, /var/lib/file2] apps: app: command: foo @@ -69,6 +71,16 @@ func (s *dotfilesInterfaceSuite) TestName(c *C) { c.Assert(s.iface.Name(), Equals, "dotfiles") } +func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { + apparmorSpec := &apparmor.Specification{} + err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.file1 rwklix,`) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.dir1/** rwklix,`) + +} + func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) slot := &snap.SlotInfo{ @@ -89,139 +101,70 @@ func (s *dotfilesInterfaceSuite) TestSanitizePlugHappy(c *C) { version: 1.0 plugs: dotfiles: - files: [".file1"] - dirs: [".dir1/"] + files: ["$HOME/.file1"] + dirs: ["$HOME/.dir1/"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) } -func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyFilesAttrib(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - files: "" -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "files" must be a list of strings`) -} - -func (s *dotfilesInterfaceSuite) TestSanitizePlugWithWrongFileAttrType(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - files: [ 121 ] -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "files" must be a list of strings`) -} - -func (s *dotfilesInterfaceSuite) TestSanitizePlugWithUncleanPath(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - files: [ "./foo/./bar" ] -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "./foo/./bar" must be clean`) -} - -func (s *dotfilesInterfaceSuite) TestSanitizePlugDots(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - files: [ "../foo" ] -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "../foo" contains invalid ".."`) -} - -func (s *dotfilesInterfaceSuite) TestSanitizePlugFilesWithTrailingSlash(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - files: [ "foo/" ] -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "foo/" must be clean`) -} - -func (s *dotfilesInterfaceSuite) TestSanitizePlugAARE(c *C) { +func (s *dotfilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { const mockSnapYaml = `name: dotfiles-plug-snap version: 1.0 plugs: dotfiles: - files: [ "foo[" ] + $t ` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "foo\[" contains a reserved apparmor char from .*`) -} + errPrefix := `cannot add dotfiles plug: ` + var testCases = []struct { + inp string + errStr string + }{ + {`files: ""`, `"files" must be a list of strings`}, + {`files: [ 123 ]`, `"files" must be a list of strings`}, + {`files: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, + {`files: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, + {`files: [ "/foo/" ]`, `"/foo/" must be clean`}, + {`files: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, + {`dirs: ""`, `"dirs" must be a list of strings`}, + {`foo: bar`, `needs valid "files" or "dirs" attribute`}, + {`files: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, + } -func (s *dotfilesInterfaceSuite) TestSanitizePlugWithEmptyDirsAttrib(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - dirs: "" -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "dirs" must be a list of strings`) -} + for _, t := range testCases { + yml := strings.Replace(mockSnapYaml, "$t", t.inp, -1) + info := snaptest.MockInfo(c, yml, nil) + plug := info.Plugs["dotfiles"] -func (s *dotfilesInterfaceSuite) TestSanitizePlugWithBadAttrib(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap -version: 1.0 -plugs: - dotfiles: - foo: bar -` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug without valid "files" or "dirs" attribute`) + c.Check(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, errPrefix+t.errStr, Commentf("unexpected error for %q", t.inp)) + } } -func (s *dotfilesInterfaceSuite) TestSanitizePlugFilesWithTilde(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap +func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { + const mockPlugSnapInfo = `name: other version: 1.0 plugs: dotfiles: - files: [ "~/foo" ] + files: [ 123 , 345 ] +apps: + app: + command: foo + plugs: [dotfiles] ` - info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, - `cannot add dotfiles plug: "~/foo" contains invalid "~"`) -} + s.slotInfo = &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "dotfiles", + Interface: "dotfiles", + } + s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) + plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) + s.plugInfo = plugSnap.Plugs["dotfiles"] + s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) -func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) - c.Assert(err, IsNil) - c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.file1 rwklix,`) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.dir1/** rwklix,`) - + c.Assert(err, ErrorMatches, `cannot connect plug dotfiles: 123 \(int64\) is not a string`) } func (s *dotfilesInterfaceSuite) TestInterfaces(c *C) { From 726c3b5a8dd8f57e8866329101da180d39a831aa Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 26 Sep 2018 15:55:15 +0200 Subject: [PATCH 011/580] dotfiles: use correct apparmor permissions --- interfaces/builtin/dotfiles.go | 12 ++++++++---- interfaces/builtin/dotfiles_test.go | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index a88413309cd..843f2e45c00 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -112,8 +112,12 @@ func formatPath(ip interface{}) (string, error) { if !ok { return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) } - p = strings.Replace(p, "$HOME", "@${HOME}", 1) - return filepath.Clean(p), nil + prefix := "" + if strings.Count(p, "$HOME") > 0 { + p = strings.Replace(p, "$HOME", "@${HOME}", 1) + prefix = "owner " + } + return filepath.Clean(prefix + p), nil } func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { @@ -128,7 +132,7 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati if err != nil { return fmt.Errorf(errPrefix+"%v", err) } - s := fmt.Sprintf("owner %s rwklix,", f) + s := fmt.Sprintf("%s rwkl,", f) spec.AddSnippet(s) } for _, dir := range dirs { @@ -136,7 +140,7 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati if err != nil { return fmt.Errorf(errPrefix+"%v", err) } - s := fmt.Sprintf("owner %s/** rwklix,", d) + s := fmt.Sprintf("%s/** rwkl,", d) spec.AddSnippet(s) } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 82f7af310bc..8378ce6faec 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -78,7 +78,8 @@ func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.file1 rwklix,`) c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.dir1/** rwklix,`) - + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `/var/lib/file2 rwklix,`) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `/etc/dir2/** rwklix,`) } func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { From d5298ae335c28b9a6098ecd8de08dc289d1a5a0f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 26 Sep 2018 16:12:27 +0200 Subject: [PATCH 012/580] dotfiles: split into read/write paths, use trailing "/" to denote dirs --- interfaces/builtin/dotfiles.go | 51 ++++++++++++++++------------- interfaces/builtin/dotfiles_test.go | 43 ++++++++++++++---------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 843f2e45c00..6bfc4781c73 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -29,7 +29,7 @@ import ( "github.com/snapcore/snapd/snap" ) -const dotfilesSummary = `allows access to specific hidden files in the home directory` +const dotfilesSummary = `allows access to files or directories` const dotfilesBaseDeclarationSlots = ` dotfiles: @@ -40,8 +40,8 @@ const dotfilesBaseDeclarationSlots = ` ` const dotfilesConnectedPlugAppArmor = ` -# Description: Can access specific hidden files in user's $HOME. This is restricted -# because it gives file access to some of the user's $HOME. +# Description: Can access specific files or directories. +# This is restricted because it gives file access to arbitrary locations. ` type dotfilesInterface struct { @@ -55,15 +55,11 @@ func validatePaths(attrName string, paths []interface{}) error { return fmt.Errorf("%q must be a list of strings", attrName) } // filepath.Clean() will remove trailing "/" but we allow this - // for "paths" - if attrName == "dirs" { - last := len(np) - if np[last-1] == '/' { - np = np[:last-1] - } + // to differentiate between files or dirs + last := len(np) + if np[last-1] == '/' { + np = np[:last-1] } - - // FIXME: should we ensure that no other "$" is part of the string? if !strings.HasPrefix(np, "/") && !strings.HasPrefix(np, "$HOME/") { return fmt.Errorf(`%q must start with "/" or "$HOME"`, np) } @@ -71,7 +67,6 @@ func validatePaths(attrName string, paths []interface{}) error { if p != np { return fmt.Errorf("%q must be clean", np) } - if strings.Contains(p, "..") { return fmt.Errorf(`%q contains invalid ".."`, p) } @@ -87,7 +82,7 @@ func validatePaths(attrName string, paths []interface{}) error { func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { hasValidAttr := false - for _, att := range []string{"files", "dirs"} { + for _, att := range []string{"read", "write"} { if _, ok := plug.Attrs[att]; !ok { continue } @@ -101,7 +96,7 @@ func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { hasValidAttr = true } if !hasValidAttr { - return fmt.Errorf(`cannot add dotfiles plug: needs valid "files" or "dirs" attribute`) + return fmt.Errorf(`cannot add dotfiles plug: needs valid "read" or "write" attribute`) } return nil @@ -121,26 +116,36 @@ func formatPath(ip interface{}) (string, error) { } func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - var files, dirs []interface{} - _ = plug.Attr("files", &files) - _ = plug.Attr("dirs", &dirs) + var reads, writes []interface{} + _ = plug.Attr("read", &reads) + _ = plug.Attr("write", &writes) errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) spec.AddSnippet(dotfilesConnectedPlugAppArmor) - for _, file := range files { - f, err := formatPath(file) + for _, r := range reads { + p, err := formatPath(r) if err != nil { return fmt.Errorf(errPrefix+"%v", err) } - s := fmt.Sprintf("%s rwkl,", f) + var s string + if strings.HasSuffix(p, "/") { + s = fmt.Sprintf("%s/** rkl,", p) + } else { + s = fmt.Sprintf("%s rkl,", p) + } spec.AddSnippet(s) } - for _, dir := range dirs { - d, err := formatPath(dir) + for _, w := range writes { + p, err := formatPath(w) if err != nil { return fmt.Errorf(errPrefix+"%v", err) } - s := fmt.Sprintf("%s/** rwkl,", d) + var s string + if strings.HasSuffix(p, "/") { + s = fmt.Sprintf("%s/** rwkl,", p) + } else { + s = fmt.Sprintf("%s rwkl,", p) + } spec.AddSnippet(s) } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 8378ce6faec..6cd3f8f7477 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -49,8 +49,8 @@ func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { version: 1.0 plugs: dotfiles: - dirs: [$HOME/.dir1/, /etc/dir2] - files: [$HOME/.file1, /var/lib/file2] + read: [$HOME/.read-dir1/, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] + write: [$HOME/.write-dir1/, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] apps: app: command: foo @@ -76,10 +76,18 @@ func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.file1 rwklix,`) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `owner @${HOME}/.dir1/** rwklix,`) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `/var/lib/file2 rwklix,`) - c.Check(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, `/etc/dir2/** rwklix,`) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` +# Description: Can access specific files or directories. +# This is restricted because it gives file access to arbitrary locations. + +/etc/read-dir2 rkl, +/etc/read-file2 rkl, +/etc/write-dir2 rwkl, +/etc/write-file2 rwkl, +owner @${HOME}/.read-dir1 rkl, +owner @${HOME}/.read-file2 rkl, +owner @${HOME}/.write-dir1 rwkl, +owner @${HOME}/.write-file2 rwkl,`) } func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { @@ -102,8 +110,8 @@ func (s *dotfilesInterfaceSuite) TestSanitizePlugHappy(c *C) { version: 1.0 plugs: dotfiles: - files: ["$HOME/.file1"] - dirs: ["$HOME/.dir1/"] + read: ["$HOME/.file1"] + write: ["$HOME/.dir1/"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] @@ -122,15 +130,14 @@ plugs: inp string errStr string }{ - {`files: ""`, `"files" must be a list of strings`}, - {`files: [ 123 ]`, `"files" must be a list of strings`}, - {`files: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, - {`files: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, - {`files: [ "/foo/" ]`, `"/foo/" must be clean`}, - {`files: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, - {`dirs: ""`, `"dirs" must be a list of strings`}, - {`foo: bar`, `needs valid "files" or "dirs" attribute`}, - {`files: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, + {`read: ""`, `"read" must be a list of strings`}, + {`read: [ 123 ]`, `"read" must be a list of strings`}, + {`read: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, + {`read: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, + {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, + {`write: ""`, `"write" must be a list of strings`}, + {`write: bar`, `"write" must be a list of strings`}, + {`read: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, } for _, t := range testCases { @@ -147,7 +154,7 @@ func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { version: 1.0 plugs: dotfiles: - files: [ 123 , 345 ] + read: [ 123 , 345 ] apps: app: command: foo From 8cf0b859d41279c21c6e9eb7cabf900fc50493ee Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 27 Sep 2018 09:35:41 +0200 Subject: [PATCH 013/580] dotfiles: refactor to avoid code duplication --- interfaces/builtin/dotfiles.go | 46 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 6bfc4781c73..0869b842d72 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -112,7 +112,22 @@ func formatPath(ip interface{}) (string, error) { p = strings.Replace(p, "$HOME", "@${HOME}", 1) prefix = "owner " } - return filepath.Clean(prefix + p), nil + return prefix + filepath.Clean(p), nil +} + +func addSnippet(spec *apparmor.Specification, perm string, paths []interface{}) error { + for _, rp := range paths { + p, err := formatPath(rp) + if err != nil { + return err + } + if strings.HasSuffix(p, "/") { + spec.AddSnippet(fmt.Sprintf("%s/** %s", p, perm)) + } else { + spec.AddSnippet(fmt.Sprintf("%s %s", p, perm)) + } + } + return nil } func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { @@ -122,33 +137,12 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) spec.AddSnippet(dotfilesConnectedPlugAppArmor) - for _, r := range reads { - p, err := formatPath(r) - if err != nil { - return fmt.Errorf(errPrefix+"%v", err) - } - var s string - if strings.HasSuffix(p, "/") { - s = fmt.Sprintf("%s/** rkl,", p) - } else { - s = fmt.Sprintf("%s rkl,", p) - } - spec.AddSnippet(s) + if err := addSnippet(spec, "rkl,", reads); err != nil { + return fmt.Errorf(errPrefix+"%v", err) } - for _, w := range writes { - p, err := formatPath(w) - if err != nil { - return fmt.Errorf(errPrefix+"%v", err) - } - var s string - if strings.HasSuffix(p, "/") { - s = fmt.Sprintf("%s/** rwkl,", p) - } else { - s = fmt.Sprintf("%s rwkl,", p) - } - spec.AddSnippet(s) + if err := addSnippet(spec, "rwkl,", writes); err != nil { + return fmt.Errorf(errPrefix+"%v", err) } - return nil } From e02c1d6f3331482f9a99d133f5f21488979a3a8a Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 24 Sep 2018 16:37:16 -0300 Subject: [PATCH 014/580] Moving core-snap-refresh-on-core test from main to nested suite This is done because it is set as manual in the main suite and it is failing when the core is refreshed. By moving the test to the nested suite, it is easier to run it and control the reboots which are currently failing. Currently it is failing because after the revert (from edge to stable), the installed core is the stable one and the tracking is edge, which is wrong and makes the test fail. See the error log: https://paste.ubuntu.com/p/vw7DsjxZhk/ --- tests/lib/nested.sh | 17 +++ .../main/core-snap-refresh-on-core/task.yaml | 123 ------------------ .../core-snap-refresh-on-core/task.yaml | 67 ++++++++++ 3 files changed, 84 insertions(+), 123 deletions(-) delete mode 100644 tests/main/core-snap-refresh-on-core/task.yaml create mode 100644 tests/nested/core-snap-refresh-on-core/task.yaml diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index 36ada69cd24..05afdca04ed 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -15,6 +15,18 @@ wait_for_ssh(){ done } +wait_for_no_ssh(){ + retry=150 + while execute_remote true; do + retry=$(( retry - 1 )) + if [ $retry -le 0 ]; then + echo "Timed out waiting for no ssh. Aborting!" + return 1 + fi + sleep 1 + done +} + prepare_ssh(){ execute_remote "sudo adduser --extrausers --quiet --disabled-password --gecos '' test" execute_remote "echo test:ubuntu | sudo chpasswd" @@ -79,3 +91,8 @@ destroy_nested_core_vm(){ execute_remote(){ sshpass -p ubuntu ssh -p 8022 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no user1@localhost "$*" } + +get_nested_core_revision(){ + ID=$1 + execute_remote "snap info core" | awk "/${ID}: / {print(\$3)}" | sed -e 's/(\(.*\))/\1/' +} \ No newline at end of file diff --git a/tests/main/core-snap-refresh-on-core/task.yaml b/tests/main/core-snap-refresh-on-core/task.yaml deleted file mode 100644 index 5d0dc230f74..00000000000 --- a/tests/main/core-snap-refresh-on-core/task.yaml +++ /dev/null @@ -1,123 +0,0 @@ -summary: Check that the core snap can be refreshed on a core device - -details: | - This test checks that the core snap can be refreshed from an installed - revision to a new one. It expects to find a new snap revision in the - channel pointed by the NEW_CORE_CHANNEL env var. - -systems: [ubuntu-core-16-64] - -manual: true - -prepare: | - snap install test-snapd-tools - -restore: | - rm -f prevBoot nextBoot - rm -f core_*.{assert,snap} - -execute: | - wait_core_pre_boot() { - chg_id="$1" - - # save change id to wait later or abort - echo "${chg_id}" >curChg - - # wait for the link task to be done - while ! snap change "${chg_id}"|grep -q "^Done.*Make snap.*available to the system" ; do sleep 1 ; done - - } - wait_core_post_boot() { - # booted - while [ "$(bootenv snap_mode)" != "" ]; do - sleep 1 - done - # and change fully done - while ! snap changes | grep "^$(cat curChg).* Done "; do - sleep 1; - done - } - - if [ "$NEW_CORE_CHANNEL" = "" ]; then - echo "please set the SPREAD_NEW_CORE_CHANNEL environment" - exit 1 - fi - - #shellcheck source=tests/lib/boot.sh - . "$TESTSLIB"/boot.sh - if [ "$SPREAD_REBOOT" = 0 ]; then - # ensure we have a good starting place - - # sanity - test-snapd-tools.echo hello | MATCH hello - - # go to known good starting place - snap download core "--${CORE_CHANNEL}" - snap ack core_*.assert - wait_core_pre_boot "$(snap install --no-wait core_*.snap)" - REBOOT - - elif [ "$SPREAD_REBOOT" = 1 ]; then - # from our good starting place we refresh - - wait_core_post_boot - - # save current core revision - snap list | awk "/^core / {print(\$3)}" > prevBoot - - # refresh - wait_core_pre_boot "$(snap refresh core "--${NEW_CORE_CHANNEL}" --no-wait)" - - # check boot env vars - snap list | awk "/^core / {print(\$3)}" > nextBoot - - test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" - test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" - - # there are no errors in the changes list - ! snap changes | MATCH '^[0-9]+ +Error' - - # test-snapd-tools works - test-snapd-tools.echo hello | MATCH hello - - REBOOT - elif [ "$SPREAD_REBOOT" = 2 ]; then - # after refresh to NEW_CHANNEL - - wait_core_post_boot - - # check boot env vars - test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" - test "$(bootenv snap_try_core)" = "" - - # and there are no errors in the changes list - ! snap changes | MATCH '^[0-9]+ +Error' - - # test-snapd-tools works - test-snapd-tools.echo hello | MATCH hello - - # revert core - wait_core_pre_boot "$(snap revert core --no-wait)" - - test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" - test "$(bootenv snap_try_core)" = "core_$(cat prevBoot).snap" - - # there are no errors in the changes list - ! snap changes | MATCH '^[0-9]+ +Error' - - REBOOT - elif [ "$SPREAD_REBOOT" = 3 ]; then - # after revert - - wait_core_post_boot - - # check that we reverted - test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" - test "$(bootenv snap_try_core)" = "" - - # and there are no errors in the changes list - ! snap changes | MATCH '^[0-9]+ +Error' - - # test-snapd-tools works - test-snapd-tools.echo hello | MATCH hello - fi diff --git a/tests/nested/core-snap-refresh-on-core/task.yaml b/tests/nested/core-snap-refresh-on-core/task.yaml new file mode 100644 index 00000000000..d1563dbfe56 --- /dev/null +++ b/tests/nested/core-snap-refresh-on-core/task.yaml @@ -0,0 +1,67 @@ +summary: Check that the core snap can be refreshed on a core device + +details: | + This test checks that the core snap can be refreshed from an installed + revision to a new one. It expects to find a new snap revision in the + channel pointed by the NEW_CORE_CHANNEL env var. + +prepare: | + . "$TESTSLIB/nested.sh" + create_nested_core_vm + +restore: | + rm -f prevBoot nextBoot + rm -f core_*.{assert,snap} + +execute: | + . "$TESTSLIB/nested.sh" + + if [ "$NEW_CORE_CHANNEL" = "" ]; then + echo "please set the SPREAD_NEW_CORE_CHANNEL environment" + exit 1 + fi + + INITIAL_REV="$(get_nested_core_revision ${CORE_CHANNEL})" + NEW_REV="$(get_nested_core_revision ${NEW_CORE_CHANNEL})" + + # Install test snap + execute_remote "snap install test-snapd-tools" + + # Ensure we have a good starting place + execute_remote "test-snapd-tools.echo hello" | MATCH hello + + # go to known good starting place + execute_remote "snap download core --${CORE_CHANNEL}" + execute_remote "snap ack core_*.assert" + execute_remote "snap install core_*.snap" + + # Check the initial core is installed and snaps can be executed + test "$(get_nested_core_revision installed)" = "${INITIAL_REV}" + execute_remote "snap info core" | MATCH "tracking: *${CORE_CHANNEL}" + + # Ensure test-snapd-tools works + execute_remote "test-snapd-tools.echo hello" | MATCH hello + + # Refresh + execute_remote "snap refresh core --${NEW_CORE_CHANNEL}" + wait_for_no_ssh + wait_for_ssh + + # After refresh, check new core is installed + test "$(get_nested_core_revision installed)" = "${NEW_REV}" + execute_remote "snap info core" | MATCH "tracking: *${NEW_CORE_CHANNEL}" + + # Ensure test-snapd-tools works + execute_remote "test-snapd-tools.echo hello" | MATCH hello + + # Revert core + execute_remote "snap revert core" || true + wait_for_no_ssh + wait_for_ssh + + # After revert, check initial core is installed + test "$(get_nested_core_revision installed)" = "${INITIAL_REV}" + execute_remote "snap info core" | MATCH "tracking: *${CORE_CHANNEL}" + + # Ensure test-snapd-tools works + execute_remote "test-snapd-tools.echo hello" | MATCH hello From aec12a1c5603c6e20cb0b09a629e8c96622298d6 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Sat, 29 Sep 2018 17:32:53 -0300 Subject: [PATCH 015/580] New line on nested.sh --- tests/lib/nested.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index 05afdca04ed..df455f3aa92 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -95,4 +95,4 @@ execute_remote(){ get_nested_core_revision(){ ID=$1 execute_remote "snap info core" | awk "/${ID}: / {print(\$3)}" | sed -e 's/(\(.*\))/\1/' -} \ No newline at end of file +} From a93415f96f6590a19bd803f00785c1eaea10bfa2 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 5 Oct 2018 09:09:08 +0200 Subject: [PATCH 016/580] address review feedback (thanks jdstrand!) --- interfaces/builtin/dotfiles.go | 21 ++++++++------------- interfaces/builtin/dotfiles_test.go | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 0869b842d72..fcfcd64bc8b 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -54,11 +54,8 @@ func validatePaths(attrName string, paths []interface{}) error { if !ok { return fmt.Errorf("%q must be a list of strings", attrName) } - // filepath.Clean() will remove trailing "/" but we allow this - // to differentiate between files or dirs - last := len(np) - if np[last-1] == '/' { - np = np[:last-1] + if strings.HasSuffix(np, "/") { + return fmt.Errorf(`%q cannot end with "/"`, np) } if !strings.HasPrefix(np, "/") && !strings.HasPrefix(np, "$HOME/") { return fmt.Errorf(`%q must start with "/" or "$HOME"`, np) @@ -109,10 +106,12 @@ func formatPath(ip interface{}) (string, error) { } prefix := "" if strings.Count(p, "$HOME") > 0 { - p = strings.Replace(p, "$HOME", "@${HOME}", 1) + p = strings.Replace(p, "$HOME", "@{HOME}", 1) prefix = "owner " } - return prefix + filepath.Clean(p), nil + p += "{,/,/**}" + + return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil } func addSnippet(spec *apparmor.Specification, perm string, paths []interface{}) error { @@ -121,11 +120,7 @@ func addSnippet(spec *apparmor.Specification, perm string, paths []interface{}) if err != nil { return err } - if strings.HasSuffix(p, "/") { - spec.AddSnippet(fmt.Sprintf("%s/** %s", p, perm)) - } else { - spec.AddSnippet(fmt.Sprintf("%s %s", p, perm)) - } + spec.AddSnippet(fmt.Sprintf("%s %s", p, perm)) } return nil } @@ -137,7 +132,7 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) spec.AddSnippet(dotfilesConnectedPlugAppArmor) - if err := addSnippet(spec, "rkl,", reads); err != nil { + if err := addSnippet(spec, "rk,", reads); err != nil { return fmt.Errorf(errPrefix+"%v", err) } if err := addSnippet(spec, "rwkl,", writes); err != nil { diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 6cd3f8f7477..15f1ee2dac3 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -49,8 +49,8 @@ func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { version: 1.0 plugs: dotfiles: - read: [$HOME/.read-dir1/, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] - write: [$HOME/.write-dir1/, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] + read: [$HOME/.read-dir1, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] + write: [$HOME/.write-dir1, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] apps: app: command: foo @@ -80,14 +80,14 @@ func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { # Description: Can access specific files or directories. # This is restricted because it gives file access to arbitrary locations. -/etc/read-dir2 rkl, -/etc/read-file2 rkl, -/etc/write-dir2 rwkl, -/etc/write-file2 rwkl, -owner @${HOME}/.read-dir1 rkl, -owner @${HOME}/.read-file2 rkl, -owner @${HOME}/.write-dir1 rwkl, -owner @${HOME}/.write-file2 rwkl,`) +"/etc/read-dir2{,/,/**}" rk, +"/etc/read-file2{,/,/**}" rk, +"/etc/write-dir2{,/,/**}" rwkl, +"/etc/write-file2{,/,/**}" rwkl, +owner "@{HOME}/.read-dir1{,/,/**}" rk, +owner "@{HOME}/.read-file2{,/,/**}" rk, +owner "@{HOME}/.write-dir1{,/,/**}" rwkl, +owner "@{HOME}/.write-file2{,/,/**}" rwkl,`) } func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { @@ -111,7 +111,7 @@ version: 1.0 plugs: dotfiles: read: ["$HOME/.file1"] - write: ["$HOME/.dir1/"] + write: ["$HOME/.dir1"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["dotfiles"] From 69f54f4aba68a8554ab7e5fdfd0af675d234bd56 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 10 Oct 2018 13:19:38 -0300 Subject: [PATCH 017/580] Add debug info to main suite --- spread.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spread.yaml b/spread.yaml index 43370141134..e55d0d99abb 100644 --- a/spread.yaml +++ b/spread.yaml @@ -453,6 +453,9 @@ suites: summary: Full-system tests for snapd prepare: | "$TESTSLIB"/prepare-restore.sh --prepare-suite + debug: | + systemctl status snapd.socket || true + journalctl -xe prepare-each: | "$TESTSLIB"/prepare-restore.sh --prepare-suite-each restore-each: | From f9c0523fd1dbc041eacb163069f96acb908a672a Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Fri, 12 Oct 2018 16:00:14 +0000 Subject: [PATCH 018/580] interfaces/docker-support: add unsafe change_profile rules --- interfaces/builtin/docker_support.go | 32 +++++++++++-- interfaces/builtin/docker_support_test.go | 56 +++++++++++++++++++++++ interfaces/builtin/export_test.go | 8 ++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index bc4975c9c3d..0d08d5e243e 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.go @@ -21,10 +21,12 @@ package builtin import ( "fmt" + "regexp" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) @@ -124,7 +126,7 @@ pivot_root, /sys/kernel/security/apparmor/{,**} r, # use 'privileged-containers: true' to support --security-opts -change_profile -> docker-default, +###CHANGEPROFILE_DOCKERDEFAULT### signal (send) peer=docker-default, ptrace (read, trace) peer=docker-default, @@ -529,7 +531,7 @@ const dockerSupportPrivilegedAppArmor = ` # These rules are here to allow Docker to launch unconfined containers but # allow the docker daemon itself to go unconfined. Since it runs as root, this # grants device ownership. -change_profile -> *, +###CHANGEPROFILE_PRIVILEGED### signal (send) peer=unconfined, ptrace (read, trace) peer=unconfined, @@ -568,12 +570,34 @@ func (iface *dockerSupportInterface) StaticInfo() interfaces.StaticInfo { } } +var ( + cpDockerDefaultPattern = regexp.MustCompile("(###CHANGEPROFILE_DOCKERDEFAULT###)") + cpPrivilegedPattern = regexp.MustCompile("(###CHANGEPROFILE_PRIVILEGED###)") + parserFeatures = release.AppArmorParserFeatures +) + func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { var privileged bool _ = plug.Attr("privileged-containers", &privileged) - spec.AddSnippet(dockerSupportConnectedPlugAppArmor) + useUnsafe := false + for _, f := range parserFeatures() { + if f == "unsafe" { + useUnsafe = true + } + } + rule := "change_profile -> docker-default," + if useUnsafe { + rule = "change_profile unsafe /** -> docker-default," + } + snippet := cpDockerDefaultPattern.ReplaceAllString(dockerSupportConnectedPlugAppArmor, rule) + spec.AddSnippet(snippet) if privileged { - spec.AddSnippet(dockerSupportPrivilegedAppArmor) + rule = "change_profile -> *," + if useUnsafe { + rule = "change_profile unsafe /**," + } + snippet = cpPrivilegedPattern.ReplaceAllString(dockerSupportPrivilegedAppArmor, rule) + spec.AddSnippet(snippet) } spec.UsesPtraceTrace() return nil diff --git a/interfaces/builtin/docker_support_test.go b/interfaces/builtin/docker_support_test.go index 8814a194d35..bb7b568b591 100644 --- a/interfaces/builtin/docker_support_test.go +++ b/interfaces/builtin/docker_support_test.go @@ -106,6 +106,9 @@ func (s *DockerSupportInterfaceSuite) TestSanitizePlug(c *C) { } func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedTrue(c *C) { + restore := builtin.MockParserFeatures(func() []string { return []string{} }) + defer restore() + var mockSnapYaml = []byte(`name: docker version: 1.0 plugs: @@ -186,6 +189,59 @@ plugs: c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "docker-support plug requires bool with 'privileged-containers'") } +func (s *DockerSupportInterfaceSuite) TestAppArmorUnsafe(c *C) { + var mockSnapYaml = []byte(`name: docker +version: 1.0 +plugs: + privileged: + interface: docker-support + privileged-containers: true +apps: + app: + command: foo + plugs: + - privileged +`) + + type changeProfileScenario struct { + features []string + expected []string + } + + var changeProfileScenarios = []changeProfileScenario{{ + features: []string{}, + expected: []string{ + "change_profile -> docker-default,", + "change_profile -> *,", + }, + }, { + features: []string{"unsafe"}, + expected: []string{ + "change_profile unsafe /** -> docker-default,", + "change_profile unsafe /**,", + }, + }} + + info, err := snap.InfoFromSnapYaml(mockSnapYaml) + c.Assert(err, IsNil) + + plug := info.Plugs["privileged"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) + + for _, scenario := range changeProfileScenarios { + restore := builtin.MockParserFeatures(func() []string { return scenario.features }) + defer restore() + + apparmorSpec := &apparmor.Specification{} + err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"}) + for _, rule := range scenario.expected { + c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), testutil.Contains, rule) + } + } +} + func (s *DockerSupportInterfaceSuite) TestInterfaces(c *C) { c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) } diff --git a/interfaces/builtin/export_test.go b/interfaces/builtin/export_test.go index fbe7410e9a4..5d41b1c6591 100644 --- a/interfaces/builtin/export_test.go +++ b/interfaces/builtin/export_test.go @@ -102,3 +102,11 @@ func MockOsGetenv(mock func(string) string) (restore func()) { return restore } + +func MockParserFeatures(f func() []string) (resture func()) { + old := parserFeatures + parserFeatures = f + return func() { + parserFeatures = old + } +} From db3664e5a9e27a3544e4e97a7e89ede0f3b0d3f0 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Sun, 14 Oct 2018 16:07:29 +0000 Subject: [PATCH 019/580] conditionally apply 'ix' rule to home interface to avoid LP: #1797786 Certain change_profile rules conflict with the 'ix' rules in the home interface. By default, always add 'ix' but allow interfaces to request suppressing the use of 'ix' References: https://bugs.launchpad.net/apparmor/+bug/1797786 https://forum.snapcraft.io/t/ld-library-path-in-snapped-docker/6903/16 https://github.com/kubeflow/kubeflow/issues/1367 --- interfaces/apparmor/backend.go | 10 +++++- interfaces/apparmor/backend_test.go | 49 +++++++++++++++++++++++++++- interfaces/apparmor/spec.go | 19 ++++++++++- interfaces/apparmor/spec_test.go | 8 ++++- interfaces/builtin/common.go | 6 +++- interfaces/builtin/common_test.go | 37 ++++++++++++++++++++- interfaces/builtin/docker_support.go | 5 ++- interfaces/builtin/home.go | 12 +++---- 8 files changed, 133 insertions(+), 13 deletions(-) diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index f515b5f1e0b..d19089a65ca 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.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 @@ -560,6 +560,14 @@ func addContent(securityTag string, snapInfo *snap.Info, opts interfaces.Confine if spec.suppressPtraceTrace && !spec.usesPtraceTrace { tagSnippets += ptraceTraceDenySnippet } + + // Use 'ix' rules in the home interface unless an + // interface asked to suppress them + repl := "ix" + if spec.suppressHomeIx { + repl = "" + } + tagSnippets = strings.Replace(tagSnippets, "###HOME_IX###", repl, -1) } return tagSnippets diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 8bd26958f1d..9a4870aeebc 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_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 @@ -1660,3 +1660,50 @@ func (s *backendSuite) TestPtraceTraceRule(c *C) { s.RemoveSnap(c, snapInfo) } } + +func (s *backendSuite) TestHomeIxRule(c *C) { + restoreTemplate := apparmor.MockTemplate("template\n###SNIPPETS###\nneedle rwkl###HOME_IX###,\n") + defer restoreTemplate() + restore := release.MockAppArmorLevel(release.FullAppArmor) + defer restore() + restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) + defer restore() + + for _, tc := range []struct { + opts interfaces.ConfinementOptions + suppress bool + expected string + }{ + { + opts: interfaces.ConfinementOptions{}, + suppress: true, + expected: "needle rwkl,", + }, + { + opts: interfaces.ConfinementOptions{}, + suppress: false, + expected: "needle rwklix,", + }, + } { + s.Iface.AppArmorPermanentSlotCallback = func(spec *apparmor.Specification, slot *snap.SlotInfo) error { + if tc.suppress { + spec.SuppressHomeIx() + } + spec.AddSnippet("needle rwkl###HOME_IX###,") + return nil + } + + snapInfo := s.InstallSnap(c, tc.opts, "", ifacetest.SambaYamlV1, 1) + s.parserCmd.ForgetCalls() + + err := s.Backend.Setup(snapInfo, tc.opts, s.Repo) + c.Assert(err, IsNil) + + profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") + data, err := ioutil.ReadFile(profile) + c.Assert(err, IsNil) + + c.Assert(string(data), testutil.Contains, tc.expected) + s.RemoveSnap(c, snapInfo) + } +} diff --git a/interfaces/apparmor/spec.go b/interfaces/apparmor/spec.go index 9451af6c714..9a80ec84138 100644 --- a/interfaces/apparmor/spec.go +++ b/interfaces/apparmor/spec.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,14 @@ type Specification struct { // } suppressPtraceTrace bool usesPtraceTrace bool + + // The home interface typically should have 'ix' as part of its rules, + // but specifying certain change_profile rules with these rules cases + // a 'conflicting x modifiers' parser error. Allow interfaces that + // require this type of change_profile rule to suppress 'ix' so that + // the calling interface can be used with the home interface. Ideally, + // we would not need this, but we currently do (LP: #1797786) + suppressHomeIx bool } // setScope sets the scope of subsequent AddSnippet family functions. @@ -509,3 +517,12 @@ func (spec *Specification) GetUsesPtraceTrace() bool { func (spec *Specification) GetSuppressPtraceTrace() bool { return spec.suppressPtraceTrace } + +// SuppressHomeIx to request explicit ptrace deny rules +func (spec *Specification) SuppressHomeIx() { + spec.suppressHomeIx = true +} + +func (spec *Specification) GetSuppressHomeIx() bool { + return spec.suppressHomeIx +} diff --git a/interfaces/apparmor/spec_test.go b/interfaces/apparmor/spec_test.go index ff19cd941d9..0f5783a41e9 100644 --- a/interfaces/apparmor/spec_test.go +++ b/interfaces/apparmor/spec_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 @@ -551,3 +551,9 @@ func (s *specSuite) TestSuppressPtraceTrace(c *C) { s.spec.SuppressPtraceTrace() c.Assert(s.spec.GetSuppressPtraceTrace(), Equals, true) } + +func (s *specSuite) TestSuppressHomeIx(c *C) { + c.Assert(s.spec.GetSuppressHomeIx(), Equals, false) + s.spec.SuppressHomeIx() + c.Assert(s.spec.GetSuppressHomeIx(), Equals, true) +} diff --git a/interfaces/builtin/common.go b/interfaces/builtin/common.go index 9c655e93fb5..65249c910e4 100644 --- a/interfaces/builtin/common.go +++ b/interfaces/builtin/common.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 @@ -58,6 +58,7 @@ type commonInterface struct { usesPtraceTrace bool suppressPtraceTrace bool + suppressHomeIx bool } // Name returns the interface name. @@ -94,6 +95,9 @@ func (iface *commonInterface) AppArmorConnectedPlug(spec *apparmor.Specification } else if iface.suppressPtraceTrace { spec.SuppressPtraceTrace() } + if iface.suppressHomeIx { + spec.SuppressHomeIx() + } if iface.connectedPlugAppArmor != "" { spec.AddSnippet(iface.connectedPlugAppArmor) } diff --git a/interfaces/builtin/common_test.go b/interfaces/builtin/common_test.go index aca2b0052c9..2346ef89a74 100644 --- a/interfaces/builtin/common_test.go +++ b/interfaces/builtin/common_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 @@ -151,3 +151,38 @@ slots: c.Assert(spec.GetUsesPtraceTrace(), Equals, true) c.Assert(spec.GetSuppressPtraceTrace(), Equals, false) } + +func (s *commonIfaceSuite) TestSuppressHomeIx(c *C) { + plug, _ := MockConnectedPlug(c, ` +name: consumer +version: 0 +apps: + app: + plugs: [common] +`, nil, "common") + slot, _ := MockConnectedSlot(c, ` +name: producer +version: 0 +slots: + common: +`, nil, "common") + + // setting nothing + iface := &commonInterface{ + name: "common", + suppressHomeIx: false, + } + spec := &apparmor.Specification{} + c.Assert(spec.GetSuppressHomeIx(), Equals, false) + c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) + c.Assert(spec.GetSuppressHomeIx(), Equals, false) + + iface = &commonInterface{ + name: "common", + suppressHomeIx: true, + } + spec = &apparmor.Specification{} + c.Assert(spec.GetSuppressHomeIx(), Equals, false) + c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) + c.Assert(spec.GetSuppressHomeIx(), Equals, true) +} diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index 0d08d5e243e..6d63434e01f 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.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 @@ -588,6 +588,9 @@ func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specif rule := "change_profile -> docker-default," if useUnsafe { rule = "change_profile unsafe /** -> docker-default," + // This rule conflicts with the 'ix' rules in the home + // interface, so suppress them (LP: #1797786) + spec.SuppressHomeIx() } snippet := cpDockerDefaultPattern.ReplaceAllString(dockerSupportConnectedPlugAppArmor, rule) spec.AddSnippet(snippet) diff --git a/interfaces/builtin/home.go b/interfaces/builtin/home.go index 507b9525c07..0a4bc391aa1 100644 --- a/interfaces/builtin/home.go +++ b/interfaces/builtin/home.go @@ -55,14 +55,14 @@ owner @{HOME}/ r, # Allow read/write access to all files in @{HOME}, except snap application # data in @{HOME}/snap and toplevel hidden directories in @{HOME}. -owner @{HOME}/[^s.]** rwklix, -owner @{HOME}/s[^n]** rwklix, -owner @{HOME}/sn[^a]** rwklix, -owner @{HOME}/sna[^p]** rwklix, -owner @{HOME}/snap[^/]** rwklix, +owner @{HOME}/[^s.]** rwkl###HOME_IX###, +owner @{HOME}/s[^n]** rwkl###HOME_IX###, +owner @{HOME}/sn[^a]** rwkl###HOME_IX###, +owner @{HOME}/sna[^p]** rwkl###HOME_IX###, +owner @{HOME}/snap[^/]** rwkl###HOME_IX###, # Allow creating a few files not caught above -owner @{HOME}/{s,sn,sna}{,/} rwklix, +owner @{HOME}/{s,sn,sna}{,/} rwkl###HOME_IX###, # Allow access to @{HOME}/snap/ to allow directory traversals from # @{HOME}/snap/@{SNAP_INSTANCE_NAME} through @{HOME}/snap to @{HOME}. From 697b12b992bb615a04164b80cc60b03d4eb314d1 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Mon, 15 Oct 2018 20:58:33 +0000 Subject: [PATCH 020/580] adjust Getters and Setters thanks to zyga --- interfaces/apparmor/backend.go | 2 +- interfaces/apparmor/backend_test.go | 2 +- interfaces/apparmor/spec.go | 6 +++--- interfaces/apparmor/spec_test.go | 8 ++++---- interfaces/builtin/common.go | 2 +- interfaces/builtin/common_test.go | 8 ++++---- interfaces/builtin/docker_support.go | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index d19089a65ca..56cd09da031 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -564,7 +564,7 @@ func addContent(securityTag string, snapInfo *snap.Info, opts interfaces.Confine // Use 'ix' rules in the home interface unless an // interface asked to suppress them repl := "ix" - if spec.suppressHomeIx { + if spec.SuppressHomeIx() { repl = "" } tagSnippets = strings.Replace(tagSnippets, "###HOME_IX###", repl, -1) diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 9a4870aeebc..dcd64f04f57 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -1687,7 +1687,7 @@ func (s *backendSuite) TestHomeIxRule(c *C) { } { s.Iface.AppArmorPermanentSlotCallback = func(spec *apparmor.Specification, slot *snap.SlotInfo) error { if tc.suppress { - spec.SuppressHomeIx() + spec.SetSuppressHomeIx() } spec.AddSnippet("needle rwkl###HOME_IX###,") return nil diff --git a/interfaces/apparmor/spec.go b/interfaces/apparmor/spec.go index 9a80ec84138..f512b28c2b8 100644 --- a/interfaces/apparmor/spec.go +++ b/interfaces/apparmor/spec.go @@ -518,11 +518,11 @@ func (spec *Specification) GetSuppressPtraceTrace() bool { return spec.suppressPtraceTrace } -// SuppressHomeIx to request explicit ptrace deny rules -func (spec *Specification) SuppressHomeIx() { +// SetSuppressHomeIx to request explicit ptrace deny rules +func (spec *Specification) SetSuppressHomeIx() { spec.suppressHomeIx = true } -func (spec *Specification) GetSuppressHomeIx() bool { +func (spec *Specification) SuppressHomeIx() bool { return spec.suppressHomeIx } diff --git a/interfaces/apparmor/spec_test.go b/interfaces/apparmor/spec_test.go index 0f5783a41e9..d6e2763ae0c 100644 --- a/interfaces/apparmor/spec_test.go +++ b/interfaces/apparmor/spec_test.go @@ -552,8 +552,8 @@ func (s *specSuite) TestSuppressPtraceTrace(c *C) { c.Assert(s.spec.GetSuppressPtraceTrace(), Equals, true) } -func (s *specSuite) TestSuppressHomeIx(c *C) { - c.Assert(s.spec.GetSuppressHomeIx(), Equals, false) - s.spec.SuppressHomeIx() - c.Assert(s.spec.GetSuppressHomeIx(), Equals, true) +func (s *specSuite) TestSetSuppressHomeIx(c *C) { + c.Assert(s.spec.SuppressHomeIx(), Equals, false) + s.spec.SetSuppressHomeIx() + c.Assert(s.spec.SuppressHomeIx(), Equals, true) } diff --git a/interfaces/builtin/common.go b/interfaces/builtin/common.go index 65249c910e4..5fe9bb4119b 100644 --- a/interfaces/builtin/common.go +++ b/interfaces/builtin/common.go @@ -96,7 +96,7 @@ func (iface *commonInterface) AppArmorConnectedPlug(spec *apparmor.Specification spec.SuppressPtraceTrace() } if iface.suppressHomeIx { - spec.SuppressHomeIx() + spec.SetSuppressHomeIx() } if iface.connectedPlugAppArmor != "" { spec.AddSnippet(iface.connectedPlugAppArmor) diff --git a/interfaces/builtin/common_test.go b/interfaces/builtin/common_test.go index 2346ef89a74..8524745c79b 100644 --- a/interfaces/builtin/common_test.go +++ b/interfaces/builtin/common_test.go @@ -173,16 +173,16 @@ slots: suppressHomeIx: false, } spec := &apparmor.Specification{} - c.Assert(spec.GetSuppressHomeIx(), Equals, false) + c.Assert(spec.SuppressHomeIx(), Equals, false) c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.GetSuppressHomeIx(), Equals, false) + c.Assert(spec.SuppressHomeIx(), Equals, false) iface = &commonInterface{ name: "common", suppressHomeIx: true, } spec = &apparmor.Specification{} - c.Assert(spec.GetSuppressHomeIx(), Equals, false) + c.Assert(spec.SuppressHomeIx(), Equals, false) c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.GetSuppressHomeIx(), Equals, true) + c.Assert(spec.SuppressHomeIx(), Equals, true) } diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index 6d63434e01f..7f04ff8cb44 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.go @@ -590,7 +590,7 @@ func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specif rule = "change_profile unsafe /** -> docker-default," // This rule conflicts with the 'ix' rules in the home // interface, so suppress them (LP: #1797786) - spec.SuppressHomeIx() + spec.SetSuppressHomeIx() } snippet := cpDockerDefaultPattern.ReplaceAllString(dockerSupportConnectedPlugAppArmor, rule) spec.AddSnippet(snippet) From c13340dda36a02ac23b9b902e351172ef11ad055 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 10 Oct 2018 12:24:54 +0200 Subject: [PATCH 021/580] snap/name: new package for validation snap related names Introuduce a new package that pulls out the validation of various names around snaps. Name validation covers: snaps, instances, apps, aliases, hooks, slots/plugs/interfaces, sockets. The package has no extra dependencies. Signed-off-by: Maciej Borzecki --- snap/name/name_test.go | 29 ++++ snap/name/validate.go | 164 ++++++++++++++++++++++ snap/name/validate_test.go | 279 +++++++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+) create mode 100644 snap/name/name_test.go create mode 100644 snap/name/validate.go create mode 100644 snap/name/validate_test.go diff --git a/snap/name/name_test.go b/snap/name/name_test.go new file mode 100644 index 00000000000..ac47aec209a --- /dev/null +++ b/snap/name/name_test.go @@ -0,0 +1,29 @@ +// -*- 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 . + * + */ + +package name_test + +import ( + "testing" + + . "gopkg.in/check.v1" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } diff --git a/snap/name/validate.go b/snap/name/validate.go new file mode 100644 index 00000000000..cc19322c16f --- /dev/null +++ b/snap/name/validate.go @@ -0,0 +1,164 @@ +// -*- 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 . + * + */ + +package name + +import ( + "fmt" + "regexp" + "strings" +) + +// almostValidName is part of snap and socket name validation. +// the full regexp we could use, "^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$", is +// O(2ⁿ) on the length of the string in python. An equivalent regexp that +// doesn't have the nested quantifiers that trip up Python's re would be +// "^(?:[a-z0-9]|(?<=[a-z0-9])-)*[a-z](?:[a-z0-9]|-(?=[a-z0-9]))*$", but Go's +// regexp package doesn't support look-aheads nor look-behinds, so in order to +// have a unified implementation in the Go and Python bits of the project +// we're doing it this way instead. Check the length (if applicable), check +// this regexp, then check the dashes. +// This still leaves sc_snap_name_validate (in cmd/snap-confine/snap.c) and +// snap_validate (cmd/snap-update-ns/bootstrap.c) with their own handcrafted +// validators. +var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") + +// validInstanceKey is a regular expression describing valid snap instance key +var validInstanceKey = regexp.MustCompile("^[a-z0-9]{1,10}$") + +// isValidName checks snap and socket socket identifiers. +func isValidName(name string) bool { + if !almostValidName.MatchString(name) { + return false + } + if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { + return false + } + return true +} + +// ValidateInstance checks if a string can be used as a snap instance name. +func ValidateInstance(instanceName string) error { + // NOTE: This function should be synchronized with the two other + // implementations: sc_instance_name_validate and validate_instance_name . + pos := strings.IndexByte(instanceName, '_') + if pos == -1 { + // just store name + return ValidateSnap(instanceName) + } + + storeName := instanceName[:pos] + instanceKey := instanceName[pos+1:] + if err := ValidateSnap(storeName); err != nil { + return err + } + if !validInstanceKey.MatchString(instanceKey) { + // TODO parallel-install: extend the error message once snap + // install help has been updated + return fmt.Errorf("invalid instance key: %q", instanceKey) + } + return nil +} + +// ValidateSnap checks if a string can be used as a snap name. +func ValidateSnap(name string) error { + // NOTE: This function should be synchronized with the two other + // implementations: sc_snap_name_validate and validate_snap_name . + if len(name) > 40 || !isValidName(name) { + return fmt.Errorf("invalid snap name: %q", name) + } + return nil +} + +// Regular expression describing correct plug, slot and interface names. +var validPlugSlotIfaceName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") + +// ValidatePlug checks if a string can be used as a slot name. +// +// Slot names and plug names within one snap must have unique names. +// This is not enforced by this function but is enforced by snap-level +// validation. +func ValidatePlug(name string) error { + if !validPlugSlotIfaceName.MatchString(name) { + return fmt.Errorf("invalid plug name: %q", name) + } + return nil +} + +// ValidateSlot checks if a string can be used as a slot name. +// +// Slot names and plug names within one snap must have unique names. +// This is not enforced by this function but is enforced by snap-level +// validation. +func ValidateSlot(name string) error { + if !validPlugSlotIfaceName.MatchString(name) { + return fmt.Errorf("invalid slot name: %q", name) + } + return nil +} + +// ValidateInterface checks if a string can be used as an interface name. +func ValidateInterface(name string) error { + if !validPlugSlotIfaceName.MatchString(name) { + return fmt.Errorf("invalid interface name: %q", name) + } + return nil +} + +// Regular expressions describing correct identifiers. +var validHookName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") + +// ValidateHook checks if a string can be used as a hook name. +func ValidateHook(name string) error { + valid := validHookName.MatchString(name) + if !valid { + return fmt.Errorf("invalid hook name: %q", name) + } + return nil +} + +var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") + +// ValidateAlias checks if a string can be used as an alias name. +func ValidateAlias(alias string) error { + valid := validAlias.MatchString(alias) + if !valid { + return fmt.Errorf("invalid alias name: %q", alias) + } + return nil +} + +// ValidateApp tells whether a string is a valid application name. +func ValidateApp(n string) error { + var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") + + if !validAppName.MatchString(n) { + return fmt.Errorf("invalid app name: %q", n) + } + return nil +} + +// ValidateSockeName checks if a string ca be used as a name for a socket (for +// socket activation). +func ValidateSocket(name string) error { + if !isValidName(name) { + return fmt.Errorf("invalid socket name: %q", name) + } + return nil +} diff --git a/snap/name/validate_test.go b/snap/name/validate_test.go new file mode 100644 index 00000000000..ab8052abd45 --- /dev/null +++ b/snap/name/validate_test.go @@ -0,0 +1,279 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 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 . + * + */ + +package name_test + +import ( + . "gopkg.in/check.v1" + + snapname "github.com/snapcore/snapd/snap/name" + + "github.com/snapcore/snapd/testutil" +) + +type ValidateSuite struct { + testutil.BaseTest +} + +var _ = Suite(&ValidateSuite{}) + +func (s *ValidateSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) +} + +func (s *ValidateSuite) TearDownTest(c *C) { + s.BaseTest.TearDownTest(c) +} + +func (s *ValidateSuite) TestValidateName(c *C) { + validNames := []string{ + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2", + // a regexp stresser + "u-94903713687486543234157734673284536758", + } + for _, name := range validNames { + err := snapname.ValidateSnap(name) + c.Assert(err, IsNil) + } + invalidNames := []string{ + // name cannot be empty + "", + // names cannot be too long + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "1111111111111111111111111111111111111111x", + "x1111111111111111111111111111111111111111", + "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", + // a regexp stresser + "u-9490371368748654323415773467328453675-", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + } + for _, name := range invalidNames { + err := snapname.ValidateSnap(name) + c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) + } +} + +func (s *ValidateSuite) TestValidateInstanceName(c *C) { + validNames := []string{ + // plain names are also valid instance names + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + // snap instance + "foo_bar", + "foo_0123456789", + "01game_0123456789", + "foo_1", "foo_1234abcd", + } + for _, name := range validNames { + err := snapname.ValidateInstance(name) + c.Assert(err, IsNil) + } + invalidNames := []string{ + // invalid names are also invalid instance names, just a few + // samples + "", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", + "a--a", + "a-", + "a ", " a", "a a", + "_", + "ру́сский_язы́к", + } + for _, name := range invalidNames { + err := snapname.ValidateInstance(name) + c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) + } + invalidInstanceKeys := []string{ + // the snap names are valid, but instance keys are not + "foo_", "foo_1-23", "foo_01234567890", "foo_123_456", + "foo__bar", + } + for _, name := range invalidInstanceKeys { + err := snapname.ValidateInstance(name) + c.Assert(err, ErrorMatches, `invalid instance key: ".*"`) + } + +} + +func (s *ValidateSuite) TestValidateHookName(c *C) { + validHooks := []string{ + "a", + "aaa", + "a-a", + "aa-a", + "a-aa", + "a-b-c", + "valid", + } + for _, hook := range validHooks { + err := snapname.ValidateHook(hook) + c.Assert(err, IsNil) + } + invalidHooks := []string{ + "", + "a a", + "a--a", + "-a", + "a-", + "0", + "123", + "123abc", + "日本語", + } + for _, hook := range invalidHooks { + err := snapname.ValidateHook(hook) + c.Assert(err, ErrorMatches, `invalid hook name: ".*"`) + } +} + +func (s *ValidateSuite) TestValidateAppName(c *C) { + validAppNames := []string{ + "1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1", + "a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a", + } + for _, name := range validAppNames { + c.Check(snapname.ValidateApp(name), IsNil) + } + invalidAppNames := []string{ + "", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글", + "ру́сский язы́к", "ໄຂ່​ອີ​ສ​ເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a", + } + for _, name := range invalidAppNames { + err := snapname.ValidateApp(name) + c.Assert(err, ErrorMatches, `invalid app name: ".*"`) + } +} + +func (s *ValidateSuite) TestValidateAlias(c *C) { + validAliases := []string{ + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "a_a", "aa_a", "a_aa", "a_b_c", + "a0", "a_0", "a_0a", + "01game", "1-or-2", + "0.1game", "1_or_2", + } + for _, alias := range validAliases { + err := snapname.ValidateAlias(alias) + c.Assert(err, IsNil) + } + invalidAliases := []string{ + "", + "_foo", + "-foo", + ".foo", + "foo$", + } + for _, alias := range invalidAliases { + err := snapname.ValidateAlias(alias) + c.Assert(err, ErrorMatches, `invalid alias name: ".*"`) + } +} + +func (s *ValidateSuite) TestValidateSocketName(c *C) { + validNames := []string{ + "a", "aa", "aaa", "aaaa", + "a-a", "aa-a", "a-aa", "a-b-c", + "a0", "a-0", "a-0a", + "01game", "1-or-2", + } + for _, name := range validNames { + err := snapname.ValidateSocket(name) + c.Assert(err, IsNil) + } + invalidNames := []string{ + // name cannot be empty + "", + // dashes alone are not a name + "-", "--", + // double dashes in a name are not allowed + "a--a", + // name should not end with a dash + "a-", + // name cannot have any spaces in it + "a ", " a", "a a", + // a number alone is not a name + "0", "123", + // identifier must be plain ASCII + "日本語", "한글", "ру́сский язы́к", + // no null chars in the string are allowed + "aa-a\000-b", + } + for _, name := range invalidNames { + err := snapname.ValidateSocket(name) + c.Assert(err, ErrorMatches, `invalid socket name: ".*"`) + } +} + +func (s *ValidateSuite) TestValidateSlotPlugInterfaceName(c *C) { + valid := []string{ + "a", + "aaa", + "a-a", + "aa-a", + "a-aa", + "a-b-c", + "valid", + "valid-123", + } + for _, name := range valid { + err := snapname.ValidateSlot(name) + c.Assert(err, IsNil) + err = snapname.ValidatePlug(name) + c.Assert(err, IsNil) + err = snapname.ValidateInterface(name) + c.Assert(err, IsNil) + } + invalid := []string{ + "", + "a a", + "a--a", + "-a", + "a-", + "0", + "123", + "123abc", + "日本語", + } + for _, name := range invalid { + err := snapname.ValidateSlot(name) + c.Assert(err, ErrorMatches, `invalid slot name: ".*"`) + err = snapname.ValidatePlug(name) + c.Assert(err, ErrorMatches, `invalid plug name: ".*"`) + err = snapname.ValidateInterface(name) + c.Assert(err, ErrorMatches, `invalid interface name: ".*"`) + } +} From e75acbc14f5a1657d77e769ca8c32bae26e68aa3 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 10 Oct 2018 12:26:47 +0200 Subject: [PATCH 022/580] snap: use snap/name for names validation Signed-off-by: Maciej Borzecki --- snap/validate.go | 99 +++---------------------- snap/validate_test.go | 166 ------------------------------------------ 2 files changed, 12 insertions(+), 253 deletions(-) diff --git a/snap/validate.go b/snap/validate.go index 94859294505..6c80687cfb2 100644 --- a/snap/validate.go +++ b/snap/validate.go @@ -30,89 +30,32 @@ import ( "strconv" "strings" + snapname "github.com/snapcore/snapd/snap/name" "github.com/snapcore/snapd/spdx" "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeutil" ) -// Regular expressions describing correct identifiers. -var validHookName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") - // The fixed length of valid snap IDs. const validSnapIDLength = 32 -// almostValidName is part of snap and socket name validation. -// the full regexp we could use, "^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$", is -// O(2ⁿ) on the length of the string in python. An equivalent regexp that -// doesn't have the nested quantifiers that trip up Python's re would be -// "^(?:[a-z0-9]|(?<=[a-z0-9])-)*[a-z](?:[a-z0-9]|-(?=[a-z0-9]))*$", but Go's -// regexp package doesn't support look-aheads nor look-behinds, so in order to -// have a unified implementation in the Go and Python bits of the project -// we're doing it this way instead. Check the length (if applicable), check -// this regexp, then check the dashes. -// This still leaves sc_snap_name_validate (in cmd/snap-confine/snap.c) and -// snap_validate (cmd/snap-update-ns/bootstrap.c) with their own handcrafted -// validators. -var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") - -// validInstanceKey is a regular expression describing valid snap instance key -var validInstanceKey = regexp.MustCompile("^[a-z0-9]{1,10}$") - -// isValidName checks snap and socket socket identifiers. -func isValidName(name string) bool { - if !almostValidName.MatchString(name) { - return false - } - if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { - return false - } - return true -} - // ValidateInstanceName checks if a string can be used as a snap instance name. func ValidateInstanceName(instanceName string) error { - // NOTE: This function should be synchronized with the two other - // implementations: sc_instance_name_validate and validate_instance_name . - pos := strings.IndexByte(instanceName, '_') - if pos == -1 { - // just store name - return ValidateName(instanceName) - } - - storeName := instanceName[:pos] - instanceKey := instanceName[pos+1:] - if err := ValidateName(storeName); err != nil { - return err - } - if !validInstanceKey.MatchString(instanceKey) { - return fmt.Errorf("invalid instance key: %q", instanceKey) - } - return nil + return snapname.ValidateInstance(instanceName) } // ValidateName checks if a string can be used as a snap name. func ValidateName(name string) error { - // NOTE: This function should be synchronized with the two other - // implementations: sc_snap_name_validate and validate_snap_name . - if len(name) > 40 || !isValidName(name) { - return fmt.Errorf("invalid snap name: %q", name) - } - return nil + return snapname.ValidateSnap(name) } -// Regular expression describing correct plug, slot and interface names. -var validPlugSlotIfaceName = regexp.MustCompile("^[a-z](?:-?[a-z0-9])*$") - // ValidatePlugName checks if a string can be used as a slot name. // // Slot names and plug names within one snap must have unique names. // This is not enforced by this function but is enforced by snap-level // validation. func ValidatePlugName(name string) error { - if !validPlugSlotIfaceName.MatchString(name) { - return fmt.Errorf("invalid plug name: %q", name) - } - return nil + return snapname.ValidatePlug(name) } // ValidateSlotName checks if a string can be used as a slot name. @@ -121,18 +64,12 @@ func ValidatePlugName(name string) error { // This is not enforced by this function but is enforced by snap-level // validation. func ValidateSlotName(name string) error { - if !validPlugSlotIfaceName.MatchString(name) { - return fmt.Errorf("invalid slot name: %q", name) - } - return nil + return snapname.ValidateSlot(name) } // ValidateInterfaceName checks if a string can be used as an interface name. func ValidateInterfaceName(name string) error { - if !validPlugSlotIfaceName.MatchString(name) { - return fmt.Errorf("invalid interface name: %q", name) - } - return nil + return snapname.ValidateInterface(name) } // NB keep this in sync with snapcraft and the review tools :-) @@ -207,9 +144,8 @@ func ValidateLicense(license string) error { // ValidateHook validates the content of the given HookInfo func ValidateHook(hook *HookInfo) error { - valid := validHookName.MatchString(hook.Name) - if !valid { - return fmt.Errorf("invalid hook name: %q", hook.Name) + if err := snapname.ValidateHook(hook.Name); err != nil { + return err } // Also validate the command chain @@ -222,24 +158,15 @@ func ValidateHook(hook *HookInfo) error { return nil } -var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") - // ValidateAlias checks if a string can be used as an alias name. func ValidateAlias(alias string) error { - valid := validAlias.MatchString(alias) - if !valid { - return fmt.Errorf("invalid alias name: %q", alias) - } - return nil + return snapname.ValidateAlias(alias) } // validateSocketName checks if a string ca be used as a name for a socket (for // socket activation). func validateSocketName(name string) error { - if !isValidName(name) { - return fmt.Errorf("invalid socket name: %q", name) - } - return nil + return snapname.ValidateSocket(name) } // validateSocketmode checks that the socket mode is a valid file mode. @@ -369,7 +296,7 @@ func Validate(info *Info) error { // validate aliases for alias, app := range info.LegacyAliases { - if !validAlias.MatchString(alias) { + if err := snapname.ValidateAlias(alias); err != nil { return fmt.Errorf("cannot have %q as alias name for app %q - use only letters, digits, dash, underscore and dot characters", alias, app.Name) } } @@ -647,9 +574,7 @@ var commandChainContentWhitelist = regexp.MustCompile(`^[A-Za-z0-9/._#:$-]*$`) // ValidAppName tells whether a string is a valid application name. func ValidAppName(n string) bool { - var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") - - return validAppName.MatchString(n) + return snapname.ValidateApp(n) == nil } // ValidateApp verifies the content in the app info. diff --git a/snap/validate_test.go b/snap/validate_test.go index 223f15f78b8..9c992c1ae15 100644 --- a/snap/validate_test.go +++ b/snap/validate_test.go @@ -68,92 +68,6 @@ func (s *ValidateSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } -func (s *ValidateSuite) TestValidateName(c *C) { - validNames := []string{ - "a", "aa", "aaa", "aaaa", - "a-a", "aa-a", "a-aa", "a-b-c", - "a0", "a-0", "a-0a", - "01game", "1-or-2", - // a regexp stresser - "u-94903713687486543234157734673284536758", - } - for _, name := range validNames { - err := ValidateName(name) - c.Assert(err, IsNil) - } - invalidNames := []string{ - // name cannot be empty - "", - // names cannot be too long - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", - "1111111111111111111111111111111111111111x", - "x1111111111111111111111111111111111111111", - "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x", - // a regexp stresser - "u-9490371368748654323415773467328453675-", - // dashes alone are not a name - "-", "--", - // double dashes in a name are not allowed - "a--a", - // name should not end with a dash - "a-", - // name cannot have any spaces in it - "a ", " a", "a a", - // a number alone is not a name - "0", "123", - // identifier must be plain ASCII - "日本語", "한글", "ру́сский язы́к", - } - for _, name := range invalidNames { - err := ValidateName(name) - c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) - } -} - -func (s *ValidateSuite) TestValidateInstanceName(c *C) { - validNames := []string{ - // plain names are also valid instance names - "a", "aa", "aaa", "aaaa", - "a-a", "aa-a", "a-aa", "a-b-c", - // snap instance - "foo_bar", - "foo_0123456789", - "01game_0123456789", - "foo_1", "foo_1234abcd", - } - for _, name := range validNames { - err := ValidateInstanceName(name) - c.Assert(err, IsNil) - } - invalidNames := []string{ - // invalid names are also invalid instance names, just a few - // samples - "", - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", - "a--a", - "a-", - "a ", " a", "a a", - "_", - "ру́сский_язы́к", - } - for _, name := range invalidNames { - err := ValidateInstanceName(name) - c.Assert(err, ErrorMatches, `invalid snap name: ".*"`) - } - invalidInstanceKeys := []string{ - // the snap names are valid, but instance keys are not - "foo_", "foo_1-23", "foo_01234567890", "foo_123_456", - "foo__bar", - } - for _, name := range invalidInstanceKeys { - err := ValidateInstanceName(name) - c.Assert(err, ErrorMatches, `invalid instance key: ".*"`) - } - -} - func (s *ValidateSuite) TestValidateVersion(c *C) { validVersions := []string{ "0", "v1.0", "0.12+16.04.20160126-0ubuntu1", @@ -256,24 +170,6 @@ func (s *ValidateSuite) TestValidateHook(c *C) { // ValidateApp -func (s *ValidateSuite) TestValidateAppName(c *C) { - validAppNames := []string{ - "1", "a", "aa", "aaa", "aaaa", "Aa", "aA", "1a", "a1", "1-a", "a-1", - "a-a", "aa-a", "a-aa", "a-b-c", "0a-a", "a-0a", - } - for _, name := range validAppNames { - c.Check(ValidateApp(&AppInfo{Name: name}), IsNil) - } - invalidAppNames := []string{ - "", "-", "--", "a--a", "a-", "a ", " a", "a a", "日本語", "한글", - "ру́сский язы́к", "ໄຂ່​ອີ​ສ​ເຕີ້", ":a", "a:", "a:a", "_a", "a_", "a_a", - } - for _, name := range invalidAppNames { - err := ValidateApp(&AppInfo{Name: name}) - c.Assert(err, ErrorMatches, `cannot have ".*" as app name.*`) - } -} - func (s *ValidateSuite) TestValidateAppSockets(c *C) { app := createSampleApp() app.Sockets["sock"].SocketMode = 0600 @@ -668,33 +564,6 @@ apps: c.Check(err, ErrorMatches, `cannot have "foo\$" as alias name for app "foo" - use only letters, digits, dash, underscore and dot characters`) } -func (s *ValidateSuite) TestValidateAlias(c *C) { - validAliases := []string{ - "a", "aa", "aaa", "aaaa", - "a-a", "aa-a", "a-aa", "a-b-c", - "a0", "a-0", "a-0a", - "a_a", "aa_a", "a_aa", "a_b_c", - "a0", "a_0", "a_0a", - "01game", "1-or-2", - "0.1game", "1_or_2", - } - for _, alias := range validAliases { - err := ValidateAlias(alias) - c.Assert(err, IsNil) - } - invalidAliases := []string{ - "", - "_foo", - "-foo", - ".foo", - "foo$", - } - for _, alias := range invalidAliases { - err := ValidateAlias(alias) - c.Assert(err, ErrorMatches, `invalid alias name: ".*"`) - } -} - func (s *ValidateSuite) TestValidatePlugSlotName(c *C) { const yaml1 = ` name: invalid-plugs @@ -1051,41 +920,6 @@ layout: c.Assert(err, IsNil) } -func (s *ValidateSuite) TestValidateSocketName(c *C) { - validNames := []string{ - "a", "aa", "aaa", "aaaa", - "a-a", "aa-a", "a-aa", "a-b-c", - "a0", "a-0", "a-0a", - "01game", "1-or-2", - } - for _, name := range validNames { - err := ValidateSocketName(name) - c.Assert(err, IsNil) - } - invalidNames := []string{ - // name cannot be empty - "", - // dashes alone are not a name - "-", "--", - // double dashes in a name are not allowed - "a--a", - // name should not end with a dash - "a-", - // name cannot have any spaces in it - "a ", " a", "a a", - // a number alone is not a name - "0", "123", - // identifier must be plain ASCII - "日本語", "한글", "ру́сский язы́к", - // no null chars in the string are allowed - "aa-a\000-b", - } - for _, name := range invalidNames { - err := ValidateSocketName(name) - c.Assert(err, ErrorMatches, `invalid socket name: ".*"`) - } -} - func (s *YamlSuite) TestValidateAppStartupOrder(c *C) { meta := []byte(` name: foo From e53d6e3760f1adc4e6a2a9b5bd0563df167fd060 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 10 Oct 2018 12:28:03 +0200 Subject: [PATCH 023/580] asserts: use snap/name helpers for snap name validation Signed-off-by: Maciej Borzecki --- asserts/device_asserts.go | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/asserts/device_asserts.go b/asserts/device_asserts.go index f03a586b713..bde97f72a7c 100644 --- a/asserts/device_asserts.go +++ b/asserts/device_asserts.go @@ -25,6 +25,7 @@ import ( "strings" "time" + snapname "github.com/snapcore/snapd/snap/name" "github.com/snapcore/snapd/strutil" ) @@ -222,24 +223,8 @@ var ( classicModelOptional = []string{"architecture", "gadget"} ) -var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") - -// validateSnapName checks whether the name can be used as a snap name -// -// This function should be synchronized with the reference implementation -// snap.ValidateName() in snap/validate.go func validateSnapName(name string, headerName string) error { - isValidName := func() bool { - if !almostValidName.MatchString(name) { - return false - } - if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { - return false - } - return true - } - - if len(name) > 40 || !isValidName() { + if err := snapname.ValidateSnap(name); err != nil { return fmt.Errorf("invalid snap name in %q header: %s", headerName, name) } return nil From 2feab203d674342773e88a45cd5ab3399bc4a658 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 19 Oct 2018 12:12:25 +0200 Subject: [PATCH 024/580] snap/name: export valid app and aliase regexps Signed-off-by: Maciej Borzecki --- snap/name/validate.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/snap/name/validate.go b/snap/name/validate.go index cc19322c16f..0595cf1ac84 100644 --- a/snap/name/validate.go +++ b/snap/name/validate.go @@ -133,22 +133,24 @@ func ValidateHook(name string) error { return nil } -var validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") +// ValidAlias is a regular expression describing a valid alias +var ValidAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") // ValidateAlias checks if a string can be used as an alias name. func ValidateAlias(alias string) error { - valid := validAlias.MatchString(alias) + valid := ValidAlias.MatchString(alias) if !valid { return fmt.Errorf("invalid alias name: %q", alias) } return nil } +// ValidAppName is a regular expression describing a valid application name +var ValidAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") + // ValidateApp tells whether a string is a valid application name. func ValidateApp(n string) error { - var validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") - - if !validAppName.MatchString(n) { + if !ValidAppName.MatchString(n) { return fmt.Errorf("invalid app name: %q", n) } return nil From 7e8951af10bd9ee7b0bfb27b87c8f2ba676ac8df Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 19 Oct 2018 12:12:44 +0200 Subject: [PATCH 025/580] asserts: use app/alias regexps from snap/name Signed-off-by: Maciej Borzecki --- asserts/snap_asserts.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go index 51b200192e7..3a599cc30c1 100644 --- a/asserts/snap_asserts.go +++ b/asserts/snap_asserts.go @@ -23,13 +23,13 @@ import ( "bytes" "crypto" "fmt" - "regexp" "time" _ "golang.org/x/crypto/sha3" // expected for digests "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" + snapname "github.com/snapcore/snapd/snap/name" ) // SnapDeclaration holds a snap-declaration assertion, declaring a @@ -195,11 +195,6 @@ func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) ( return formatnum, nil } -var ( - validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") - validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") -) - func checkAliases(headers map[string]interface{}) (map[string]string, error) { value, ok := headers["aliases"] if !ok { @@ -221,13 +216,13 @@ func checkAliases(headers map[string]interface{}) (map[string]string, error) { } what := fmt.Sprintf(`in "aliases" item %d`, i+1) - name, err := checkStringMatchesWhat(aliasItem, "name", what, validAlias) + name, err := checkStringMatchesWhat(aliasItem, "name", what, snapname.ValidAlias) if err != nil { return nil, err } what = fmt.Sprintf(`for alias %q`, name) - target, err := checkStringMatchesWhat(aliasItem, "target", what, validAppName) + target, err := checkStringMatchesWhat(aliasItem, "target", what, snapname.ValidAppName) if err != nil { return nil, err } @@ -296,7 +291,7 @@ func assembleSnapDeclaration(assert assertionBase) (Assertion, error) { } // XXX: depracated, will go away later - autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias) + autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", snapname.ValidAlias) if err != nil { return nil, err } From a3d5ed74067ca15b2ca0afa54906626cb8ce3aee Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 19 Oct 2018 10:52:23 -0300 Subject: [PATCH 026/580] Fix shell check issues --- tests/nested/core-snap-refresh-on-core/task.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/nested/core-snap-refresh-on-core/task.yaml b/tests/nested/core-snap-refresh-on-core/task.yaml index d1563dbfe56..2efc017737e 100644 --- a/tests/nested/core-snap-refresh-on-core/task.yaml +++ b/tests/nested/core-snap-refresh-on-core/task.yaml @@ -6,6 +6,7 @@ details: | channel pointed by the NEW_CORE_CHANNEL env var. prepare: | + #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" create_nested_core_vm @@ -14,6 +15,7 @@ restore: | rm -f core_*.{assert,snap} execute: | + #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" if [ "$NEW_CORE_CHANNEL" = "" ]; then @@ -21,8 +23,8 @@ execute: | exit 1 fi - INITIAL_REV="$(get_nested_core_revision ${CORE_CHANNEL})" - NEW_REV="$(get_nested_core_revision ${NEW_CORE_CHANNEL})" + INITIAL_REV="$(get_nested_core_revision "${CORE_CHANNEL}")" + NEW_REV="$(get_nested_core_revision "${NEW_CORE_CHANNEL}")" # Install test snap execute_remote "snap install test-snapd-tools" From f2b9b3c5657ac880ae1e4967e30de94e85d88f95 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 23 Oct 2018 10:28:42 -0600 Subject: [PATCH 027/580] address review feedback --- interfaces/builtin/dotfiles.go | 23 +++++++++++++---------- interfaces/builtin/dotfiles_test.go | 5 +++++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index fcfcd64bc8b..4f6a3420ce0 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -60,13 +60,16 @@ func validatePaths(attrName string, paths []interface{}) error { if !strings.HasPrefix(np, "/") && !strings.HasPrefix(np, "$HOME/") { return fmt.Errorf(`%q must start with "/" or "$HOME"`, np) } + if !strings.HasPrefix(np, "$HOME/") && strings.Contains(np, "$HOME") { + return fmt.Errorf(`$HOME must only be used at the start of the path of %q`, np) + } + if strings.Contains(np, "@{") { + return fmt.Errorf(`%q should not use "@{"`, np) + } p := filepath.Clean(np) if p != np { return fmt.Errorf("%q must be clean", np) } - if strings.Contains(p, "..") { - return fmt.Errorf(`%q contains invalid ".."`, p) - } if strings.Contains(p, "~") { return fmt.Errorf(`%q contains invalid "~"`, p) } @@ -114,9 +117,9 @@ func formatPath(ip interface{}) (string, error) { return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil } -func addSnippet(spec *apparmor.Specification, perm string, paths []interface{}) error { - for _, rp := range paths { - p, err := formatPath(rp) +func allowPathAccess(spec *apparmor.Specification, perm string, paths []interface{}) error { + for _, rawPath := range paths { + p, err := formatPath(rawPath) if err != nil { return err } @@ -132,11 +135,11 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) spec.AddSnippet(dotfilesConnectedPlugAppArmor) - if err := addSnippet(spec, "rk,", reads); err != nil { - return fmt.Errorf(errPrefix+"%v", err) + if err := allowPathAccess(spec, "rk,", reads); err != nil { + return fmt.Errorf("%s%v", errPrefix, err) } - if err := addSnippet(spec, "rwkl,", writes); err != nil { - return fmt.Errorf(errPrefix+"%v", err) + if err := allowPathAccess(spec, "rwkl,", writes); err != nil { + return fmt.Errorf("%s%v", errPrefix, err) } return nil } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index 15f1ee2dac3..af7b9302be1 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -138,6 +138,11 @@ plugs: {`write: ""`, `"write" must be a list of strings`}, {`write: bar`, `"write" must be a list of strings`}, {`read: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, + {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, + {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, + {`read: [ "/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "/home/\$HOME/foo"`}, + {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, + {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, } for _, t := range testCases { From a679c552a4b43e7cf0f10b3b55555343f6beb664 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 26 Oct 2018 10:17:32 +0200 Subject: [PATCH 028/580] interfaces/apparmor: remove duplicate Setup Setup is already performed by Install, the second call is spurious and doesn't change anything. Signed-off-by: Zygmunt Krynicki --- interfaces/apparmor/backend_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 7c1212ae294..65738dd1c17 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -1696,9 +1696,6 @@ func (s *backendSuite) TestHomeIxRule(c *C) { snapInfo := s.InstallSnap(c, tc.opts, "", ifacetest.SambaYamlV1, 1) s.parserCmd.ForgetCalls() - err := s.Backend.Setup(snapInfo, tc.opts, s.Repo) - c.Assert(err, IsNil) - profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") data, err := ioutil.ReadFile(profile) c.Assert(err, IsNil) From e56a42b313075de407ef383d268c53d1340b4cf0 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Tue, 30 Oct 2018 13:38:17 +0100 Subject: [PATCH 029/580] updateDevice helper for hotplug. --- overlord/ifacestate/export_test.go | 1 + overlord/ifacestate/hotplug.go | 18 ++++++++++++ overlord/ifacestate/hotplug_test.go | 44 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go index 2cdb0a048b9..7dca55b3fa6 100644 --- a/overlord/ifacestate/export_test.go +++ b/overlord/ifacestate/export_test.go @@ -42,6 +42,7 @@ var ( SetHotplugAttrs = setHotplugAttrs GetHotplugSlots = getHotplugSlots SetHotplugSlots = setHotplugSlots + UpdateDevice = updateDevice ) func NewConnectOptsWithAutoSet() connectOpts { diff --git a/overlord/ifacestate/hotplug.go b/overlord/ifacestate/hotplug.go index f8540b89d0c..3b6b465fac0 100644 --- a/overlord/ifacestate/hotplug.go +++ b/overlord/ifacestate/hotplug.go @@ -27,6 +27,7 @@ import ( "unicode" "github.com/snapcore/snapd/interfaces/hotplug" + "github.com/snapcore/snapd/overlord/state" ) // List of attributes that determine the computation of default device key. @@ -173,3 +174,20 @@ func suggestedSlotName(devinfo *hotplug.HotplugDeviceInfo, fallbackName string) } return shortestName } + +// create tasks to disconnect slots of given device, update the slot in the repository, then connect it back. +func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[string]interface{}) *state.TaskSet { + hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of device %q", hotplugKey)) + setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey) + + updateSlot := st.NewTask("hotplug-update-slot", fmt.Sprintf("Update slot of interface %s, hotplug key %q", ifaceName, hotplugKey)) + setHotplugAttrs(updateSlot, ifaceName, hotplugKey) + updateSlot.Set("slot-attrs", newAttrs) + updateSlot.WaitFor(hotplugDisconnect) + + hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %s hotplug key %s", ifaceName, hotplugKey)) + setHotplugAttrs(hotplugConnect, ifaceName, hotplugKey) + hotplugConnect.WaitFor(updateSlot) + + return state.NewTaskSet(hotplugDisconnect, updateSlot, hotplugConnect) +} diff --git a/overlord/ifacestate/hotplug_test.go b/overlord/ifacestate/hotplug_test.go index 3fa0b33e222..a4c4a471dce 100644 --- a/overlord/ifacestate/hotplug_test.go +++ b/overlord/ifacestate/hotplug_test.go @@ -25,6 +25,7 @@ import ( "github.com/snapcore/snapd/interfaces/hotplug" "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/state" . "gopkg.in/check.v1" ) @@ -321,3 +322,46 @@ func (s *hotplugSuite) TestSuggestedSlotName(c *C) { c.Assert(slotName, Equals, data.outName) } } + +func (s *hotplugSuite) TestUpdateDeviceTasks(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + tss := ifacestate.UpdateDevice(st, "interface", "key", map[string]interface{}{"attr": "value"}) + c.Assert(tss, NotNil) + c.Assert(tss.Tasks(), HasLen, 3) + + task1 := tss.Tasks()[0] + c.Assert(task1.Kind(), Equals, "hotplug-disconnect") + + iface, key, err := ifacestate.GetHotplugAttrs(task1) + c.Assert(err, IsNil) + c.Assert(iface, Equals, "interface") + c.Assert(key, Equals, "key") + + task2 := tss.Tasks()[1] + c.Assert(task2.Kind(), Equals, "hotplug-update-slot") + iface, key, err = ifacestate.GetHotplugAttrs(task2) + c.Assert(err, IsNil) + c.Assert(iface, Equals, "interface") + c.Assert(key, Equals, "key") + var attrs map[string]interface{} + c.Assert(task2.Get("slot-attrs", &attrs), IsNil) + c.Assert(attrs, DeepEquals, map[string]interface{}{"attr": "value"}) + + task3 := tss.Tasks()[2] + c.Assert(task3.Kind(), Equals, "hotplug-connect") + iface, key, err = ifacestate.GetHotplugAttrs(task2) + c.Assert(err, IsNil) + c.Assert(iface, Equals, "interface") + c.Assert(key, Equals, "key") + + wt := task2.WaitTasks() + c.Assert(wt, HasLen, 1) + c.Assert(wt[0], DeepEquals, task1) + + wt = task3.WaitTasks() + c.Assert(wt, HasLen, 1) + c.Assert(wt[0], DeepEquals, task2) +} From 8097f99293a427766eb48ad0d335a06bb8ce93f9 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 5 Nov 2018 09:15:40 +0100 Subject: [PATCH 030/580] apparmor: add new TestValidateFreeFromAAREUnhappy test --- interfaces/apparmor/apparmor_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/interfaces/apparmor/apparmor_test.go b/interfaces/apparmor/apparmor_test.go index c87fbf9d48a..9bf9c288b68 100644 --- a/interfaces/apparmor/apparmor_test.go +++ b/interfaces/apparmor/apparmor_test.go @@ -227,3 +227,11 @@ func (s *appArmorSuite) TestValidateFreeFromAAREUnhappy(c *C) { c.Check(apparmor.ValidateFreeFromAARE(s), ErrorMatches, ".* contains a reserved apparmor char from .*", Commentf("%q is not raising an error", s)) } } + +func (s *appArmorSuite) TestValidateFreeFromAAREhappy(c *C) { + var testCases = []string{"foo", "BaR", "b-z", "foo+bar", "b00m!", "be/ep", "a%b", "a&b", "a(b", "a)b", "a=b", "a#b", "a~b", "a'b", "a_b", "a,b", "a;b", "a>b", "a Date: Mon, 5 Nov 2018 09:29:30 +0100 Subject: [PATCH 031/580] interfaces/buildin: build AppArmorConnectedPlug in dotfilesInterface into a buffer first --- interfaces/builtin/dotfiles.go | 13 ++++++++----- interfaces/builtin/dotfiles_test.go | 10 +++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 4f6a3420ce0..9b4183970ff 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -20,6 +20,7 @@ package builtin import ( + "bytes" "fmt" "path/filepath" "strings" @@ -117,13 +118,13 @@ func formatPath(ip interface{}) (string, error) { return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil } -func allowPathAccess(spec *apparmor.Specification, perm string, paths []interface{}) error { +func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error { for _, rawPath := range paths { p, err := formatPath(rawPath) if err != nil { return err } - spec.AddSnippet(fmt.Sprintf("%s %s", p, perm)) + fmt.Fprintf(buf, "%s %s\n", p, perm) } return nil } @@ -134,13 +135,15 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati _ = plug.Attr("write", &writes) errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) - spec.AddSnippet(dotfilesConnectedPlugAppArmor) - if err := allowPathAccess(spec, "rk,", reads); err != nil { + buf := bytes.NewBufferString(dotfilesConnectedPlugAppArmor) + if err := allowPathAccess(buf, "rk,", reads); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } - if err := allowPathAccess(spec, "rwkl,", writes); err != nil { + if err := allowPathAccess(buf, "rwkl,", writes); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } + spec.AddSnippet(buf.String()) + return nil } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/dotfiles_test.go index af7b9302be1..f071b5cbf7d 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/dotfiles_test.go @@ -79,15 +79,15 @@ func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` # Description: Can access specific files or directories. # This is restricted because it gives file access to arbitrary locations. - +owner "@{HOME}/.read-dir1{,/,/**}" rk, "/etc/read-dir2{,/,/**}" rk, +owner "@{HOME}/.read-file2{,/,/**}" rk, "/etc/read-file2{,/,/**}" rk, +owner "@{HOME}/.write-dir1{,/,/**}" rwkl, "/etc/write-dir2{,/,/**}" rwkl, +owner "@{HOME}/.write-file2{,/,/**}" rwkl, "/etc/write-file2{,/,/**}" rwkl, -owner "@{HOME}/.read-dir1{,/,/**}" rk, -owner "@{HOME}/.read-file2{,/,/**}" rk, -owner "@{HOME}/.write-dir1{,/,/**}" rwkl, -owner "@{HOME}/.write-file2{,/,/**}" rwkl,`) +`) } func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { From 6331619cb688533e526af41f826776f56c1e34fd Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Tue, 6 Nov 2018 13:26:31 +0100 Subject: [PATCH 032/580] hotplug-remove-slot task handler. --- overlord/ifacestate/handlers.go | 52 ++++++++++ overlord/ifacestate/ifacemgr.go | 1 + overlord/ifacestate/ifacestate_test.go | 125 +++++++++++++++++++++++++ 3 files changed, 178 insertions(+) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 19414f9911b..9cd8bf75024 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1181,3 +1181,55 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error task.SetStatus(state.DoneStatus) return nil } + +// doHotplugRemoveSlot removes hotplug slot for given device from the repository in response to udev "remove" event. +// This task must necessarily be run after all affected slot gets disconnected in the repo. +func (m *InterfaceManager) doHotplugRemoveSlot(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + ifaceName, hotplugKey, err := getHotplugAttrs(task) + if err != nil { + return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) + } + + slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return fmt.Errorf("cannot determine slots: %s", err) + } + if slot != nil { + if err := m.repo.RemoveSlot(slot.Snap.InstanceName(), slot.Name); err != nil { + return fmt.Errorf("cannot remove slot %s of snap %q: %s", slot.Snap.InstanceName(), slot.Name, err) + } + } + + stateSlots, err := getHotplugSlots(st) + if err != nil { + return fmt.Errorf("internal error obtaining hotplug slots: %v", err.Error()) + } + + // remove the slot from hotplug-slots in the state as long as there are no connections referencing it, + // including connection with hotplug-gone=true. +Loop: + for slotName, def := range stateSlots { + if def.Interface != ifaceName || def.HotplugKey != hotplugKey { + continue + } + conns, err := getConns(st) + if err != nil { + return err + } + for _, conn := range conns { + if conn.Interface == def.Interface && conn.HotplugKey == def.HotplugKey { + // there is a connection referencing this slot, do not remove it + break Loop + } + } + delete(stateSlots, slotName) + setHotplugSlots(st, stateSlots) + break + } + + return nil +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 4b1d6df32ca..3404ff6f67e 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -81,6 +81,7 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.T addHandler("auto-connect", m.doAutoConnect, m.undoAutoConnect) addHandler("gadget-connect", m.doGadgetConnect, nil) addHandler("auto-disconnect", m.doAutoDisconnect, nil) + addHandler("hotplug-remove-slot", m.doHotplugRemoveSlot, nil) // helper for ubuntu-core -> core addHandler("transition-ubuntu-core", m.doTransitionUbuntuCore, m.undoTransitionUbuntuCore) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 229cf218207..764b15b161b 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4905,3 +4905,128 @@ func (s *interfaceManagerSuite) TestAttributesRestoredFromConns(c *C) { c.Check(number, Equals, int64(1)) c.Check(restoredSlot.Attr("dynamic-number", &dynnumber), IsNil) } + +func (s *interfaceManagerSuite) TestHotplugRemoveSlot(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + // sanity check + c.Assert(repo.Slot("core", "hotplugslot"), NotNil) + + s.state.Lock() + defer s.state.Unlock() + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }, + "otherslot": map[string]interface{}{ + "name": "otherslot", + "interface": "test", + "hotplug-key": "5678", + }}) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-remove-slot", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + chg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + // hotplugslot is removed from the repository and from the state + c.Assert(repo.Slot("core", "hotplugslot"), IsNil) + slot, err := repo.SlotForHotplugKey("test", "1234") + c.Assert(err, IsNil) + c.Assert(slot, IsNil) + + var hotplugSlots map[string]interface{} + c.Assert(s.state.Get("hotplug-slots", &hotplugSlots), IsNil) + c.Assert(hotplugSlots, DeepEquals, map[string]interface{}{ + "otherslot": map[string]interface{}{ + "name": "otherslot", + "interface": "test", + "hotplug-key": "5678", + }}) +} + +func (s *interfaceManagerSuite) TestHotplugRemoveSlotWhenConnected(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + // sanity check + c.Assert(repo.Slot("core", "hotplugslot"), NotNil) + + s.state.Lock() + defer s.state.Unlock() + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "hotplug-gone": true, + }}) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-remove-slot", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + chg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + // hotplugslot is removed from the repository but not from the state, because of existing connection + c.Assert(repo.Slot("core", "hotplugslot"), IsNil) + slot, err := repo.SlotForHotplugKey("test", "1234") + c.Assert(err, IsNil) + c.Assert(slot, IsNil) + + var hotplugSlots map[string]interface{} + c.Assert(s.state.Get("hotplug-slots", &hotplugSlots), IsNil) + c.Assert(hotplugSlots, DeepEquals, map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) +} From 25c2dcf52c5f48b953271fc67cce8cdb0f90f941 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 6 Nov 2018 17:37:28 -0300 Subject: [PATCH 033/580] Split nested vm suite on core and classic and new snapshots test This is split because in the future the setups will be different in case the nested vm is core or classic. Also it is included a new test for snapshots which tests in case the core is refreshed and reverted. Until the snapshots feature is in stable we need to run from beta to edge. To run it do: SPREAD_CORE_CHANNEL=beta SPREAD_CORE_REFRESH_CHANNEL=edge spread google-nested:ubuntu-16.04-64:tests/nested/classic/snapshots-with-core-refresh-revert Those tests will be executed on each new core on edge soon as part of a spread cron job. --- spread.yaml | 26 +++++++- tests/nested/{ => classic}/hot-plug/task.yaml | 11 +--- .../task.yaml | 61 +++++++++++++++++++ tests/nested/{ => core}/core-revert/task.yaml | 0 .../extra-snaps-assertions/task.yaml | 0 tests/nested/{ => core}/image-build/task.yaml | 0 6 files changed, 86 insertions(+), 12 deletions(-) rename tests/nested/{ => classic}/hot-plug/task.yaml (83%) create mode 100644 tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml rename tests/nested/{ => core}/core-revert/task.yaml (100%) rename tests/nested/{ => core}/extra-snaps-assertions/task.yaml (100%) rename tests/nested/{ => core}/image-build/task.yaml (100%) diff --git a/spread.yaml b/spread.yaml index 6f3d693ab70..2530a05499b 100644 --- a/spread.yaml +++ b/spread.yaml @@ -620,7 +620,7 @@ suites: restore: | "$TESTSLIB"/prepare-restore.sh --restore-suite - tests/nested/: + tests/nested/classic/: summary: Tests for nested images # Test cases are not yet ported to Fedora/openSUSE that is why # we keep them disabled. A later PR will enable most tests and @@ -629,7 +629,29 @@ suites: environment: NESTED_ARCH: "$(HOST: echo ${SPREAD_NESTED_ARCH:-amd64})" NESTED_SYSTEM: "$(HOST: echo ${SPREAD_NESTED_SYSTEM:-xenial})" - CORE_REFRESH_CHANNEL: "$(HOST: echo ${SPREAD_CORE_REFRESH_CHANNEL:-candidate})" + CORE_CHANNEL: "$(HOST: echo ${SPREAD_CORE_CHANNEL:-stable})" + CORE_REFRESH_CHANNEL: "$(HOST: echo ${SPREAD_CORE_REFRESH_CHANNEL:-edge})" + manual: true + prepare: | + "$TESTSLIB"/prepare-restore.sh --prepare-suite + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB"/pkgdb.sh + distro_install_package snapd qemu genisoimage sshpass qemu-kvm cloud-image-utils ubuntu-image + prepare-each: | + "$TESTSLIB"/prepare-restore.sh --prepare-suite-each + restore: | + "$TESTSLIB"/prepare-restore.sh --restore-suite + + tests/nested/core/: + summary: Tests for nested images + # Test cases are not yet ported to Fedora/openSUSE that is why + # we keep them disabled. A later PR will enable most tests and + # drop this blacklist. + backends: [google-nested] + environment: + NESTED_ARCH: "$(HOST: echo ${SPREAD_NESTED_ARCH:-amd64})" + CORE_CHANNEL: "$(HOST: echo ${SPREAD_CORE_CHANNEL:-stable})" + CORE_REFRESH_CHANNEL: "$(HOST: echo ${SPREAD_CORE_REFRESH_CHANNEL:-edge})" manual: true prepare: | #shellcheck source=tests/lib/pkgdb.sh diff --git a/tests/nested/hot-plug/task.yaml b/tests/nested/classic/hot-plug/task.yaml similarity index 83% rename from tests/nested/hot-plug/task.yaml rename to tests/nested/classic/hot-plug/task.yaml index ce4ca48f6f4..6e1396c0e2b 100644 --- a/tests/nested/hot-plug/task.yaml +++ b/tests/nested/classic/hot-plug/task.yaml @@ -1,12 +1,10 @@ summary: create ubuntu classic image, install snapd and test hot plug feature prepare: | - "$TESTSLIB"/prepare-restore.sh --prepare-suite - "$TESTSLIB"/prepare-restore.sh --prepare-suite-each - #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" create_nested_classic_vm + copy_remote "${GOHOME}"/snapd_*.deb execute_remote "sudo apt update" execute_remote "sudo apt install -y ./snapd_*.deb" @@ -18,13 +16,6 @@ restore: | . "$TESTSLIB/nested.sh" destroy_nested_vm - "$TESTSLIB"/prepare-restore.sh --restore-suite-each - "$TESTSLIB"/prepare-restore.sh --restore-suite - - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - distro_install_package snapd - execute: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" diff --git a/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml new file mode 100644 index 00000000000..09e321e6a3f --- /dev/null +++ b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml @@ -0,0 +1,61 @@ +summary: test snapshots work when core snap is refreshed and reverted + +prepare: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + create_nested_classic_vm + + # configure hosts file + execute_remote 'echo "127.0.1.1 $HOSTNAME" | sudo tee /etc/hosts' + + # install snapd and snaps on nested vm + copy_remote "${GOHOME}"/snapd_*.deb + execute_remote "sudo apt update" + execute_remote "sudo apt install -y ./snapd_*.deb" + execute_remote "sudo snap install test-snapd-tools" + execute_remote "sudo snap install test-snapd-rsync" + +restore: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + destroy_nested_vm + +execute: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + # Make sure the core in the nested vm is the correct one + execute_remote "sudo snap refresh core --${CORE_CHANNEL}" + + # use the snaps, so they create the dirs: + execute_remote "sudo test-snapd-tools.echo" + execute_remote "sudo test-snapd-rsync.rsync --version >/dev/null" + for snap in test-snapd-tools test-snapd-rsync; do + execute_remote "echo "hello versioned $snap" | sudo tee /root/snap/$snap/current/canary.txt" + execute_remote "echo "hello common $snap" | sudo tee /root/snap/$snap/common/canary.txt" + done + + # create snapshot, grab its id + SET_ID=$( execute_remote "sudo snap save test-snapd-tools test-snapd-rsync" | cut -d\ -f1 | tail -n1 ) + + # delete the canary files + execute_remote "sudo rm /root/snap/test-snapd-tools/{current,common}/canary.txt" + execute_remote "sudo rm /root/snap/test-snapd-rsync/{current,common}/canary.txt" + + # when the core is refreshed the snap snapshot can be restored" + execute_remote "sudo snap refresh core --${CORE_REFRESH_CHANNEL}" + execute_remote "sudo snap restore "$SET_ID" test-snapd-rsync" + test "$( execute_remote "sudo cat /root/snap/test-snapd-rsync/current/canary.txt" )" = "hello versioned test-snapd-rsync" + test "$( execute_remote "sudo cat /root/snap/test-snapd-rsync/common/canary.txt" )" = "hello common test-snapd-rsync" + execute_remote "sudo test ! -f /root/snap/test-snapd-tools/common/canary.txt" + execute_remote "sudo test ! -f /root/snap/test-snapd-tools/current/canary.txt" + + # when the core is reverted the snap snapshot can be restored" + execute_remote "sudo snap revert core" + execute_remote "sudo snap restore "$SET_ID" test-snapd-tools" + test "$( execute_remote "sudo cat /root/snap/test-snapd-tools/current/canary.txt" )" = "hello versioned test-snapd-tools" + test "$( execute_remote "sudo cat /root/snap/test-snapd-tools/common/canary.txt" )" = "hello common test-snapd-tools" + + echo "And the snapshot can be removed" + execute_remote "sudo snap forget $SET_ID" + execute_remote "sudo snap saved --id=$SET_ID" | MATCH "No snapshots found" diff --git a/tests/nested/core-revert/task.yaml b/tests/nested/core/core-revert/task.yaml similarity index 100% rename from tests/nested/core-revert/task.yaml rename to tests/nested/core/core-revert/task.yaml diff --git a/tests/nested/extra-snaps-assertions/task.yaml b/tests/nested/core/extra-snaps-assertions/task.yaml similarity index 100% rename from tests/nested/extra-snaps-assertions/task.yaml rename to tests/nested/core/extra-snaps-assertions/task.yaml diff --git a/tests/nested/image-build/task.yaml b/tests/nested/core/image-build/task.yaml similarity index 100% rename from tests/nested/image-build/task.yaml rename to tests/nested/core/image-build/task.yaml From 680d77824413bc573b14e26f1254e7be09b3ce22 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 6 Nov 2018 23:03:43 -0300 Subject: [PATCH 034/580] Fix shellcheks issues --- .../classic/snapshots-with-core-refresh-revert/task.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml index 09e321e6a3f..f12191c0e5a 100644 --- a/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml +++ b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml @@ -6,6 +6,7 @@ prepare: | create_nested_classic_vm # configure hosts file + # shellcheck disable=SC2086 execute_remote 'echo "127.0.1.1 $HOSTNAME" | sudo tee /etc/hosts' # install snapd and snaps on nested vm @@ -44,7 +45,7 @@ execute: | # when the core is refreshed the snap snapshot can be restored" execute_remote "sudo snap refresh core --${CORE_REFRESH_CHANNEL}" - execute_remote "sudo snap restore "$SET_ID" test-snapd-rsync" + execute_remote "sudo snap restore $SET_ID test-snapd-rsync" test "$( execute_remote "sudo cat /root/snap/test-snapd-rsync/current/canary.txt" )" = "hello versioned test-snapd-rsync" test "$( execute_remote "sudo cat /root/snap/test-snapd-rsync/common/canary.txt" )" = "hello common test-snapd-rsync" execute_remote "sudo test ! -f /root/snap/test-snapd-tools/common/canary.txt" @@ -52,7 +53,7 @@ execute: | # when the core is reverted the snap snapshot can be restored" execute_remote "sudo snap revert core" - execute_remote "sudo snap restore "$SET_ID" test-snapd-tools" + execute_remote "sudo snap restore $SET_ID test-snapd-tools" test "$( execute_remote "sudo cat /root/snap/test-snapd-tools/current/canary.txt" )" = "hello versioned test-snapd-tools" test "$( execute_remote "sudo cat /root/snap/test-snapd-tools/common/canary.txt" )" = "hello common test-snapd-tools" From 63d23cd2fb360fe8d8a7c7bc90c825ce7e1f34ca Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Wed, 7 Nov 2018 13:25:08 +0100 Subject: [PATCH 035/580] hotplug-update-slot task handler. --- overlord/ifacestate/handlers.go | 54 +++++++++++ overlord/ifacestate/ifacemgr.go | 1 + overlord/ifacestate/ifacestate_test.go | 126 +++++++++++++++++++++++++ 3 files changed, 181 insertions(+) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 19414f9911b..45709c2475a 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1181,3 +1181,57 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error task.SetStatus(state.DoneStatus) return nil } + +// doHotplugUpdateSlot updates static attributes of a hotplug slot for given device. +func (m *InterfaceManager) doHotplugUpdateSlot(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + ifaceName, hotplugKey, err := getHotplugAttrs(task) + if err != nil { + return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) + } + var attrs map[string]interface{} + if err := task.Get("slot-attrs", &attrs); err != nil { + return fmt.Errorf("internal error: cannot get slot-attrs attribute for device %s, interface %s: %s", hotplugKey, ifaceName, err) + } + + stateSlots, err := getHotplugSlots(st) + if err != nil { + return fmt.Errorf("internal error obtaining hotplug slots: %v", err.Error()) + } + + slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return fmt.Errorf("internal error: cannot determine slot for device %s, interface %s: %s", hotplugKey, ifaceName, err) + } + if slot == nil { + return nil + } + + conns, err := m.repo.ConnectionsForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return fmt.Errorf("internal error: cannot determine connections for device %s, interface %s: %s", hotplugKey, ifaceName, err) + } + + // hotplug-update-slot is meant to be run as part of a change that first disconnects all slots, then updates the slot and finally + // reconnects all connections, so that disconnect- and connect- hooks are run with old and new attributes, respectively. In theory + // we should never hit this condition as it should be prevented by conflict logic. + if len(conns) > 0 { + return fmt.Errorf("internal error: cannot update slot %s while connected", slot.Name) + } + + if slotSpec, ok := stateSlots[slot.Name]; ok { + slotSpec.StaticAttrs = attrs + stateSlots[slot.Name] = slotSpec + setHotplugSlots(st, stateSlots) + + // XXX: this is ugly and relies on the slot infos being kept as pointers in the repository + slot.Attrs = attrs + } else { + return fmt.Errorf("internal error: cannot find slot %s for device %s", slot.Name, hotplugKey) + } + + return nil +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 4b1d6df32ca..a619f06675c 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -81,6 +81,7 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.T addHandler("auto-connect", m.doAutoConnect, m.undoAutoConnect) addHandler("gadget-connect", m.doGadgetConnect, nil) addHandler("auto-disconnect", m.doAutoDisconnect, nil) + addHandler("hotplug-update-slot", m.doHotplugUpdateSlot, nil) // helper for ubuntu-core -> core addHandler("transition-ubuntu-core", m.doTransitionUbuntuCore, m.undoTransitionUbuntuCore) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 229cf218207..2e42d92e2bc 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4905,3 +4905,129 @@ func (s *interfaceManagerSuite) TestAttributesRestoredFromConns(c *C) { c.Check(number, Equals, int64(1)) c.Check(restoredSlot.Attr("dynamic-number", &dynnumber), IsNil) } + +func (s *interfaceManagerSuite) TestHotplugUpdateSlot(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + // sanity check + c.Assert(repo.Slot("core", "hotplugslot"), NotNil) + + s.state.Lock() + defer s.state.Unlock() + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-update-slot", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + t.Set("slot-attrs", map[string]interface{}{"foo": "bar"}) + chg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + // hotplugslot is updated int the repository + slot := repo.Slot("core", "hotplugslot") + c.Assert(slot, NotNil) + c.Assert(slot.Attrs, DeepEquals, map[string]interface{}{"foo": "bar"}) + + var hotplugSlots map[string]interface{} + c.Assert(s.state.Get("hotplug-slots", &hotplugSlots), IsNil) + c.Assert(hotplugSlots, DeepEquals, map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + "static-attrs": map[string]interface{}{"foo": "bar"}, + }}) +} + +func (s *interfaceManagerSuite) TestHotplugUpdateSlotWhenConnected(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + consumer := s.mockSnap(c, consumerYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + err = repo.AddPlug(consumer.Plugs["plug"]) + c.Assert(err, IsNil) + + // sanity check + c.Assert(repo.Slot("core", "hotplugslot"), NotNil) + + s.state.Lock() + defer s.state.Unlock() + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "hotplug-gone": true, + }}) + _, err = repo.Connect(&interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"}}, + nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-update-slot", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + t.Set("slot-attrs", map[string]interface{}{}) + chg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n.*internal error: cannot update slot hotplugslot while connected.*`) + + // hotplugslot is not removed because of existing connection + c.Assert(repo.Slot("core", "hotplugslot"), NotNil) + + var hotplugSlots map[string]interface{} + c.Assert(s.state.Get("hotplug-slots", &hotplugSlots), IsNil) + c.Assert(hotplugSlots, DeepEquals, map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) +} From 2daa2c69d28f3ad443cfbd1d83429dbca1f68fa1 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 8 Nov 2018 13:34:15 +0100 Subject: [PATCH 036/580] Handler for hotplug-disconnect task. --- overlord/ifacestate/handlers.go | 61 ++++++++++++++++++++ overlord/ifacestate/ifacemgr.go | 1 + overlord/ifacestate/ifacestate_test.go | 80 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 19414f9911b..28811f7beca 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1181,3 +1181,64 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error task.SetStatus(state.DoneStatus) return nil } + +// doHotplugDisconnect creates task(s) to disconnect connections and remove slots in response to hotplug "remove" event. +func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + core, err := snapstate.CoreInfo(st) + if err != nil { + return err + } + coreSnapName := core.InstanceName() + + ifaceName, hotplugKey, err := getHotplugAttrs(task) + if err != nil { + return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) + } + + connections, err := m.repo.ConnectionsForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return err + } + if len(connections) == 0 { + return nil + } + + // check for conflicts on all connections first before creating disconnect hooks + for _, connRef := range connections { + if err := checkDisconnectConflicts(st, coreSnapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { + if _, retry := err.(*state.Retry); retry { + logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", coreSnapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap) + task.Logf("Waiting for conflicting change in progress...") + return err // will retry + } + return fmt.Errorf("cannot check conflicts when disconnecting interfaces: %s", err) + } + } + + dts := state.NewTaskSet() + for _, connRef := range connections { + conn, err := m.repo.Connection(connRef) + if err != nil { + // this should never happen since we get all connections from the repo + return fmt.Errorf("internal error: cannot get connection %q: %s", connRef, err) + } + // "by-hotplug" flag indicates it's a disconnect triggered as part of hotplug removal. + ts, err := disconnectTasks(st, conn, disconnectOpts{ByHotplug: true}) + if err != nil { + return fmt.Errorf("internal error: cannot create disconnect tasks: %s", err) + } + dts.AddAll(ts) + } + + snapstate.InjectTasks(task, dts) + st.EnsureBefore(0) + + // make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again + task.SetStatus(state.DoneStatus) + + return nil +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 4b1d6df32ca..091b6da88c0 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -81,6 +81,7 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.T addHandler("auto-connect", m.doAutoConnect, m.undoAutoConnect) addHandler("gadget-connect", m.doGadgetConnect, nil) addHandler("auto-disconnect", m.doAutoDisconnect, nil) + addHandler("hotplug-disconnect", m.doHotplugDisconnect, nil) // helper for ubuntu-core -> core addHandler("transition-ubuntu-core", m.doTransitionUbuntuCore, m.undoTransitionUbuntuCore) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 229cf218207..1e85770cb06 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4905,3 +4905,83 @@ func (s *interfaceManagerSuite) TestAttributesRestoredFromConns(c *C) { c.Check(number, Equals, int64(1)) c.Check(restoredSlot.Attr("dynamic-number", &dynnumber), IsNil) } + +func (s *interfaceManagerSuite) TestHotplugDisconnect(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // mock the consumer + si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)} + testSnap := snaptest.MockSnapInstance(c, "", consumerYaml, si) + c.Assert(testSnap.Plugs["plug"], NotNil) + c.Assert(repo.AddPlug(testSnap.Plugs["plug"]), IsNil) + snapstate.Set(s.state, "consumer", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + }}) + _, err = repo.Connect(&interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"}}, + nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-disconnect", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + chg.AddTask(t) + + s.state.Unlock() + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + c.Assert(chg.Err(), IsNil) + + var byHotplug bool + for _, t := range s.state.Tasks() { + // the 'disconnect' task created by hotplug-disconnect should have by-hotplug flag set + if t.Kind() == "disconnect" { + c.Assert(t.Get("by-hotplug", &byHotplug), IsNil) + } + } + c.Assert(byHotplug, Equals, true) + + // hotplug-gone flag on the connection is set + var conns map[string]interface{} + c.Assert(s.state.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "hotplug-gone": true, + }}) +} From 2f58208c8aee8f96c065572c07f9f18e28c35e17 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 8 Nov 2018 11:17:41 +0100 Subject: [PATCH 037/580] Handler for hotplug-connect task. --- overlord/ifacestate/handlers.go | 140 +++++++++++++++++++++++++ overlord/ifacestate/ifacemgr.go | 1 + overlord/ifacestate/ifacestate.go | 2 +- overlord/ifacestate/ifacestate_test.go | 70 +++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 19414f9911b..0af7b3d1bda 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -436,6 +436,7 @@ func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error { DynamicSlotAttrs: conn.Slot.DynamicAttrs(), Auto: autoConnect, ByGadget: byGadget, + HotplugKey: slot.HotplugKey, } setConns(st, conns) @@ -1181,3 +1182,142 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error task.SetStatus(state.DoneStatus) return nil } + +// doHotplugConnect creates task(s) to (re)create connections in response to hotplug "add" event. +func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + conns, err := getConns(st) + if err != nil { + return err + } + + ifaceName, hotplugKey, err := getHotplugAttrs(task) + if err != nil { + return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) + } + + // find old connections for slots of this device - note we can't ask the repository since we need + // to recreate old connections that are only remembered in the state. + connsForDevice := findConnsForHotplugKey(conns, ifaceName, hotplugKey) + + // we see this device for the first time (or it didn't have any connected slot before) + if len(connsForDevice) == 0 { + return m.autoconnectNewDevice(task, ifaceName, hotplugKey) + } + + // recreate old connections + var recreate []*interfaces.ConnRef + for _, id := range connsForDevice { + conn := conns[id] + // device was not unplugged, this is the case if snapd is restarted and we enumerate devices. + // note, the situation where device was not unplugged but has changed is handled + // by hotlugDeviceAdded handler - updateDevice. + if !conn.HotplugGone || conn.Undesired { + continue + } + + // the device was unplugged while connected, so it had disconnect hooks run; recreate the connection + connRef, err := interfaces.ParseConnRef(id) + if err != nil { + return err + } + + if err := checkAutoconnectConflicts(st, task, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { + if _, retry := err.(*state.Retry); retry { + task.Logf("hotplug connect will be retried because of %q - %q conflict", connRef.PlugRef.Snap, connRef.SlotRef.Snap) + return err // will retry + } + return fmt.Errorf("hotplug connect conflict check failed: %s", err) + } + recreate = append(recreate, connRef) + } + + if len(recreate) == 0 { + return nil + } + + // Create connect tasks and interface hooks + connectTs := state.NewTaskSet() + for _, conn := range recreate { + ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: conns[conn.ID()].Auto}) + if err != nil { + return fmt.Errorf("internal error: connect of %q failed: %s", conn, err) + } + connectTs.AddAll(ts) + } + + if len(connectTs.Tasks()) > 0 { + snapstate.InjectTasks(task, connectTs) + st.EnsureBefore(0) + } + + // make sure that we add tasks and mark this task done in the same atomic write, otherwise there is a risk of re-adding tasks again + task.SetStatus(state.DoneStatus) + + return nil +} + +func (m *InterfaceManager) autoconnectNewDevice(task *state.Task, ifaceName, hotplugKey string) error { + slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return err + } + + st := task.State() + autochecker, err := newAutoConnectChecker(st) + if err != nil { + return err + } + + instanceName := slot.Snap.InstanceName() + candidates := m.repo.AutoConnectCandidatePlugs(instanceName, slot.Name, autochecker.check) + var newconns []*interfaces.ConnRef + // Auto-connect the slots + for _, plug := range candidates { + // make sure slot is the only viable + // connection for plug, same check as if we were + // considering auto-connections from plug + candSlots := m.repo.AutoConnectCandidateSlots(plug.Snap.InstanceName(), plug.Name, autochecker.check) + if len(candSlots) != 1 || candSlots[0].String() != slot.String() { + crefs := make([]string, len(candSlots)) + for i, candidate := range candSlots { + crefs[i] = candidate.String() + } + task.Logf("cannot auto-connect slot %s to %s, candidates found: %s", slot, plug, strings.Join(crefs, ", ")) + continue + } + + if err := checkAutoconnectConflicts(st, task, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { + if _, retry := err.(*state.Retry); retry { + logger.Debugf("auto-connect of snap %q will be retried because of %q - %q conflict", instanceName, plug.Snap.InstanceName(), slot.Snap.InstanceName()) + task.Logf("Waiting for conflicting change in progress...") + return err // will retry + } + return fmt.Errorf("auto-connect conflict check failed: %s", err) + } + connRef := interfaces.NewConnRef(plug, slot) + newconns = append(newconns, connRef) + } + + autots := state.NewTaskSet() + // Create connect tasks and interface hooks + for _, conn := range newconns { + ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: true}) + if err != nil { + return fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err) + } + autots.AddAll(ts) + } + + if len(autots.Tasks()) > 0 { + snapstate.InjectTasks(task, autots) + + st.EnsureBefore(0) + } + + task.SetStatus(state.DoneStatus) + return nil +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 4b1d6df32ca..8ccb115ff12 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -81,6 +81,7 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.T addHandler("auto-connect", m.doAutoConnect, m.undoAutoConnect) addHandler("gadget-connect", m.doGadgetConnect, nil) addHandler("auto-disconnect", m.doAutoDisconnect, nil) + addHandler("hotplug-connect", m.doHotplugConnect, nil) // helper for ubuntu-core -> core addHandler("transition-ubuntu-core", m.doTransitionUbuntuCore, m.undoTransitionUbuntuCore) diff --git a/overlord/ifacestate/ifacestate.go b/overlord/ifacestate/ifacestate.go index 8bd5a8734d4..fa0ae563457 100644 --- a/overlord/ifacestate/ifacestate.go +++ b/overlord/ifacestate/ifacestate.go @@ -114,7 +114,7 @@ func connect(st *state.State, plugSnap, plugName, slotSnap, slotName string, fla return nil, err } connRef := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plugName}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slotName}} - if conn, ok := conns[connRef.ID()]; ok && conn.Undesired == false { + if conn, ok := conns[connRef.ID()]; ok && conn.Undesired == false && conn.HotplugGone == false { return nil, &ErrAlreadyConnected{Connection: connRef} } diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 229cf218207..4fa5e80c36f 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4905,3 +4905,73 @@ func (s *interfaceManagerSuite) TestAttributesRestoredFromConns(c *C) { c.Check(number, Equals, int64(1)) c.Check(restoredSlot.Attr("dynamic-number", &dynnumber), IsNil) } + +func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { + s.MockModel(c, nil) + coreInfo := s.mockSnap(c, coreSnapYaml) + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // mock the consumer + si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)} + testSnap := snaptest.MockSnapInstance(c, "", consumerYaml, si) + c.Assert(testSnap.Plugs["plug"], NotNil) + c.Assert(repo.AddPlug(testSnap.Plugs["plug"]), IsNil) + snapstate.Set(s.state, "consumer", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + // simulate a device that was known and connected before + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "hotplug-gone": true, + }}) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-connect", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + chg.AddTask(t) + + s.state.Unlock() + for i := 0; i < 5; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + var conns map[string]interface{} + c.Assert(s.state.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "plug-static": map[string]interface{}{"attr1": "value1"}, + }}) +} From e302605177e1e4d080d4b1455a24e213e800eb28 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Thu, 8 Nov 2018 22:16:41 -0300 Subject: [PATCH 038/580] New test for snapshots with more than 1 user --- tests/main/snapshot-users/task.yaml | 122 ++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/main/snapshot-users/task.yaml diff --git a/tests/main/snapshot-users/task.yaml b/tests/main/snapshot-users/task.yaml new file mode 100644 index 00000000000..06c0a3d21bc --- /dev/null +++ b/tests/main/snapshot-users/task.yaml @@ -0,0 +1,122 @@ +summary: Check that the basic snapshots functionality works for different users + +prepare: | + snap install test-snapd-tools + +execute: | + # use the snaps, so they create the dirs + test-snapd-tools.echo + su -p -c "/snap/bin/test-snapd-tools.echo" test + + # drop in canaries for both users + echo "hello versioned test-snapd-tools" > /root/snap/test-snapd-tools/current/canary.txt + echo "hello common test-snapd-tools" > /root/snap/test-snapd-tools/common/canary.txt + echo "hello versioned test-snapd-tools" > /home/test/snap/test-snapd-tools/current/canary.txt + echo "hello common test-snapd-tools" > /home/test/snap/test-snapd-tools/common/canary.txt + + # create different snapshots for each user + SET_ID_ROOT=$( snap save --users=root test-snapd-tools | cut -d\ -f1 | tail -n1 ) + SET_ID_TEST=$( snap save --users=test test-snapd-tools | cut -d\ -f1 | tail -n1 ) + + # Update content on canary files + echo "content updated" > /root/snap/test-snapd-tools/current/canary.txt + echo "content updated" > /root/snap/test-snapd-tools/common/canary.txt + echo "content updated" > /home/test/snap/test-snapd-tools/current/canary.txt + echo "content updated" > /home/test/snap/test-snapd-tools/common/canary.txt + + # create snapshots for both users + SET_ID_BOTH=$( snap save --users=root,test test-snapd-tools | cut -d\ -f1 | tail -n1 ) + + # Add more files and dirs for the test user and save snapshot for both users + mkdir -p /home/test/snap/test-snapd-tools/current/canary/ + echo "content updated" > /home/test/snap/test-snapd-tools/current/canary/canary.txt + SET_ID_NONE=$( snap save test-snapd-tools | cut -d\ -f1 | tail -n1 ) + + # check all the snapshots include the correct snap + snap saved --id="$SET_ID_ROOT" | MATCH test-snapd-tools + snap saved --id="$SET_ID_TEST" | MATCH test-snapd-tools + snap saved --id="$SET_ID_BOTH" | MATCH test-snapd-tools + snap saved --id="$SET_ID_NONE" | MATCH test-snapd-tools + + # check the snapshots + snap check-snapshot "$SET_ID_ROOT" + snap check-snapshot "$SET_ID_TEST" + snap check-snapshot "$SET_ID_BOTH" + snap check-snapshot "$SET_ID_NONE" + + # remove the canaries for both users + rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt + rm -f /home/test/snap/test-snapd-tools/{current,common}/canary.txt + + # restore the snapshot for the root user and check the files + snap restore "$SET_ID_ROOT" test-snapd-tools + test -e /root/snap/test-snapd-tools/current/canary.txt + test -e /root/snap/test-snapd-tools/common/canary.txt + test ! -e /home/test/snap/test-snapd-tools/current/canary.txt + test ! -e /home/test/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt + + # restore the snapshot for the test user and check the files + snap restore "$SET_ID_TEST" test-snapd-tools + test -e /home/test/snap/test-snapd-tools/current/canary.txt + test -e /home/test/snap/test-snapd-tools/common/canary.txt + test ! -d /home/test/snap/test-snapd-tools/current/canary + + # remove the canaries for both users + rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt + rm -f /home/test/snap/test-snapd-tools/{current,common}/canary.txt + + # restore the snapshot for both users and check the files + snap restore "$SET_ID_BOTH" test-snapd-tools + test -e /root/snap/test-snapd-tools/current/canary.txt + test -e /root/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary.txt + test -e /home/test/snap/test-snapd-tools/common/canary.txt + test ! -d /home/test/snap/test-snapd-tools/current/canary + + # remove the canaries for both users + rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt + rm -f /home/test/snap/test-snapd-tools/{current,common}/canary.txt + + # restore the snapshot for root user and check the files + snap restore "$SET_ID_BOTH" --users=root + test -e /root/snap/test-snapd-tools/current/canary.txt + test -e /root/snap/test-snapd-tools/common/canary.txt + test ! -e /home/test/snap/test-snapd-tools/current/canary.txt + test ! -e /home/test/snap/test-snapd-tools/common/canary.txt + test ! -d /home/test/snap/test-snapd-tools/current/canary + + # remove the canaries for both users + rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt + rm -f /home/test/snap/test-snapd-tools/{current,common}/canary.txt + + # restore the snapshot for test user and check the files + snap restore "$SET_ID_NONE" --users=test + test ! -e /root/snap/test-snapd-tools/current/canary.txt + test ! -e /root/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary.txt + test -e /home/test/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt + + # remove the canaries for both users + rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt + rm -rf /home/test/snap/test-snapd-tools/{current,common,current/canary}/canary.txt + + # restore the snapshot for both user and check the files + snap restore "$SET_ID_NONE" --users=test,root + test -e /root/snap/test-snapd-tools/current/canary.txt + test -e /root/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary.txt + test -e /home/test/snap/test-snapd-tools/common/canary.txt + test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt + + # check files content + cat /home/test/snap/test-snapd-tools/current/canary/canary.txt | MATCH "content updated" + cat /home/test/snap/test-snapd-tools/current/canary.txt | MATCH "content updated" + + # check removal works + snap forget "$SET_ID_NONE" + snap saved --id="$SET_ID_NONE" | grep "No snapshots found" + snap forget "$SET_ID_ROOT" + snap forget "$SET_ID_TEST" + snap forget "$SET_ID_BOTH" From cb92f76c0b1b39ed9c2914a646f291f41aa3de56 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 9 Nov 2018 12:07:39 +0100 Subject: [PATCH 039/580] run gofmt -w -r "ValidateFreeFromAARE -> ValidateNoAppArmorRegexp" ./ --- interfaces/apparmor/apparmor.go | 2 +- interfaces/apparmor/apparmor_test.go | 4 ++-- interfaces/builtin/daemon_notify.go | 2 +- interfaces/builtin/dotfiles.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interfaces/apparmor/apparmor.go b/interfaces/apparmor/apparmor.go index dfa2d2aa07a..2cf8a852265 100644 --- a/interfaces/apparmor/apparmor.go +++ b/interfaces/apparmor/apparmor.go @@ -37,7 +37,7 @@ import ( // ValidateFreeFromAARE will check that the given string does not // contain AppArmor regular expressions (AARE) or double quotes -func ValidateFreeFromAARE(s string) error { +func ValidateNoAppArmorRegexp(s string) error { const AARE = `?*[]{}^"` + "\x00" if strings.ContainsAny(s, AARE) { diff --git a/interfaces/apparmor/apparmor_test.go b/interfaces/apparmor/apparmor_test.go index 9bf9c288b68..066c9f8b3bd 100644 --- a/interfaces/apparmor/apparmor_test.go +++ b/interfaces/apparmor/apparmor_test.go @@ -224,7 +224,7 @@ func (s *appArmorSuite) TestValidateFreeFromAAREUnhappy(c *C) { var testCases = []string{"a?", "*b", "c[c", "dd]", "e{", "f}", "g^", `h"`, "f\000", "g\x00"} for _, s := range testCases { - c.Check(apparmor.ValidateFreeFromAARE(s), ErrorMatches, ".* contains a reserved apparmor char from .*", Commentf("%q is not raising an error", s)) + c.Check(apparmor.ValidateNoAppArmorRegexp(s), ErrorMatches, ".* contains a reserved apparmor char from .*", Commentf("%q is not raising an error", s)) } } @@ -232,6 +232,6 @@ func (s *appArmorSuite) TestValidateFreeFromAAREhappy(c *C) { var testCases = []string{"foo", "BaR", "b-z", "foo+bar", "b00m!", "be/ep", "a%b", "a&b", "a(b", "a)b", "a=b", "a#b", "a~b", "a'b", "a_b", "a,b", "a;b", "a>b", "a Date: Fri, 9 Nov 2018 12:08:56 +0100 Subject: [PATCH 040/580] dotfiles: deny all connection --- interfaces/builtin/dotfiles.go | 1 + interfaces/policy/basedeclaration_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/dotfiles.go index 251b133643b..983acbd74f9 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/dotfiles.go @@ -37,6 +37,7 @@ const dotfilesBaseDeclarationSlots = ` allow-installation: slot-snap-type: - core + deny-connection: true deny-auto-connection: true ` diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index f4e1e0bc773..5717a2133a5 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -695,6 +695,7 @@ func (s *baseDeclSuite) TestConnection(c *C) { "mir": true, "network-status": true, "online-accounts-service": true, + "dotfiles": true, "storage-framework-service": true, "thumbnailer-service": true, "ubuntu-download-manager": true, From b1f5f4d86dd2e7932e18e9d5476fcab0d98e2823 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 9 Nov 2018 12:18:15 +0100 Subject: [PATCH 041/580] rename to "sensitive-files" --- .../{dotfiles.go => sensitive_files.go} | 30 ++++---- ...tfiles_test.go => sensitive_files_test.go} | 68 +++++++++---------- interfaces/policy/basedeclaration_test.go | 2 +- 3 files changed, 50 insertions(+), 50 deletions(-) rename interfaces/builtin/{dotfiles.go => sensitive_files.go} (77%) rename interfaces/builtin/{dotfiles_test.go => sensitive_files_test.go} (74%) diff --git a/interfaces/builtin/dotfiles.go b/interfaces/builtin/sensitive_files.go similarity index 77% rename from interfaces/builtin/dotfiles.go rename to interfaces/builtin/sensitive_files.go index 983acbd74f9..f1cd391b507 100644 --- a/interfaces/builtin/dotfiles.go +++ b/interfaces/builtin/sensitive_files.go @@ -30,10 +30,10 @@ import ( "github.com/snapcore/snapd/snap" ) -const dotfilesSummary = `allows access to files or directories` +const sensitiveFilesSummary = `allows access to sensitive files or directories` -const dotfilesBaseDeclarationSlots = ` - dotfiles: +const sensitiveFilesBaseDeclarationSlots = ` + sensitive-files: allow-installation: slot-snap-type: - core @@ -41,12 +41,12 @@ const dotfilesBaseDeclarationSlots = ` deny-auto-connection: true ` -const dotfilesConnectedPlugAppArmor = ` +const sensitiveFilesConnectedPlugAppArmor = ` # Description: Can access specific files or directories. # This is restricted because it gives file access to arbitrary locations. ` -type dotfilesInterface struct { +type sensitiveFilesInterface struct { commonInterface } @@ -82,7 +82,7 @@ func validatePaths(attrName string, paths []interface{}) error { return nil } -func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { +func (iface *sensitiveFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { hasValidAttr := false for _, att := range []string{"read", "write"} { if _, ok := plug.Attrs[att]; !ok { @@ -90,15 +90,15 @@ func (iface *dotfilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { } il, ok := plug.Attrs[att].([]interface{}) if !ok { - return fmt.Errorf("cannot add dotfiles plug: %q must be a list of strings", att) + return fmt.Errorf("cannot add sensitive-files plug: %q must be a list of strings", att) } if err := validatePaths(att, il); err != nil { - return fmt.Errorf("cannot add dotfiles plug: %s", err) + return fmt.Errorf("cannot add sensitive-files plug: %s", err) } hasValidAttr = true } if !hasValidAttr { - return fmt.Errorf(`cannot add dotfiles plug: needs valid "read" or "write" attribute`) + return fmt.Errorf(`cannot add sensitive-files plug: needs valid "read" or "write" attribute`) } return nil @@ -130,13 +130,13 @@ func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error return nil } -func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { +func (iface *sensitiveFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { var reads, writes []interface{} _ = plug.Attr("read", &reads) _ = plug.Attr("write", &writes) errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) - buf := bytes.NewBufferString(dotfilesConnectedPlugAppArmor) + buf := bytes.NewBufferString(sensitiveFilesConnectedPlugAppArmor) if err := allowPathAccess(buf, "rk,", reads); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } @@ -149,12 +149,12 @@ func (iface *dotfilesInterface) AppArmorConnectedPlug(spec *apparmor.Specificati } func init() { - registerIface(&dotfilesInterface{commonInterface{ - name: "dotfiles", - summary: dotfilesSummary, + registerIface(&sensitiveFilesInterface{commonInterface{ + name: "sensitive-files", + summary: sensitiveFilesSummary, implicitOnCore: true, implicitOnClassic: true, - baseDeclarationSlots: dotfilesBaseDeclarationSlots, + baseDeclarationSlots: sensitiveFilesBaseDeclarationSlots, reservedForOS: true, }}) } diff --git a/interfaces/builtin/dotfiles_test.go b/interfaces/builtin/sensitive_files_test.go similarity index 74% rename from interfaces/builtin/dotfiles_test.go rename to interfaces/builtin/sensitive_files_test.go index f071b5cbf7d..332d649c4ec 100644 --- a/interfaces/builtin/dotfiles_test.go +++ b/interfaces/builtin/sensitive_files_test.go @@ -32,7 +32,7 @@ import ( "github.com/snapcore/snapd/testutil" ) -type dotfilesInterfaceSuite struct { +type sensitiveFilesInterfaceSuite struct { iface interfaces.Interface slot *interfaces.ConnectedSlot slotInfo *snap.SlotInfo @@ -40,38 +40,38 @@ type dotfilesInterfaceSuite struct { plugInfo *snap.PlugInfo } -var _ = Suite(&dotfilesInterfaceSuite{ - iface: builtin.MustInterface("dotfiles"), +var _ = Suite(&sensitiveFilesInterfaceSuite{ + iface: builtin.MustInterface("sensitive-files"), }) -func (s *dotfilesInterfaceSuite) SetUpTest(c *C) { +func (s *sensitiveFilesInterfaceSuite) SetUpTest(c *C) { const mockPlugSnapInfo = `name: other version: 1.0 plugs: - dotfiles: + sensitive-files: read: [$HOME/.read-dir1, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] write: [$HOME/.write-dir1, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] apps: app: command: foo - plugs: [dotfiles] + plugs: [sensitive-files] ` s.slotInfo = &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, - Name: "dotfiles", - Interface: "dotfiles", + Name: "sensitive-files", + Interface: "sensitive-files", } s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) - s.plugInfo = plugSnap.Plugs["dotfiles"] + s.plugInfo = plugSnap.Plugs["sensitive-files"] s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) } -func (s *dotfilesInterfaceSuite) TestName(c *C) { - c.Assert(s.iface.Name(), Equals, "dotfiles") +func (s *sensitiveFilesInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "sensitive-files") } -func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { +func (s *sensitiveFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) @@ -90,42 +90,42 @@ owner "@{HOME}/.write-file2{,/,/**}" rwkl, `) } -func (s *dotfilesInterfaceSuite) TestSanitizeSlot(c *C) { +func (s *sensitiveFilesInterfaceSuite) TestSanitizeSlot(c *C) { c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) slot := &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "some-snap"}, - Name: "dotfiles", - Interface: "dotfiles", + Name: "sensitive-files", + Interface: "sensitive-files", } c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, - "dotfiles slots are reserved for the core snap") + "sensitive-files slots are reserved for the core snap") } -func (s *dotfilesInterfaceSuite) TestSanitizePlug(c *C) { +func (s *sensitiveFilesInterfaceSuite) TestSanitizePlug(c *C) { c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) } -func (s *dotfilesInterfaceSuite) TestSanitizePlugHappy(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap +func (s *sensitiveFilesInterfaceSuite) TestSanitizePlugHappy(c *C) { + const mockSnapYaml = `name: sensitive-files-plug-snap version: 1.0 plugs: - dotfiles: + sensitive-files: read: ["$HOME/.file1"] write: ["$HOME/.dir1"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["dotfiles"] + plug := info.Plugs["sensitive-files"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) } -func (s *dotfilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { - const mockSnapYaml = `name: dotfiles-plug-snap +func (s *sensitiveFilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + const mockSnapYaml = `name: sensitive-files-plug-snap version: 1.0 plugs: - dotfiles: + sensitive-files: $t ` - errPrefix := `cannot add dotfiles plug: ` + errPrefix := `cannot add sensitive-files plug: ` var testCases = []struct { inp string errStr string @@ -148,38 +148,38 @@ plugs: for _, t := range testCases { yml := strings.Replace(mockSnapYaml, "$t", t.inp, -1) info := snaptest.MockInfo(c, yml, nil) - plug := info.Plugs["dotfiles"] + plug := info.Plugs["sensitive-files"] c.Check(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, errPrefix+t.errStr, Commentf("unexpected error for %q", t.inp)) } } -func (s *dotfilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { +func (s *sensitiveFilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { const mockPlugSnapInfo = `name: other version: 1.0 plugs: - dotfiles: + sensitive-files: read: [ 123 , 345 ] apps: app: command: foo - plugs: [dotfiles] + plugs: [sensitive-files] ` s.slotInfo = &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, - Name: "dotfiles", - Interface: "dotfiles", + Name: "sensitive-files", + Interface: "sensitive-files", } s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) - s.plugInfo = plugSnap.Plugs["dotfiles"] + s.plugInfo = plugSnap.Plugs["sensitive-files"] s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) - c.Assert(err, ErrorMatches, `cannot connect plug dotfiles: 123 \(int64\) is not a string`) + c.Assert(err, ErrorMatches, `cannot connect plug sensitive-files: 123 \(int64\) is not a string`) } -func (s *dotfilesInterfaceSuite) TestInterfaces(c *C) { +func (s *sensitiveFilesInterfaceSuite) TestInterfaces(c *C) { c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) } diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index 5717a2133a5..da7d0693b4d 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -695,7 +695,7 @@ func (s *baseDeclSuite) TestConnection(c *C) { "mir": true, "network-status": true, "online-accounts-service": true, - "dotfiles": true, + "sensitive-files": true, "storage-framework-service": true, "thumbnailer-service": true, "ubuntu-download-manager": true, From 97e3507de3935e845955ad8f7db9a92777f2bb9c Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 9 Nov 2018 08:40:31 -0300 Subject: [PATCH 042/580] Shellcheck improvements --- tests/main/snapshot-users/task.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/main/snapshot-users/task.yaml b/tests/main/snapshot-users/task.yaml index 06c0a3d21bc..da7b205f189 100644 --- a/tests/main/snapshot-users/task.yaml +++ b/tests/main/snapshot-users/task.yaml @@ -6,7 +6,7 @@ prepare: | execute: | # use the snaps, so they create the dirs test-snapd-tools.echo - su -p -c "/snap/bin/test-snapd-tools.echo" test + su -p -c "$SNAP_MOUNT_DIR/bin/test-snapd-tools.echo" test # drop in canaries for both users echo "hello versioned test-snapd-tools" > /root/snap/test-snapd-tools/current/canary.txt @@ -111,8 +111,8 @@ execute: | test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt # check files content - cat /home/test/snap/test-snapd-tools/current/canary/canary.txt | MATCH "content updated" - cat /home/test/snap/test-snapd-tools/current/canary.txt | MATCH "content updated" + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary.txt # check removal works snap forget "$SET_ID_NONE" From 845f1d7c1788df5df0487386b5dee0d5efb9d835 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 9 Nov 2018 18:37:30 +0100 Subject: [PATCH 043/580] interfaces: split dotfiles into {system,personal}-files Note that this does not enforce checking of system paths or personal paths just yet. As these interfaces need a manual review and a snap declaration anyway we may not need to do this in code. --- interfaces/builtin/personal_files.go | 51 +++++++++++ interfaces/builtin/personal_files_test.go | 88 +++++++++++++++++++ .../{sensitive_files.go => system_files.go} | 68 +++++++------- ...ive_files_test.go => system_files_test.go} | 70 +++++++-------- interfaces/policy/basedeclaration_test.go | 3 +- .../meta/snap.yaml | 22 ++--- 6 files changed, 222 insertions(+), 80 deletions(-) create mode 100644 interfaces/builtin/personal_files.go create mode 100644 interfaces/builtin/personal_files_test.go rename interfaces/builtin/{sensitive_files.go => system_files.go} (76%) rename interfaces/builtin/{sensitive_files_test.go => system_files_test.go} (73%) diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go new file mode 100644 index 00000000000..d231d0210ab --- /dev/null +++ b/interfaces/builtin/personal_files.go @@ -0,0 +1,51 @@ +// -*- 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 . + * + */ + +package builtin + +const personalFilesSummary = `allows access to personal files or directories` + +const personalFilesBaseDeclarationSlots = ` + personal-files: + allow-installation: + slot-snap-type: + - core + deny-connection: true + deny-auto-connection: true +` + +const personalFilesConnectedPlugAppArmor = ` +# Description: Can access specific personal files or directories. +# This is restricted because it gives file access to arbitrary locations. +` + +type personalFilesInterface struct { + systemFilesInterface +} + +func init() { + registerIface(&personalFilesInterface{systemFilesInterface{commonInterface{ + name: "personal-files", + summary: personalFilesSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: personalFilesBaseDeclarationSlots, + reservedForOS: true, + }}}) +} diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go new file mode 100644 index 00000000000..b4bde51f881 --- /dev/null +++ b/interfaces/builtin/personal_files_test.go @@ -0,0 +1,88 @@ +// -*- 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 . + * + */ + +package builtin_test + +import ( + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type personalFilesInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.ConnectedSlot + slotInfo *snap.SlotInfo + plug *interfaces.ConnectedPlug + plugInfo *snap.PlugInfo +} + +var _ = Suite(&personalFilesInterfaceSuite{ + iface: builtin.MustInterface("personal-files"), +}) + +func (s *personalFilesInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "personal-files") +} + +func (s *personalFilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + const mockSnapYaml = `name: personal-files-plug-snap +version: 1.0 +plugs: + personal-files: + $t +` + errPrefix := `cannot add personal-files plug: ` + var testCases = []struct { + inp string + errStr string + }{ + {`read: ""`, `"read" must be a list of strings`}, + {`read: [ 123 ]`, `"read" must be a list of strings`}, + {`read: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, + {`read: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, + {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, + {`write: ""`, `"write" must be a list of strings`}, + {`write: bar`, `"write" must be a list of strings`}, + {`read: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, + {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, + {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, + {`read: [ "/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "/home/\$HOME/foo"`}, + {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, + {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, + } + + for _, t := range testCases { + yml := strings.Replace(mockSnapYaml, "$t", t.inp, -1) + info := snaptest.MockInfo(c, yml, nil) + plug := info.Plugs["personal-files"] + + c.Check(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, errPrefix+t.errStr, Commentf("unexpected error for %q", t.inp)) + } +} + +func (s *personalFilesInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff --git a/interfaces/builtin/sensitive_files.go b/interfaces/builtin/system_files.go similarity index 76% rename from interfaces/builtin/sensitive_files.go rename to interfaces/builtin/system_files.go index f1cd391b507..874dfe4b362 100644 --- a/interfaces/builtin/sensitive_files.go +++ b/interfaces/builtin/system_files.go @@ -30,10 +30,10 @@ import ( "github.com/snapcore/snapd/snap" ) -const sensitiveFilesSummary = `allows access to sensitive files or directories` +const systemFilesSummary = `allows access to system files or directories` -const sensitiveFilesBaseDeclarationSlots = ` - sensitive-files: +const systemFilesBaseDeclarationSlots = ` + system-files: allow-installation: slot-snap-type: - core @@ -41,12 +41,12 @@ const sensitiveFilesBaseDeclarationSlots = ` deny-auto-connection: true ` -const sensitiveFilesConnectedPlugAppArmor = ` -# Description: Can access specific files or directories. +const systemFilesConnectedPlugAppArmor = ` +# Description: Can access specific system files or directories. # This is restricted because it gives file access to arbitrary locations. ` -type sensitiveFilesInterface struct { +type systemFilesInterface struct { commonInterface } @@ -82,28 +82,6 @@ func validatePaths(attrName string, paths []interface{}) error { return nil } -func (iface *sensitiveFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { - hasValidAttr := false - for _, att := range []string{"read", "write"} { - if _, ok := plug.Attrs[att]; !ok { - continue - } - il, ok := plug.Attrs[att].([]interface{}) - if !ok { - return fmt.Errorf("cannot add sensitive-files plug: %q must be a list of strings", att) - } - if err := validatePaths(att, il); err != nil { - return fmt.Errorf("cannot add sensitive-files plug: %s", err) - } - hasValidAttr = true - } - if !hasValidAttr { - return fmt.Errorf(`cannot add sensitive-files plug: needs valid "read" or "write" attribute`) - } - - return nil -} - func formatPath(ip interface{}) (string, error) { p, ok := ip.(string) if !ok { @@ -130,13 +108,35 @@ func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error return nil } -func (iface *sensitiveFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { +func (iface *systemFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + hasValidAttr := false + for _, att := range []string{"read", "write"} { + if _, ok := plug.Attrs[att]; !ok { + continue + } + il, ok := plug.Attrs[att].([]interface{}) + if !ok { + return fmt.Errorf("cannot add %s plug: %q must be a list of strings", iface.name, att) + } + if err := validatePaths(att, il); err != nil { + return fmt.Errorf("cannot add %s plug: %s", iface.name, err) + } + hasValidAttr = true + } + if !hasValidAttr { + return fmt.Errorf(`cannot add %s plug: needs valid "read" or "write" attribute`, iface.name) + } + + return nil +} + +func (iface *systemFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { var reads, writes []interface{} _ = plug.Attr("read", &reads) _ = plug.Attr("write", &writes) errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) - buf := bytes.NewBufferString(sensitiveFilesConnectedPlugAppArmor) + buf := bytes.NewBufferString(systemFilesConnectedPlugAppArmor) if err := allowPathAccess(buf, "rk,", reads); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } @@ -149,12 +149,12 @@ func (iface *sensitiveFilesInterface) AppArmorConnectedPlug(spec *apparmor.Speci } func init() { - registerIface(&sensitiveFilesInterface{commonInterface{ - name: "sensitive-files", - summary: sensitiveFilesSummary, + registerIface(&systemFilesInterface{commonInterface{ + name: "system-files", + summary: systemFilesSummary, implicitOnCore: true, implicitOnClassic: true, - baseDeclarationSlots: sensitiveFilesBaseDeclarationSlots, + baseDeclarationSlots: systemFilesBaseDeclarationSlots, reservedForOS: true, }}) } diff --git a/interfaces/builtin/sensitive_files_test.go b/interfaces/builtin/system_files_test.go similarity index 73% rename from interfaces/builtin/sensitive_files_test.go rename to interfaces/builtin/system_files_test.go index 332d649c4ec..ba6b00204e0 100644 --- a/interfaces/builtin/sensitive_files_test.go +++ b/interfaces/builtin/system_files_test.go @@ -32,7 +32,7 @@ import ( "github.com/snapcore/snapd/testutil" ) -type sensitiveFilesInterfaceSuite struct { +type systemFilesInterfaceSuite struct { iface interfaces.Interface slot *interfaces.ConnectedSlot slotInfo *snap.SlotInfo @@ -40,44 +40,44 @@ type sensitiveFilesInterfaceSuite struct { plugInfo *snap.PlugInfo } -var _ = Suite(&sensitiveFilesInterfaceSuite{ - iface: builtin.MustInterface("sensitive-files"), +var _ = Suite(&systemFilesInterfaceSuite{ + iface: builtin.MustInterface("system-files"), }) -func (s *sensitiveFilesInterfaceSuite) SetUpTest(c *C) { +func (s *systemFilesInterfaceSuite) SetUpTest(c *C) { const mockPlugSnapInfo = `name: other version: 1.0 plugs: - sensitive-files: + system-files: read: [$HOME/.read-dir1, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] write: [$HOME/.write-dir1, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] apps: app: command: foo - plugs: [sensitive-files] + plugs: [system-files] ` s.slotInfo = &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, - Name: "sensitive-files", - Interface: "sensitive-files", + Name: "system-files", + Interface: "system-files", } s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) - s.plugInfo = plugSnap.Plugs["sensitive-files"] + s.plugInfo = plugSnap.Plugs["system-files"] s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) } -func (s *sensitiveFilesInterfaceSuite) TestName(c *C) { - c.Assert(s.iface.Name(), Equals, "sensitive-files") +func (s *systemFilesInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "system-files") } -func (s *sensitiveFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { +func (s *systemFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` -# Description: Can access specific files or directories. +# Description: Can access specific system files or directories. # This is restricted because it gives file access to arbitrary locations. owner "@{HOME}/.read-dir1{,/,/**}" rk, "/etc/read-dir2{,/,/**}" rk, @@ -90,42 +90,42 @@ owner "@{HOME}/.write-file2{,/,/**}" rwkl, `) } -func (s *sensitiveFilesInterfaceSuite) TestSanitizeSlot(c *C) { +func (s *systemFilesInterfaceSuite) TestSanitizeSlot(c *C) { c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) slot := &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "some-snap"}, - Name: "sensitive-files", - Interface: "sensitive-files", + Name: "system-files", + Interface: "system-files", } c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, - "sensitive-files slots are reserved for the core snap") + "system-files slots are reserved for the core snap") } -func (s *sensitiveFilesInterfaceSuite) TestSanitizePlug(c *C) { +func (s *systemFilesInterfaceSuite) TestSanitizePlug(c *C) { c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) } -func (s *sensitiveFilesInterfaceSuite) TestSanitizePlugHappy(c *C) { - const mockSnapYaml = `name: sensitive-files-plug-snap +func (s *systemFilesInterfaceSuite) TestSanitizePlugHappy(c *C) { + const mockSnapYaml = `name: system-files-plug-snap version: 1.0 plugs: - sensitive-files: + system-files: read: ["$HOME/.file1"] write: ["$HOME/.dir1"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) - plug := info.Plugs["sensitive-files"] + plug := info.Plugs["system-files"] c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) } -func (s *sensitiveFilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { - const mockSnapYaml = `name: sensitive-files-plug-snap +func (s *systemFilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + const mockSnapYaml = `name: system-files-plug-snap version: 1.0 plugs: - sensitive-files: + system-files: $t ` - errPrefix := `cannot add sensitive-files plug: ` + errPrefix := `cannot add system-files plug: ` var testCases = []struct { inp string errStr string @@ -148,38 +148,38 @@ plugs: for _, t := range testCases { yml := strings.Replace(mockSnapYaml, "$t", t.inp, -1) info := snaptest.MockInfo(c, yml, nil) - plug := info.Plugs["sensitive-files"] + plug := info.Plugs["system-files"] c.Check(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, errPrefix+t.errStr, Commentf("unexpected error for %q", t.inp)) } } -func (s *sensitiveFilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { +func (s *systemFilesInterfaceSuite) TestConnectedPlugAppArmorInternalError(c *C) { const mockPlugSnapInfo = `name: other version: 1.0 plugs: - sensitive-files: + system-files: read: [ 123 , 345 ] apps: app: command: foo - plugs: [sensitive-files] + plugs: [system-files] ` s.slotInfo = &snap.SlotInfo{ Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, - Name: "sensitive-files", - Interface: "sensitive-files", + Name: "system-files", + Interface: "system-files", } s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) - s.plugInfo = plugSnap.Plugs["sensitive-files"] + s.plugInfo = plugSnap.Plugs["system-files"] s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) apparmorSpec := &apparmor.Specification{} err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) - c.Assert(err, ErrorMatches, `cannot connect plug sensitive-files: 123 \(int64\) is not a string`) + c.Assert(err, ErrorMatches, `cannot connect plug system-files: 123 \(int64\) is not a string`) } -func (s *sensitiveFilesInterfaceSuite) TestInterfaces(c *C) { +func (s *systemFilesInterfaceSuite) TestInterfaces(c *C) { c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) } diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index da7d0693b4d..e246c2351a2 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -695,8 +695,9 @@ func (s *baseDeclSuite) TestConnection(c *C) { "mir": true, "network-status": true, "online-accounts-service": true, - "sensitive-files": true, + "personal-files": true, "storage-framework-service": true, + "system-files": true, "thumbnailer-service": true, "ubuntu-download-manager": true, "unity8-calendar": true, 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 ab728a9f37a..08d31444e51 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 @@ -83,12 +83,12 @@ apps: docker-support: command: bin/run plugs: [ docker-support ] - dotfiles-files: + system-files: command: bin/run - plugs: [ dotfiles-files ] - dotfiles-dirs: + plugs: [ system-files ] + personal-files: command: bin/run - plugs: [ dotfiles-dirs ] + plugs: [ personal-files ] dvb: command: bin/run plugs: [ dvb ] @@ -367,11 +367,13 @@ plugs: interface: dbus bus: system name: test.system - dotfiles-files: - interface: dotfiles - files: [.file1] - dotfiles-dirs: - interface: dotfiles - dirs: [.dir1] + system-files: + interface: system-files + files: [file1] + dirs: [dir1] + personal-dirs: + interface: personal-files + files: [file1] + dirs: [dir1] dummy: interface: dummy From b8940ba62181836f3861b8f96a004443f6023487 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 12 Nov 2018 20:58:11 -0300 Subject: [PATCH 044/580] new kvm parameters --- tests/lib/nested.sh | 16 ++++++++++++++-- tests/nested/classic/hot-plug/task.yaml | 10 +++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index b248f9f2966..46b3df2625c 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -49,6 +49,12 @@ get_qemu_for_nested_vm(){ esac } +get_kvm_for_nested_vm(){ + if [ -c /dev/kvm ]; then + echo "-machine accel=kvm" + fi +} + get_image_url_for_nested_vm(){ case "$NESTED_SYSTEM" in xenial|trusty) @@ -93,12 +99,15 @@ create_nested_core_vm(){ start_nested_core_vm(){ local QEMU QEMU=$(get_qemu_for_nested_vm) + local KVM + KVM=$(get_kvm_for_nested_vm) systemd_create_and_start_unit nested-vm "${QEMU} -m 2048 -nographic \ -net nic,model=virtio -net user,hostfwd=tcp::$SSH_PORT-:22 \ -drive file=$WORK_DIR/ubuntu-core.img,if=virtio,cache=none,format=raw \ -drive file=${PWD}/assertions.disk,if=virtio,cache=none,format=raw \ -monitor tcp:127.0.0.1:$MON_PORT,server,nowait -usb \ - -machine accel=kvm" + -snapshot + ${KVM}" if ! wait_for_ssh; then systemctl restart nested-vm fi @@ -139,13 +148,16 @@ start_nested_classic_vm(){ local IMAGE=$1 local QEMU QEMU=$(get_qemu_for_nested_vm) + local KVM + KVM=$(get_kvm_for_nested_vm) systemd_create_and_start_unit nested-vm "${QEMU} -m 2048 -nographic \ -net nic,model=virtio -net user,hostfwd=tcp::$SSH_PORT-:22 \ -drive file=$IMAGE,if=virtio \ -drive file=$WORK_DIR/seed.img,if=virtio \ -monitor tcp:127.0.0.1:$MON_PORT,server,nowait -usb \ - -machine accel=kvm" + -snapshot + ${KVM}" wait_for_ssh } diff --git a/tests/nested/classic/hot-plug/task.yaml b/tests/nested/classic/hot-plug/task.yaml index 6e1396c0e2b..0abc32a0fc0 100644 --- a/tests/nested/classic/hot-plug/task.yaml +++ b/tests/nested/classic/hot-plug/task.yaml @@ -3,13 +3,13 @@ summary: create ubuntu classic image, install snapd and test hot plug feature prepare: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - create_nested_classic_vm + #create_nested_classic_vm - copy_remote "${GOHOME}"/snapd_*.deb - execute_remote "sudo apt update" - execute_remote "sudo apt install -y ./snapd_*.deb" + #copy_remote "${GOHOME}"/snapd_*.deb + #execute_remote "sudo apt update" + #execute_remote "sudo apt install -y ./snapd_*.deb" #shellcheck disable=SC2016 - execute_remote 'sudo apt install -y linux-image-extra-$(uname -r) || sudo apt install -y linux-modules-extra-$(uname -r)' + #execute_remote 'sudo apt install -y linux-image-extra-$(uname -r) || sudo apt install -y linux-modules-extra-$(uname -r)' restore: | #shellcheck source=tests/lib/nested.sh From 65442ebdf6d44667bedf203ab1ac9f13c3a2d0e2 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 12 Nov 2018 23:03:44 -0300 Subject: [PATCH 045/580] Add missing lib --- tests/main/snapshot-users/task.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/main/snapshot-users/task.yaml b/tests/main/snapshot-users/task.yaml index da7b205f189..a8eefaf762e 100644 --- a/tests/main/snapshot-users/task.yaml +++ b/tests/main/snapshot-users/task.yaml @@ -4,6 +4,9 @@ prepare: | snap install test-snapd-tools execute: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB/dirs.sh" + # use the snaps, so they create the dirs test-snapd-tools.echo su -p -c "$SNAP_MOUNT_DIR/bin/test-snapd-tools.echo" test From fc811a2d9844138139e1908b64e233bc7eb82958 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 12 Nov 2018 23:49:47 -0300 Subject: [PATCH 046/580] Skip opensuse from interfaces-openvswitch-support test Next update for opensuse is failing when interfaces-openvswitch-support test is executed. The cause is the same than for arch system, where the interface is allowing access to /run/uuidd/request and in these systems the request is done in /run/run/uuidd/request, making fail the snaps which try to request a random id. test error: https://paste.ubuntu.com/p/bv9xZj36XR/ debug info: https://paste.ubuntu.com/p/nMF4BR8ZF7/ --- tests/main/interfaces-openvswitch-support/task.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/main/interfaces-openvswitch-support/task.yaml b/tests/main/interfaces-openvswitch-support/task.yaml index 9d174c07832..f9371298109 100644 --- a/tests/main/interfaces-openvswitch-support/task.yaml +++ b/tests/main/interfaces-openvswitch-support/task.yaml @@ -5,8 +5,8 @@ details: | # ubuntu-core, ubuntu-14, fedora, amazon are skipped as /run/uuidd/request file does not # exist. On those systems different files are being used instead. -# arch: uses /run/run/uuidd/request, filed a bug report https://bugs.archlinux.org/task/58122 -systems: [-ubuntu-14.04-*,-ubuntu-core-*,-fedora-*, -arch-*, -amazon-*] +# arch, opensuse: uses /run/run/uuidd/request, filed a bug report https://bugs.archlinux.org/task/58122 +systems: [-ubuntu-14.04-*,-ubuntu-core-*,-fedora-*, -arch-*, -amazon-*, -opensuse-*] prepare: | snap install test-snapd-openvswitch-support From 9709eca6b2e260afbb1e2ae0b313b0c86600addc Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 12 Nov 2018 23:53:00 -0300 Subject: [PATCH 047/580] Revert "Skip opensuse from interfaces-openvswitch-support test" This reverts commit fc811a2d9844138139e1908b64e233bc7eb82958. --- tests/main/interfaces-openvswitch-support/task.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/main/interfaces-openvswitch-support/task.yaml b/tests/main/interfaces-openvswitch-support/task.yaml index f9371298109..9d174c07832 100644 --- a/tests/main/interfaces-openvswitch-support/task.yaml +++ b/tests/main/interfaces-openvswitch-support/task.yaml @@ -5,8 +5,8 @@ details: | # ubuntu-core, ubuntu-14, fedora, amazon are skipped as /run/uuidd/request file does not # exist. On those systems different files are being used instead. -# arch, opensuse: uses /run/run/uuidd/request, filed a bug report https://bugs.archlinux.org/task/58122 -systems: [-ubuntu-14.04-*,-ubuntu-core-*,-fedora-*, -arch-*, -amazon-*, -opensuse-*] +# arch: uses /run/run/uuidd/request, filed a bug report https://bugs.archlinux.org/task/58122 +systems: [-ubuntu-14.04-*,-ubuntu-core-*,-fedora-*, -arch-*, -amazon-*] prepare: | snap install test-snapd-openvswitch-support From c1e47918d9827caf3a01bd92c68d3197103feb06 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 12 Nov 2018 23:58:33 -0300 Subject: [PATCH 048/580] Revert "new kvm parameters" This reverts commit b8940ba62181836f3861b8f96a004443f6023487. --- tests/lib/nested.sh | 16 ++-------------- tests/nested/classic/hot-plug/task.yaml | 10 +++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/tests/lib/nested.sh b/tests/lib/nested.sh index 46b3df2625c..b248f9f2966 100644 --- a/tests/lib/nested.sh +++ b/tests/lib/nested.sh @@ -49,12 +49,6 @@ get_qemu_for_nested_vm(){ esac } -get_kvm_for_nested_vm(){ - if [ -c /dev/kvm ]; then - echo "-machine accel=kvm" - fi -} - get_image_url_for_nested_vm(){ case "$NESTED_SYSTEM" in xenial|trusty) @@ -99,15 +93,12 @@ create_nested_core_vm(){ start_nested_core_vm(){ local QEMU QEMU=$(get_qemu_for_nested_vm) - local KVM - KVM=$(get_kvm_for_nested_vm) systemd_create_and_start_unit nested-vm "${QEMU} -m 2048 -nographic \ -net nic,model=virtio -net user,hostfwd=tcp::$SSH_PORT-:22 \ -drive file=$WORK_DIR/ubuntu-core.img,if=virtio,cache=none,format=raw \ -drive file=${PWD}/assertions.disk,if=virtio,cache=none,format=raw \ -monitor tcp:127.0.0.1:$MON_PORT,server,nowait -usb \ - -snapshot - ${KVM}" + -machine accel=kvm" if ! wait_for_ssh; then systemctl restart nested-vm fi @@ -148,16 +139,13 @@ start_nested_classic_vm(){ local IMAGE=$1 local QEMU QEMU=$(get_qemu_for_nested_vm) - local KVM - KVM=$(get_kvm_for_nested_vm) systemd_create_and_start_unit nested-vm "${QEMU} -m 2048 -nographic \ -net nic,model=virtio -net user,hostfwd=tcp::$SSH_PORT-:22 \ -drive file=$IMAGE,if=virtio \ -drive file=$WORK_DIR/seed.img,if=virtio \ -monitor tcp:127.0.0.1:$MON_PORT,server,nowait -usb \ - -snapshot - ${KVM}" + -machine accel=kvm" wait_for_ssh } diff --git a/tests/nested/classic/hot-plug/task.yaml b/tests/nested/classic/hot-plug/task.yaml index 0abc32a0fc0..6e1396c0e2b 100644 --- a/tests/nested/classic/hot-plug/task.yaml +++ b/tests/nested/classic/hot-plug/task.yaml @@ -3,13 +3,13 @@ summary: create ubuntu classic image, install snapd and test hot plug feature prepare: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - #create_nested_classic_vm + create_nested_classic_vm - #copy_remote "${GOHOME}"/snapd_*.deb - #execute_remote "sudo apt update" - #execute_remote "sudo apt install -y ./snapd_*.deb" + copy_remote "${GOHOME}"/snapd_*.deb + execute_remote "sudo apt update" + execute_remote "sudo apt install -y ./snapd_*.deb" #shellcheck disable=SC2016 - #execute_remote 'sudo apt install -y linux-image-extra-$(uname -r) || sudo apt install -y linux-modules-extra-$(uname -r)' + execute_remote 'sudo apt install -y linux-image-extra-$(uname -r) || sudo apt install -y linux-modules-extra-$(uname -r)' restore: | #shellcheck source=tests/lib/nested.sh From 0ad63634639d3c8e327bb84daba3a40dd191e5fb Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 13 Nov 2018 00:01:03 -0300 Subject: [PATCH 049/580] Fix shellcheck --- .../nested/classic/snapshots-with-core-refresh-revert/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml index f12191c0e5a..a36dd49a749 100644 --- a/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml +++ b/tests/nested/classic/snapshots-with-core-refresh-revert/task.yaml @@ -6,7 +6,7 @@ prepare: | create_nested_classic_vm # configure hosts file - # shellcheck disable=SC2086 + # shellcheck disable=SC2016 execute_remote 'echo "127.0.1.1 $HOSTNAME" | sudo tee /etc/hosts' # install snapd and snaps on nested vm From de93c18690bdcce3896878a675d6111681fd4b11 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 13 Nov 2018 12:48:01 +0100 Subject: [PATCH 050/580] interfaces: add validation to {personal,system}-files and extract common code --- interfaces/builtin/common_files.go | 141 ++++++++++++++++++++++ interfaces/builtin/personal_files.go | 39 ++++-- interfaces/builtin/personal_files_test.go | 79 +++++++++++- interfaces/builtin/system_files.go | 127 +++---------------- interfaces/builtin/system_files_test.go | 18 ++- 5 files changed, 271 insertions(+), 133 deletions(-) create mode 100644 interfaces/builtin/common_files.go diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go new file mode 100644 index 00000000000..f783ac097de --- /dev/null +++ b/interfaces/builtin/common_files.go @@ -0,0 +1,141 @@ +// -*- 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 . + * + */ + +package builtin + +import ( + "bytes" + "fmt" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +type commonFilesInterface struct { + commonInterface + + apparmorHeader string + extraPathValidate func(string) error +} + +func formatPath(ip interface{}) (string, error) { + p, ok := ip.(string) + if !ok { + return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) + } + prefix := "" + if strings.Count(p, "$HOME") > 0 { + p = strings.Replace(p, "$HOME", "@{HOME}", 1) + prefix = "owner " + } + p += "{,/,/**}" + + return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil +} + +func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error { + for _, rawPath := range paths { + p, err := formatPath(rawPath) + if err != nil { + return err + } + fmt.Fprintf(buf, "%s %s\n", p, perm) + } + return nil +} + +func (iface *commonFilesInterface) validatePaths(attrName string, paths []interface{}) error { + for _, npp := range paths { + np, ok := npp.(string) + if !ok { + return fmt.Errorf("%q must be a list of strings", attrName) + } + if err := iface.validateSinglePath(np); err != nil { + return err + } + } + return nil +} + +func (iface *commonFilesInterface) validateSinglePath(np string) error { + if strings.HasSuffix(np, "/") { + return fmt.Errorf(`%q cannot end with "/"`, np) + } + if strings.Contains(np, "@{") { + return fmt.Errorf(`%q should not use "@{"`, np) + } + p := filepath.Clean(np) + if p != np { + return fmt.Errorf("%q must be clean", np) + } + if strings.Contains(p, "~") { + return fmt.Errorf(`%q contains invalid "~"`, p) + } + if err := apparmor.ValidateNoAppArmorRegexp(p); err != nil { + return err + } + + if err := iface.extraPathValidate(np); err != nil { + return err + } + return nil +} + +func (iface *commonFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + hasValidAttr := false + for _, att := range []string{"read", "write"} { + if _, ok := plug.Attrs[att]; !ok { + continue + } + il, ok := plug.Attrs[att].([]interface{}) + if !ok { + return fmt.Errorf("cannot add %s plug: %q must be a list of strings", iface.name, att) + } + if err := iface.validatePaths(att, il); err != nil { + return fmt.Errorf("cannot add %s plug: %s", iface.name, err) + } + hasValidAttr = true + } + if !hasValidAttr { + return fmt.Errorf(`cannot add %s plug: needs valid "read" or "write" attribute`, iface.name) + } + + return nil +} + +func (iface *commonFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + var reads, writes []interface{} + _ = plug.Attr("read", &reads) + _ = plug.Attr("write", &writes) + + errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) + buf := bytes.NewBufferString(iface.apparmorHeader) + if err := allowPathAccess(buf, "rk,", reads); err != nil { + return fmt.Errorf("%s%v", errPrefix, err) + } + if err := allowPathAccess(buf, "rwkl,", writes); err != nil { + return fmt.Errorf("%s%v", errPrefix, err) + } + spec.AddSnippet(buf.String()) + + return nil +} diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index d231d0210ab..a5d234c49af 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -19,6 +19,11 @@ package builtin +import ( + "fmt" + "strings" +) + const personalFilesSummary = `allows access to personal files or directories` const personalFilesBaseDeclarationSlots = ` @@ -36,16 +41,32 @@ const personalFilesConnectedPlugAppArmor = ` ` type personalFilesInterface struct { - systemFilesInterface + commonFilesInterface +} + +func validateSinglePathHome(np string) error { + if !strings.HasPrefix(np, "$HOME/") { + return fmt.Errorf(`%q must start with "$HOME"`, np) + } + if strings.Count(np, "$HOME") > 1 { + return fmt.Errorf(`$HOME must only be used at the start of the path of %q`, np) + } + return nil } func init() { - registerIface(&personalFilesInterface{systemFilesInterface{commonInterface{ - name: "personal-files", - summary: personalFilesSummary, - implicitOnCore: true, - implicitOnClassic: true, - baseDeclarationSlots: personalFilesBaseDeclarationSlots, - reservedForOS: true, - }}}) + registerIface(&personalFilesInterface{ + commonFilesInterface{ + commonInterface: commonInterface{ + name: "personal-files", + summary: personalFilesSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: personalFilesBaseDeclarationSlots, + reservedForOS: true, + }, + apparmorHeader: personalFilesConnectedPlugAppArmor, + extraPathValidate: validateSinglePathHome, + }, + }) } diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go index b4bde51f881..cc12d2905f6 100644 --- a/interfaces/builtin/personal_files_test.go +++ b/interfaces/builtin/personal_files_test.go @@ -25,6 +25,7 @@ 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/snap" "github.com/snapcore/snapd/snap/snaptest" @@ -43,10 +44,76 @@ var _ = Suite(&personalFilesInterfaceSuite{ iface: builtin.MustInterface("personal-files"), }) +func (s *personalFilesInterfaceSuite) SetUpTest(c *C) { + const mockPlugSnapInfo = `name: other +version: 1.0 +plugs: + personal-files: + read: [$HOME/.read-dir, $HOME/.read-file] + write: [$HOME/.write-dir, $HOME/.write-file] +apps: + app: + command: foo + plugs: [personal-files] +` + s.slotInfo = &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", Type: snap.TypeOS}, + Name: "personal-files", + Interface: "personal-files", + } + s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) + plugSnap := snaptest.MockInfo(c, mockPlugSnapInfo, nil) + s.plugInfo = plugSnap.Plugs["personal-files"] + s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) +} + func (s *personalFilesInterfaceSuite) TestName(c *C) { c.Assert(s.iface.Name(), Equals, "personal-files") } +func (s *personalFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { + apparmorSpec := &apparmor.Specification{} + err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) + c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` +# Description: Can access specific personal files or directories. +# This is restricted because it gives file access to arbitrary locations. +owner "@{HOME}/.read-dir{,/,/**}" rk, +owner "@{HOME}/.read-file{,/,/**}" rk, +owner "@{HOME}/.write-dir{,/,/**}" rwkl, +owner "@{HOME}/.write-file{,/,/**}" rwkl, +`) +} + +func (s *personalFilesInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) + slot := &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "some-snap"}, + Name: "personal-files", + Interface: "personal-files", + } + c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, + "personal-files slots are reserved for the core snap") +} + +func (s *personalFilesInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *personalFilesInterfaceSuite) TestSanitizePlugHappy(c *C) { + const mockSnapYaml = `name: personal-files-plug-snap +version: 1.0 +plugs: + personal-files: + read: ["$HOME/file1", "$HOME/.hidden1"] + write: ["$HOME/dir1", "$HOME/.hidden2"] +` + info := snaptest.MockInfo(c, mockSnapYaml, nil) + plug := info.Plugs["personal-files"] + c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) +} + func (s *personalFilesInterfaceSuite) TestSanitizePlugUnhappy(c *C) { const mockSnapYaml = `name: personal-files-plug-snap version: 1.0 @@ -61,15 +128,15 @@ plugs: }{ {`read: ""`, `"read" must be a list of strings`}, {`read: [ 123 ]`, `"read" must be a list of strings`}, - {`read: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, - {`read: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, + {`read: [ "$HOME/foo/./bar" ]`, `"\$HOME/foo/./bar" must be clean`}, + {`read: [ "../foo" ]`, `"../foo" must start with "\$HOME"`}, {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, {`write: ""`, `"write" must be a list of strings`}, {`write: bar`, `"write" must be a list of strings`}, - {`read: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, - {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, - {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, - {`read: [ "/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "/home/\$HOME/foo"`}, + {`read: [ "~/foo" ]`, `"~/foo" contains invalid "~"`}, + {`read: [ "$HOME/foo/~/foo" ]`, `"\$HOME/foo/~/foo" contains invalid "~"`}, + {`read: [ "$HOME/foo/../foo" ]`, `"\$HOME/foo/../foo" must be clean`}, + {`read: [ "$HOME/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "\$HOME/home/\$HOME/foo"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, } diff --git a/interfaces/builtin/system_files.go b/interfaces/builtin/system_files.go index 874dfe4b362..050fd789e67 100644 --- a/interfaces/builtin/system_files.go +++ b/interfaces/builtin/system_files.go @@ -20,14 +20,8 @@ package builtin import ( - "bytes" "fmt" - "path/filepath" "strings" - - "github.com/snapcore/snapd/interfaces" - "github.com/snapcore/snapd/interfaces/apparmor" - "github.com/snapcore/snapd/snap" ) const systemFilesSummary = `allows access to system files or directories` @@ -47,114 +41,33 @@ const systemFilesConnectedPlugAppArmor = ` ` type systemFilesInterface struct { - commonInterface -} - -func validatePaths(attrName string, paths []interface{}) error { - for _, npp := range paths { - np, ok := npp.(string) - if !ok { - return fmt.Errorf("%q must be a list of strings", attrName) - } - if strings.HasSuffix(np, "/") { - return fmt.Errorf(`%q cannot end with "/"`, np) - } - if !strings.HasPrefix(np, "/") && !strings.HasPrefix(np, "$HOME/") { - return fmt.Errorf(`%q must start with "/" or "$HOME"`, np) - } - if !strings.HasPrefix(np, "$HOME/") && strings.Contains(np, "$HOME") { - return fmt.Errorf(`$HOME must only be used at the start of the path of %q`, np) - } - if strings.Contains(np, "@{") { - return fmt.Errorf(`%q should not use "@{"`, np) - } - p := filepath.Clean(np) - if p != np { - return fmt.Errorf("%q must be clean", np) - } - if strings.Contains(p, "~") { - return fmt.Errorf(`%q contains invalid "~"`, p) - } - if err := apparmor.ValidateNoAppArmorRegexp(p); err != nil { - return err - } - } - return nil + commonFilesInterface } -func formatPath(ip interface{}) (string, error) { - p, ok := ip.(string) - if !ok { - return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) - } - prefix := "" - if strings.Count(p, "$HOME") > 0 { - p = strings.Replace(p, "$HOME", "@{HOME}", 1) - prefix = "owner " - } - p += "{,/,/**}" - - return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil -} - -func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error { - for _, rawPath := range paths { - p, err := formatPath(rawPath) - if err != nil { - return err - } - fmt.Fprintf(buf, "%s %s\n", p, perm) - } - return nil -} - -func (iface *systemFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error { - hasValidAttr := false - for _, att := range []string{"read", "write"} { - if _, ok := plug.Attrs[att]; !ok { - continue - } - il, ok := plug.Attrs[att].([]interface{}) - if !ok { - return fmt.Errorf("cannot add %s plug: %q must be a list of strings", iface.name, att) - } - if err := validatePaths(att, il); err != nil { - return fmt.Errorf("cannot add %s plug: %s", iface.name, err) - } - hasValidAttr = true - } - if !hasValidAttr { - return fmt.Errorf(`cannot add %s plug: needs valid "read" or "write" attribute`, iface.name) - } - - return nil -} - -func (iface *systemFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - var reads, writes []interface{} - _ = plug.Attr("read", &reads) - _ = plug.Attr("write", &writes) - - errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) - buf := bytes.NewBufferString(systemFilesConnectedPlugAppArmor) - if err := allowPathAccess(buf, "rk,", reads); err != nil { - return fmt.Errorf("%s%v", errPrefix, err) +func validateSinglePathSystem(np string) error { + if !strings.HasPrefix(np, "/") { + return fmt.Errorf(`%q must start with "/"`, np) } - if err := allowPathAccess(buf, "rwkl,", writes); err != nil { - return fmt.Errorf("%s%v", errPrefix, err) + if strings.Contains(np, "$HOME") { + return fmt.Errorf(`$HOME cannot be used in %q`, np) } - spec.AddSnippet(buf.String()) return nil } func init() { - registerIface(&systemFilesInterface{commonInterface{ - name: "system-files", - summary: systemFilesSummary, - implicitOnCore: true, - implicitOnClassic: true, - baseDeclarationSlots: systemFilesBaseDeclarationSlots, - reservedForOS: true, - }}) + registerIface(&systemFilesInterface{ + commonFilesInterface{ + commonInterface: commonInterface{ + name: "system-files", + summary: systemFilesSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: systemFilesBaseDeclarationSlots, + reservedForOS: true, + }, + apparmorHeader: systemFilesConnectedPlugAppArmor, + extraPathValidate: validateSinglePathSystem, + }, + }) } diff --git a/interfaces/builtin/system_files_test.go b/interfaces/builtin/system_files_test.go index ba6b00204e0..06f2002ff24 100644 --- a/interfaces/builtin/system_files_test.go +++ b/interfaces/builtin/system_files_test.go @@ -49,8 +49,8 @@ func (s *systemFilesInterfaceSuite) SetUpTest(c *C) { version: 1.0 plugs: system-files: - read: [$HOME/.read-dir1, /etc/read-dir2, $HOME/.read-file2, /etc/read-file2] - write: [$HOME/.write-dir1, /etc/write-dir2, $HOME/.write-file2, /etc/write-file2] + read: [/etc/read-dir2, /etc/read-file2] + write: [/etc/write-dir2, /etc/write-file2] apps: app: command: foo @@ -79,13 +79,9 @@ func (s *systemFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` # Description: Can access specific system files or directories. # This is restricted because it gives file access to arbitrary locations. -owner "@{HOME}/.read-dir1{,/,/**}" rk, "/etc/read-dir2{,/,/**}" rk, -owner "@{HOME}/.read-file2{,/,/**}" rk, "/etc/read-file2{,/,/**}" rk, -owner "@{HOME}/.write-dir1{,/,/**}" rwkl, "/etc/write-dir2{,/,/**}" rwkl, -owner "@{HOME}/.write-file2{,/,/**}" rwkl, "/etc/write-file2{,/,/**}" rwkl, `) } @@ -110,8 +106,8 @@ func (s *systemFilesInterfaceSuite) TestSanitizePlugHappy(c *C) { version: 1.0 plugs: system-files: - read: ["$HOME/.file1"] - write: ["$HOME/.dir1"] + read: ["/etc/file1"] + write: ["/etc/dir1"] ` info := snaptest.MockInfo(c, mockSnapYaml, nil) plug := info.Plugs["system-files"] @@ -133,14 +129,14 @@ plugs: {`read: ""`, `"read" must be a list of strings`}, {`read: [ 123 ]`, `"read" must be a list of strings`}, {`read: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, - {`read: [ "../foo" ]`, `"../foo" must start with "/" or "\$HOME"`}, + {`read: [ "../foo" ]`, `"../foo" must start with "/"`}, {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, {`write: ""`, `"write" must be a list of strings`}, {`write: bar`, `"write" must be a list of strings`}, - {`read: [ "~/foo" ]`, `"~/foo" must start with "/" or "\$HOME"`}, + {`read: [ "~/foo" ]`, `"~/foo" contains invalid "~"`}, {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, - {`read: [ "/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "/home/\$HOME/foo"`}, + {`read: [ "/home/$HOME/foo" ]`, `\$HOME cannot be used in "/home/\$HOME/foo"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, } From b9e673811c7355f1880a8d4c9eb8ce1cc99918bc Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 13 Nov 2018 15:00:16 +0100 Subject: [PATCH 051/580] tests: fix test-snapd-policy-app-consumer to use correct plugs --- .../test-snapd-policy-app-consumer/meta/snap.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 08d31444e51..6c4eed742a5 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 @@ -369,11 +369,11 @@ plugs: name: test.system system-files: interface: system-files - files: [file1] - dirs: [dir1] - personal-dirs: + read: [/file1] + write: [/dir1] + personal-files: interface: personal-files - files: [file1] - dirs: [dir1] + read: [$HOME/file1] + write: [$HOME/dir1] dummy: interface: dummy From c1b9e27ad3cf441391c0d8e723c646db7d9d2299 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 15 Nov 2018 12:46:31 +0100 Subject: [PATCH 052/580] Use new systemSnapInfo helper. --- overlord/ifacestate/handlers.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 0149b3ed772..ce0c58fc355 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1224,11 +1224,10 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e st.Lock() defer st.Unlock() - core, err := snapstate.CoreInfo(st) + syssnap, err := systemSnapInfo(st) if err != nil { return err } - coreSnapName := core.InstanceName() ifaceName, hotplugKey, err := getHotplugAttrs(task) if err != nil { @@ -1245,9 +1244,9 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e // check for conflicts on all connections first before creating disconnect hooks for _, connRef := range connections { - if err := checkDisconnectConflicts(st, coreSnapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { + if err := checkDisconnectConflicts(st, syssnap.InstanceName(), connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { if _, retry := err.(*state.Retry); retry { - logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", coreSnapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap) + logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", syssnap.InstanceName(), connRef.PlugRef.Snap, connRef.SlotRef.Snap) task.Logf("Waiting for conflicting change in progress...") return err // will retry } From f7b9976616677d14448b9b3c18e1bc3af829fd4b Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 16:34:30 +0100 Subject: [PATCH 053/580] interfaces/builtin/{system,personal}_files.go: set allow-installation: false --- interfaces/builtin/personal_files.go | 4 +--- interfaces/builtin/system_files.go | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index a5d234c49af..a86dd3de867 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -28,9 +28,7 @@ const personalFilesSummary = `allows access to personal files or directories` const personalFilesBaseDeclarationSlots = ` personal-files: - allow-installation: - slot-snap-type: - - core + allow-installation: false deny-connection: true deny-auto-connection: true ` diff --git a/interfaces/builtin/system_files.go b/interfaces/builtin/system_files.go index 050fd789e67..b42554b58f7 100644 --- a/interfaces/builtin/system_files.go +++ b/interfaces/builtin/system_files.go @@ -28,9 +28,7 @@ const systemFilesSummary = `allows access to system files or directories` const systemFilesBaseDeclarationSlots = ` system-files: - allow-installation: - slot-snap-type: - - core + allow-installation: false deny-connection: true deny-auto-connection: true ` From e6ceebca52def54e749c8b7589fa60e79cd87f4c Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Thu, 15 Nov 2018 23:32:58 -0300 Subject: [PATCH 054/580] New tests for personal and system file interfaces --- tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 19 +++- .../main/interfaces-personal-files/task.yaml | 96 +++++++++++++++++++ tests/main/interfaces-system-files/task.yaml | 91 ++++++++++++++++++ 3 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 tests/main/interfaces-personal-files/task.yaml create mode 100644 tests/main/interfaces-system-files/task.yaml diff --git a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml index 004af23d23a..4a29201f109 100644 --- a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -1,6 +1,15 @@ name: test-snapd-sh summary: A no-strings-attached, no-fuss shell for writing tests version: 1.0 + +plugs: + personal-files: + read: $HOME + write: [$HOME/.testdir1, $HOME/.testfile1] + system-files: + read: /tmp + write: [/tmp/.testdir1, /tmp/.testfile1] + apps: test-snapd-sh: command: bin/sh @@ -37,6 +46,12 @@ apps: with-network-setup-observe-plug: command: bin/sh plugs: [network-setup-observe] + with-personal-files-plug: + command: bin/sh + plugs: [personal-files] + with-raw-usb-plug: + command: bin/sh + plugs: [raw-usb] with-removable-media-plug: command: bin/sh plugs: [removable-media] @@ -46,6 +61,6 @@ apps: with-ssh-public-keys-plug: command: bin/sh plugs: [ssh-public-keys] - with-raw-usb-plug: + with-system-files-plug: command: bin/sh - plugs: [raw-usb] + plugs: [system-files] diff --git a/tests/main/interfaces-personal-files/task.yaml b/tests/main/interfaces-personal-files/task.yaml new file mode 100644 index 00000000000..eaf225a7943 --- /dev/null +++ b/tests/main/interfaces-personal-files/task.yaml @@ -0,0 +1,96 @@ +summary: Ensure that the personal-files interface works. + +details: | + The personal-files interface allows access specific personal files or directories. + +prepare: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + install_local test-snapd-sh + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + # Fist layer of dirs and files + ensure_file_exists_backup_real /root/.testfile1 + ensure_file_exists_backup_real /root/testfile1 + ensure_dir_exists_backup_real /root/.testdir1 + ensure_dir_exists_backup_real /root/testdir1 + + # Second layer of dirs and files + ensure_file_exists_backup_real /root/.testdir1/.testfilé2 + ensure_file_exists_backup_real "/root/.testdir1/test file2" + ensure_dir_exists_backup_real /root/.testdir1/.testdir2 + + # Not accessible dirs and files + ensure_dir_exists_backup_real /tmp/.testdir1 + ensure_file_exists_backup_real /tmp/.testfile1 + ensure_dir_exists_backup_real /tmp/root/ + ensure_file_exists_backup_real /tmp/root/testfile2 + ensure_file_exists_backup_real /tmp/root/.testfile2 + +restore: | + rm -f call.error + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + clean_file /root/.testfile1 + clean_file /root/testfile1 + clean_dir /root/.testdir1 + clean_dir /root/testdir1 + clean_dir /tmp/root + +execute: | + echo "The interface is not connected by default" + snap interfaces -i personal-files | MATCH "\\- +test-snapd-sh:personal-files" + + echo "When the interface is connected" + snap connect test-snapd-sh:personal-files + + echo "And not other interfaces are connected" + snap disconnect test-snapd-sh:home + + echo "Then the snap is able to access all the files and dirs in $HOME" + test-snapd-sh.with-personal-files-plug -c "cat /root/.testfile1" + test-snapd-sh.with-personal-files-plug -c "cat /root/testfile1" + test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1" + test-snapd-sh.with-personal-files-plug -c "ls /root/testdir1" + test-snapd-sh.with-personal-files-plug -c "cat /root/.testdir1/.testfilé2" + test-snapd-sh.with-personal-files-plug -c "cat '/root/.testdir1/test file2'" + test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1/.testdir2/" + + echo "Then the snap is able to write just /root/.testdir1 and /root/.testfile1" + test-snapd-sh.with-personal-files-plug -c "echo test >> /root/.testfile1" + test-snapd-sh.with-personal-files-plug -c "touch /root/.testdir1/testfilé2" + if test-snapd-sh.with-personal-files-plug -c "echo test >> /root/testfile1" 2> call.error; then + echo "Expected permission error writing the personal file" + exit 1 + fi + MATCH "Permission denied" < call.error + + echo "Then the snap is not able to to access files and dirs outside $HOME" + test-snapd-sh.with-personal-files-plug -c "ls /tmp/.testdir1" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /tmp/.testfile1" && exit 1 + test-snapd-sh.with-personal-files-plug -c "ls /tmp/root/" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/testfile2" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/.testfile2" && exit 1 + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-sh:personal-files + + echo "Then the snap is not able to read files and dirs in $HOME" + if test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1" 2> call.error; then + echo "Expected permission error accessing the personal dir" + exit 1 + fi + MATCH "Permission denied" < call.error + if test-snapd-sh.with-personal-files-plug -c "cat /root/.testfile1" 2> call.error; then + echo "Expected permission error accessing the personal file" + exit 1 + fi + MATCH "Permission denied" < call.error diff --git a/tests/main/interfaces-system-files/task.yaml b/tests/main/interfaces-system-files/task.yaml new file mode 100644 index 00000000000..d6814720623 --- /dev/null +++ b/tests/main/interfaces-system-files/task.yaml @@ -0,0 +1,91 @@ +summary: Ensure that the system-files interface works. + +details: | + The system-files interface allows access specific system files or directories. + +prepare: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + install_local test-snapd-sh + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + # Fist layer of dirs and files + ensure_file_exists_backup_real /tmp/.testfile1 + ensure_file_exists_backup_real /tmp/testfile1 + ensure_dir_exists_backup_real /tmp/.testdir1 + ensure_dir_exists_backup_real /tmp/testdir1 + + # Second layer of dirs and files + ensure_file_exists_backup_real /tmp/.testdir1/.testfilé2 + ensure_file_exists_backup_real "/tmp/.testdir1/test file2" + ensure_dir_exists_backup_real /tmp/root + + # Not accessible dirs and files + ensure_dir_exists_backup_real /root/.testdir1 + ensure_file_exists_backup_real /root/.testfile1 + +restore: | + rm -f call.error + + # shellcheck source=tests/lib/files.sh + . "$TESTSLIB/files.sh" + + clean_file /tmp/.testfile1 + clean_file /tmp/testfile1 + clean_dir /tmp/.testdir1 + clean_dir /tmp/testdir1 + clean_dir /root/.testdir1 + clean_dir /root/.testfile1 + +execute: | + echo "The interface is not connected by default" + snap interfaces -i system-files | MATCH "\\- +test-snapd-sh:system-files" + + echo "When the interface is connected" + snap connect test-snapd-sh:system-files + + echo "And not other interfaces are connected" + snap disconnect test-snapd-sh:home + + echo "Then the snap is able to access all the files and dirs in /tmp" + test-snapd-sh.with-system-files-plug -c "cat /tmp/.testfile1" + test-snapd-sh.with-system-files-plug -c "cat /tmp/testfile1" + test-snapd-sh.with-system-files-plug -c "ls /tmp/.testdir1" + test-snapd-sh.with-system-files-plug -c "ls /tmp/testdir1" + test-snapd-sh.with-system-files-plug -c "cat /tmp/.testdir1/.testfilé2" + test-snapd-sh.with-system-files-plug -c "cat '/tmp/.testdir1/test file2'" + test-snapd-sh.with-system-files-plug -c "ls /tmp/root/" + + echo "Then the snap is able to write just /tmp/.testdir1 and /tmp/.testfile1" + test-snapd-sh.with-personal-files-plug -c "echo test >> /tmp/.testfile1" + test-snapd-sh.with-personal-files-plug -c "touch /tmp/.testfile1/testfilé2" + if test-snapd-sh.with-personal-files-plug -c "echo test >> /tmp/testfile1" 2> call.error; then + echo "Expected permission error writing the system file" + exit 1 + fi + MATCH "Permission denied" < call.error + + echo "Then the snap is not able to to access files and dirs in $HOME" + test-snapd-sh.with-system-files-plug -c "ls /root/.testdir1" && exit 1 + test-snapd-sh.with-system-files-plug -c "cat /root/.testfile1" && exit 1 + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-sh:system-files + + echo "Then the snap is not able to read files and dirs in $HOME" + if test-snapd-sh.with-system-files-plug -c "ls /tmp/.testdir1" 2> call.error; then + echo "Expected permission error accessing the system dir" + exit 1 + fi + MATCH "Permission denied" < call.error + if test-snapd-sh.with-system-files-plug -c "cat /tmp/.testfile1" 2> call.error; then + echo "Expected permission error accessing the system file" + exit 1 + fi + MATCH "Permission denied" < call.error From 2ae910bf6de53b08f72e9bc70cd827d68d3722bb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 16 Nov 2018 08:38:30 +0100 Subject: [PATCH 055/580] interfaces: fix deny-auto-connection rule in {personal,system}-files --- interfaces/builtin/personal_files.go | 11 ++++++++++- interfaces/builtin/system_files.go | 11 ++++++++++- interfaces/policy/basedeclaration_test.go | 6 ++++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index a86dd3de867..a8cf83a9d53 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -26,9 +26,17 @@ import ( const personalFilesSummary = `allows access to personal files or directories` -const personalFilesBaseDeclarationSlots = ` +const personalFilesBaseDeclarationPlugs = ` personal-files: allow-installation: false + deny-auto-connection: true +` + +const personalFilesBaseDeclarationSlots = ` + personal-files: + allow-installation: + slot-snap-type: + - core deny-connection: true deny-auto-connection: true ` @@ -60,6 +68,7 @@ func init() { summary: personalFilesSummary, implicitOnCore: true, implicitOnClassic: true, + baseDeclarationPlugs: personalFilesBaseDeclarationPlugs, baseDeclarationSlots: personalFilesBaseDeclarationSlots, reservedForOS: true, }, diff --git a/interfaces/builtin/system_files.go b/interfaces/builtin/system_files.go index b42554b58f7..ea73ad3dd11 100644 --- a/interfaces/builtin/system_files.go +++ b/interfaces/builtin/system_files.go @@ -26,9 +26,17 @@ import ( const systemFilesSummary = `allows access to system files or directories` -const systemFilesBaseDeclarationSlots = ` +const systemFilesBaseDeclarationPlugs = ` system-files: allow-installation: false + deny-auto-connection: true +` + +const systemFilesBaseDeclarationSlots = ` + system-files: + allow-installation: + slot-snap-type: + - core deny-connection: true deny-auto-connection: true ` @@ -61,6 +69,7 @@ func init() { summary: systemFilesSummary, implicitOnCore: true, implicitOnClassic: true, + baseDeclarationPlugs: systemFilesBaseDeclarationPlugs, baseDeclarationSlots: systemFilesBaseDeclarationSlots, reservedForOS: true, }, diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go index e246c2351a2..5ef9249c232 100644 --- a/interfaces/policy/basedeclaration_test.go +++ b/interfaces/policy/basedeclaration_test.go @@ -645,7 +645,9 @@ func (s *baseDeclSuite) TestPlugInstallation(c *C) { "kernel-module-control": true, "kubernetes-support": true, "lxd-support": true, + "personal-files": true, "snapd-control": true, + "system-files": true, "unity8": true, } @@ -695,9 +697,7 @@ func (s *baseDeclSuite) TestConnection(c *C) { "mir": true, "network-status": true, "online-accounts-service": true, - "personal-files": true, "storage-framework-service": true, - "system-files": true, "thumbnailer-service": true, "ubuntu-download-manager": true, "unity8-calendar": true, @@ -772,7 +772,9 @@ func (s *baseDeclSuite) TestSanity(c *C) { "kernel-module-control": true, "kubernetes-support": true, "lxd-support": true, + "personal-files": true, "snapd-control": true, + "system-files": true, "udisks2": true, "unity8": true, "wayland": true, From 9eed804c62acbccdc5a13fa5c99cf549f1bc9d5d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 16 Nov 2018 09:42:16 +0100 Subject: [PATCH 056/580] run-checks: remove leftover "dotfiles" reference --- run-checks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run-checks b/run-checks index d5a972ee99a..15b55ca600b 100755 --- a/run-checks +++ b/run-checks @@ -111,7 +111,7 @@ missing_interface_spread_test() { # skip gadget provided interfaces for now continue ;; - dbus|content|dotfiles) + dbus|content) search="interface: $iface" ;; autopilot) From b3ef9353c7c42ffdeec9ce96382eca66c94e822d Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 16 Nov 2018 15:54:04 +0100 Subject: [PATCH 057/580] address review feedback --- interfaces/builtin/common_files.go | 8 +++++++- interfaces/builtin/personal_files.go | 1 - interfaces/builtin/system_files.go | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index f783ac097de..cca437cd90f 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -43,7 +43,7 @@ func formatPath(ip interface{}) (string, error) { return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) } prefix := "" - if strings.Count(p, "$HOME") > 0 { + if strings.HasPrefix(p, "$HOME") && strings.Count(p, "$HOME") == 1 { p = strings.Replace(p, "$HOME", "@{HOME}", 1) prefix = "owner " } @@ -94,6 +94,12 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error { return err } + // extraPathValidation is implemented must be implemented by + // the interface that build on top of the abstract + // commonFilesInterface + if iface.extraPathValidate == nil { + panic("extraPathValidate must be set when using the commonFilesInterface") + } if err := iface.extraPathValidate(np); err != nil { return err } diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index a8cf83a9d53..435fa46103c 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -37,7 +37,6 @@ const personalFilesBaseDeclarationSlots = ` allow-installation: slot-snap-type: - core - deny-connection: true deny-auto-connection: true ` diff --git a/interfaces/builtin/system_files.go b/interfaces/builtin/system_files.go index ea73ad3dd11..5108cb1dd9b 100644 --- a/interfaces/builtin/system_files.go +++ b/interfaces/builtin/system_files.go @@ -37,7 +37,6 @@ const systemFilesBaseDeclarationSlots = ` allow-installation: slot-snap-type: - core - deny-connection: true deny-auto-connection: true ` From 2eeb3eea8e2d2c0aa12170b41fc43b67859ef324 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 16 Nov 2018 13:48:06 -0300 Subject: [PATCH 058/580] Fix for the test-snapd-sh snap declaration --- tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml index 4a29201f109..44df09b2a6b 100644 --- a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -4,10 +4,10 @@ version: 1.0 plugs: personal-files: - read: $HOME + read: [$HOME/.testdir1, $HOME/.testfile1, $HOME/testfile1, $HOME/testdir1] write: [$HOME/.testdir1, $HOME/.testfile1] system-files: - read: /tmp + read: [/tmp, /tmp/.testdir1] write: [/tmp/.testdir1, /tmp/.testfile1] apps: From 06c4bedbe7d44ced85d642932cfa9e648ccf06e8 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 16 Nov 2018 16:05:33 -0300 Subject: [PATCH 059/580] Fix spread tests for personal and system files interface On personal-files: the check for strict confinement is done before checking the negative scenario, to avoid failures on some systems. On system-files: the check is done on /testdir instead of using /tmp --- tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 4 +- .../main/interfaces-personal-files/task.yaml | 12 +++-- tests/main/interfaces-system-files/task.yaml | 50 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml index 44df09b2a6b..e9ada292826 100644 --- a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -7,8 +7,8 @@ plugs: read: [$HOME/.testdir1, $HOME/.testfile1, $HOME/testfile1, $HOME/testdir1] write: [$HOME/.testdir1, $HOME/.testfile1] system-files: - read: [/tmp, /tmp/.testdir1] - write: [/tmp/.testdir1, /tmp/.testfile1] + read: [/testdir] + write: [/testdir/.testdir1, /testdir/.testfile1] apps: test-snapd-sh: diff --git a/tests/main/interfaces-personal-files/task.yaml b/tests/main/interfaces-personal-files/task.yaml index eaf225a7943..f18c36de5df 100644 --- a/tests/main/interfaces-personal-files/task.yaml +++ b/tests/main/interfaces-personal-files/task.yaml @@ -60,9 +60,15 @@ execute: | test-snapd-sh.with-personal-files-plug -c "cat '/root/.testdir1/test file2'" test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1/.testdir2/" - echo "Then the snap is able to write just /root/.testdir1 and /root/.testfile1" + echo "Then the snap is able to write on /root/.testdir1 and /root/.testfile1" test-snapd-sh.with-personal-files-plug -c "echo test >> /root/.testfile1" test-snapd-sh.with-personal-files-plug -c "touch /root/.testdir1/testfilé2" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "Then the snap is no able to write outside /root/.testdir1 and /root/.testfile1" if test-snapd-sh.with-personal-files-plug -c "echo test >> /root/testfile1" 2> call.error; then echo "Expected permission error writing the personal file" exit 1 @@ -76,10 +82,6 @@ execute: | test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/testfile2" && exit 1 test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/.testfile2" && exit 1 - if [ "$(snap debug confinement)" = partial ] ; then - exit 0 - fi - echo "When the plug is disconnected" snap disconnect test-snapd-sh:personal-files diff --git a/tests/main/interfaces-system-files/task.yaml b/tests/main/interfaces-system-files/task.yaml index d6814720623..0aecad86edd 100644 --- a/tests/main/interfaces-system-files/task.yaml +++ b/tests/main/interfaces-system-files/task.yaml @@ -12,15 +12,16 @@ prepare: | . "$TESTSLIB/files.sh" # Fist layer of dirs and files - ensure_file_exists_backup_real /tmp/.testfile1 - ensure_file_exists_backup_real /tmp/testfile1 - ensure_dir_exists_backup_real /tmp/.testdir1 - ensure_dir_exists_backup_real /tmp/testdir1 + ensure_dir_exists_backup_real /testdir + ensure_file_exists_backup_real /testdir/.testfile1 + ensure_file_exists_backup_real /testdir/testfile1 + ensure_dir_exists_backup_real /testdir/.testdir1 + ensure_dir_exists_backup_real /testdir/testdir1 # Second layer of dirs and files - ensure_file_exists_backup_real /tmp/.testdir1/.testfilé2 - ensure_file_exists_backup_real "/tmp/.testdir1/test file2" - ensure_dir_exists_backup_real /tmp/root + ensure_file_exists_backup_real /testdir/.testdir1/.testfilé2 + ensure_file_exists_backup_real "/testdir/.testdir1/test file2" + ensure_dir_exists_backup_real /testdir/root # Not accessible dirs and files ensure_dir_exists_backup_real /root/.testdir1 @@ -32,10 +33,7 @@ restore: | # shellcheck source=tests/lib/files.sh . "$TESTSLIB/files.sh" - clean_file /tmp/.testfile1 - clean_file /tmp/testfile1 - clean_dir /tmp/.testdir1 - clean_dir /tmp/testdir1 + clean_dir /testdir clean_dir /root/.testdir1 clean_dir /root/.testfile1 @@ -49,19 +47,19 @@ execute: | echo "And not other interfaces are connected" snap disconnect test-snapd-sh:home - echo "Then the snap is able to access all the files and dirs in /tmp" - test-snapd-sh.with-system-files-plug -c "cat /tmp/.testfile1" - test-snapd-sh.with-system-files-plug -c "cat /tmp/testfile1" - test-snapd-sh.with-system-files-plug -c "ls /tmp/.testdir1" - test-snapd-sh.with-system-files-plug -c "ls /tmp/testdir1" - test-snapd-sh.with-system-files-plug -c "cat /tmp/.testdir1/.testfilé2" - test-snapd-sh.with-system-files-plug -c "cat '/tmp/.testdir1/test file2'" - test-snapd-sh.with-system-files-plug -c "ls /tmp/root/" - - echo "Then the snap is able to write just /tmp/.testdir1 and /tmp/.testfile1" - test-snapd-sh.with-personal-files-plug -c "echo test >> /tmp/.testfile1" - test-snapd-sh.with-personal-files-plug -c "touch /tmp/.testfile1/testfilé2" - if test-snapd-sh.with-personal-files-plug -c "echo test >> /tmp/testfile1" 2> call.error; then + echo "Then the snap is able to access all the files and dirs in /testdir" + test-snapd-sh.with-system-files-plug -c "cat /testdir/.testfile1" + test-snapd-sh.with-system-files-plug -c "cat /testdir/testfile1" + test-snapd-sh.with-system-files-plug -c "ls /testdir/.testdir1" + test-snapd-sh.with-system-files-plug -c "ls /testdir/testdir1" + test-snapd-sh.with-system-files-plug -c "cat /testdir/.testdir1/.testfilé2" + test-snapd-sh.with-system-files-plug -c "cat '/testdir/.testdir1/test file2'" + test-snapd-sh.with-system-files-plug -c "ls /testdir/root/" + + echo "Then the snap is able to write just /testdir/.testdir1 and /testdir/.testfile1" + test-snapd-sh.with-personal-files-plug -c "echo test >> /testdir/.testfile1" + test-snapd-sh.with-personal-files-plug -c "touch /testdir/.testfile1/testfilé2" + if test-snapd-sh.with-personal-files-plug -c "echo test >> /testdir/testfile1" 2> call.error; then echo "Expected permission error writing the system file" exit 1 fi @@ -79,12 +77,12 @@ execute: | snap disconnect test-snapd-sh:system-files echo "Then the snap is not able to read files and dirs in $HOME" - if test-snapd-sh.with-system-files-plug -c "ls /tmp/.testdir1" 2> call.error; then + if test-snapd-sh.with-system-files-plug -c "ls /testdir/.testdir1" 2> call.error; then echo "Expected permission error accessing the system dir" exit 1 fi MATCH "Permission denied" < call.error - if test-snapd-sh.with-system-files-plug -c "cat /tmp/.testfile1" 2> call.error; then + if test-snapd-sh.with-system-files-plug -c "cat /testdir/.testfile1" 2> call.error; then echo "Expected permission error accessing the system file" exit 1 fi From 39f79094692a8a687923e6bf05e7d75a18896c53 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 09:44:03 +0100 Subject: [PATCH 060/580] interfaces: refactor allowPathAccess to take new typed filesAAPerm --- interfaces/builtin/common_files.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index cca437cd90f..e8fc88629fb 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -37,6 +37,25 @@ type commonFilesInterface struct { extraPathValidate func(string) error } +// filesAAPerm can either be files{Read,Write} and converted to a string +// expands into the right apparmor permissions for the files interface. +type filesAAPerm int + +const ( + filesRead filesAAPerm = iota + filesWrite +) + +func (a filesAAPerm) String() string { + switch a { + case filesRead: + return "rk," + case filesWrite: + return "rwkl," + } + panic(fmt.Sprintf("invalid perm: %d", a)) +} + func formatPath(ip interface{}) (string, error) { p, ok := ip.(string) if !ok { @@ -52,7 +71,7 @@ func formatPath(ip interface{}) (string, error) { return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil } -func allowPathAccess(buf *bytes.Buffer, perm string, paths []interface{}) error { +func allowPathAccess(buf *bytes.Buffer, perm filesAAPerm, paths []interface{}) error { for _, rawPath := range paths { p, err := formatPath(rawPath) if err != nil { @@ -135,10 +154,10 @@ func (iface *commonFilesInterface) AppArmorConnectedPlug(spec *apparmor.Specific errPrefix := fmt.Sprintf(`cannot connect plug %s: `, plug.Name()) buf := bytes.NewBufferString(iface.apparmorHeader) - if err := allowPathAccess(buf, "rk,", reads); err != nil { + if err := allowPathAccess(buf, filesRead, reads); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } - if err := allowPathAccess(buf, "rwkl,", writes); err != nil { + if err := allowPathAccess(buf, filesWrite, writes); err != nil { return fmt.Errorf("%s%v", errPrefix, err) } spec.AddSnippet(buf.String()) From 2180a20865dd57e58d18fc43407568ea77262409 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 09:50:17 +0100 Subject: [PATCH 061/580] interfaces: simplify validateSinglePath() in commonFilesInterface --- interfaces/builtin/common_files.go | 3 --- interfaces/builtin/personal_files_test.go | 4 ++-- interfaces/builtin/system_files_test.go | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index e8fc88629fb..6a2d49954dc 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -99,9 +99,6 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error { if strings.HasSuffix(np, "/") { return fmt.Errorf(`%q cannot end with "/"`, np) } - if strings.Contains(np, "@{") { - return fmt.Errorf(`%q should not use "@{"`, np) - } p := filepath.Clean(np) if p != np { return fmt.Errorf("%q must be clean", np) diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go index cc12d2905f6..ca196ab42dd 100644 --- a/interfaces/builtin/personal_files_test.go +++ b/interfaces/builtin/personal_files_test.go @@ -137,8 +137,8 @@ plugs: {`read: [ "$HOME/foo/~/foo" ]`, `"\$HOME/foo/~/foo" contains invalid "~"`}, {`read: [ "$HOME/foo/../foo" ]`, `"\$HOME/foo/../foo" must be clean`}, {`read: [ "$HOME/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "\$HOME/home/\$HOME/foo"`}, - {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, - {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, + {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, + {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, } for _, t := range testCases { diff --git a/interfaces/builtin/system_files_test.go b/interfaces/builtin/system_files_test.go index 06f2002ff24..4b3834aa507 100644 --- a/interfaces/builtin/system_files_test.go +++ b/interfaces/builtin/system_files_test.go @@ -137,8 +137,8 @@ plugs: {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, {`read: [ "/home/$HOME/foo" ]`, `\$HOME cannot be used in "/home/\$HOME/foo"`}, - {`read: [ "/@{FOO}" ]`, `"/@{FOO}" should not use "@{"`}, - {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" should not use "@{"`}, + {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, + {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, } for _, t := range testCases { From ad563128ddaa17dbabffeb834874a304e38ba096 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 09:51:46 +0100 Subject: [PATCH 062/580] interfaces: address review feedback (thansk for jdstrand) --- interfaces/builtin/common_files.go | 5 ++--- interfaces/builtin/personal_files.go | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index 6a2d49954dc..e0201adfb17 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -110,9 +110,8 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error { return err } - // extraPathValidation is implemented must be implemented by - // the interface that build on top of the abstract - // commonFilesInterface + // extraPathValidation must be implemented by the interface + // that build on top of the abstract commonFilesInterface if iface.extraPathValidate == nil { panic("extraPathValidate must be set when using the commonFilesInterface") } diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index 435fa46103c..b36a6ef1cf5 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -41,7 +41,8 @@ const personalFilesBaseDeclarationSlots = ` ` const personalFilesConnectedPlugAppArmor = ` -# Description: Can access specific personal files or directories. +# Description: Can access specific personal files or directories in the +# users's home directory. # This is restricted because it gives file access to arbitrary locations. ` From c265f0ea31525a633dd6b8b43b32ad4c7cd2dfaf Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 11:40:50 +0100 Subject: [PATCH 063/580] tests: update {personal,system}-files tests Mostly based on the feedback from jdstrand. One limitation that the interfaces right now has is that it can only access files under one of the standard paths that snap-confine maps. So something like /random/ is not supported yet. --- tests/lib/files.sh | 7 ++- tests/lib/snaps/test-snapd-sh/meta/snap.yaml | 4 +- .../main/interfaces-personal-files/task.yaml | 30 +++++------ tests/main/interfaces-system-files/task.yaml | 52 ++++++++++--------- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/tests/lib/files.sh b/tests/lib/files.sh index 888df21e077..53757e6f433 100644 --- a/tests/lib/files.sh +++ b/tests/lib/files.sh @@ -45,8 +45,8 @@ clean_dir() { ensure_file_exists() { file="$1" if ! [ -f "$file" ]; then - touch "$file" - touch "$file.fake" + echo "content for $file" > "$file" + echo "content for fake $file" > "$file.fake" fi } @@ -59,8 +59,7 @@ ensure_file_exists_backup_real() { if [ ! -d "$(dirname "$file")" ]; then mkdir -p "$(dirname "$file")" fi - touch "$file" - touch "$file.fake" + ensure_file_exists "$file" } clean_file() { diff --git a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml index e9ada292826..989d42b396d 100644 --- a/tests/lib/snaps/test-snapd-sh/meta/snap.yaml +++ b/tests/lib/snaps/test-snapd-sh/meta/snap.yaml @@ -7,8 +7,8 @@ plugs: read: [$HOME/.testdir1, $HOME/.testfile1, $HOME/testfile1, $HOME/testdir1] write: [$HOME/.testdir1, $HOME/.testfile1] system-files: - read: [/testdir] - write: [/testdir/.testdir1, /testdir/.testfile1] + read: [/mnt/testdir] + write: [/mnt/testdir/.testdir1, /mnt/testdir/.testfile1] apps: test-snapd-sh: diff --git a/tests/main/interfaces-personal-files/task.yaml b/tests/main/interfaces-personal-files/task.yaml index f18c36de5df..29b78501c2d 100644 --- a/tests/main/interfaces-personal-files/task.yaml +++ b/tests/main/interfaces-personal-files/task.yaml @@ -23,11 +23,11 @@ prepare: | ensure_dir_exists_backup_real /root/.testdir1/.testdir2 # Not accessible dirs and files - ensure_dir_exists_backup_real /tmp/.testdir1 - ensure_file_exists_backup_real /tmp/.testfile1 - ensure_dir_exists_backup_real /tmp/root/ - ensure_file_exists_backup_real /tmp/root/testfile2 - ensure_file_exists_backup_real /tmp/root/.testfile2 + ensure_dir_exists_backup_real /var/tmp/.testdir1 + ensure_file_exists_backup_real /var/tmp/.testfile1 + ensure_dir_exists_backup_real /var/tmp/root/ + ensure_file_exists_backup_real /var/tmp/root/testfile2 + ensure_file_exists_backup_real /var/tmp/root/.testfile2 restore: | rm -f call.error @@ -39,7 +39,7 @@ restore: | clean_file /root/testfile1 clean_dir /root/.testdir1 clean_dir /root/testdir1 - clean_dir /tmp/root + clean_dir /var/tmp/root execute: | echo "The interface is not connected by default" @@ -52,12 +52,12 @@ execute: | snap disconnect test-snapd-sh:home echo "Then the snap is able to access all the files and dirs in $HOME" - test-snapd-sh.with-personal-files-plug -c "cat /root/.testfile1" - test-snapd-sh.with-personal-files-plug -c "cat /root/testfile1" + test-snapd-sh.with-personal-files-plug -c "cat /root/.testfile1" | MATCH "content for /root/.testfile1" + test-snapd-sh.with-personal-files-plug -c "cat /root/testfile1" | MATCH "content for /root/testfile1" test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1" test-snapd-sh.with-personal-files-plug -c "ls /root/testdir1" - test-snapd-sh.with-personal-files-plug -c "cat /root/.testdir1/.testfilé2" - test-snapd-sh.with-personal-files-plug -c "cat '/root/.testdir1/test file2'" + test-snapd-sh.with-personal-files-plug -c "cat /root/.testdir1/.testfilé2" | MATCH "content for /root/.testdir1/.testfilé2" + test-snapd-sh.with-personal-files-plug -c "cat '/root/.testdir1/test file2'" | MATCH "content for /root/.testdir1/test file2" test-snapd-sh.with-personal-files-plug -c "ls /root/.testdir1/.testdir2/" echo "Then the snap is able to write on /root/.testdir1 and /root/.testfile1" @@ -76,11 +76,11 @@ execute: | MATCH "Permission denied" < call.error echo "Then the snap is not able to to access files and dirs outside $HOME" - test-snapd-sh.with-personal-files-plug -c "ls /tmp/.testdir1" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /tmp/.testfile1" && exit 1 - test-snapd-sh.with-personal-files-plug -c "ls /tmp/root/" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/testfile2" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /tmp/root/.testfile2" && exit 1 + test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/.testdir1" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/.testfile1" && exit 1 + test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/root/" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/testfile2" && exit 1 + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/.testfile2" && exit 1 echo "When the plug is disconnected" snap disconnect test-snapd-sh:personal-files diff --git a/tests/main/interfaces-system-files/task.yaml b/tests/main/interfaces-system-files/task.yaml index 0aecad86edd..535dfe7deae 100644 --- a/tests/main/interfaces-system-files/task.yaml +++ b/tests/main/interfaces-system-files/task.yaml @@ -3,6 +3,10 @@ summary: Ensure that the system-files interface works. details: | The system-files interface allows access specific system files or directories. +environment: + # keep in sync with tests/lib/snaps/test-snapd-sh/meta/snap.yaml + TESTDIR: /mnt/testdir + prepare: | # shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" @@ -12,16 +16,16 @@ prepare: | . "$TESTSLIB/files.sh" # Fist layer of dirs and files - ensure_dir_exists_backup_real /testdir - ensure_file_exists_backup_real /testdir/.testfile1 - ensure_file_exists_backup_real /testdir/testfile1 - ensure_dir_exists_backup_real /testdir/.testdir1 - ensure_dir_exists_backup_real /testdir/testdir1 + ensure_dir_exists_backup_real "$TESTDIR" + ensure_file_exists_backup_real "$TESTDIR"/.testfile1 + ensure_file_exists_backup_real "$TESTDIR"/testfile1 + ensure_dir_exists_backup_real "$TESTDIR"/.testdir1 + ensure_dir_exists_backup_real "$TESTDIR"/testdir1 # Second layer of dirs and files - ensure_file_exists_backup_real /testdir/.testdir1/.testfilé2 - ensure_file_exists_backup_real "/testdir/.testdir1/test file2" - ensure_dir_exists_backup_real /testdir/root + ensure_file_exists_backup_real "$TESTDIR"/.testdir1/.testfilé2 + ensure_file_exists_backup_real "$TESTDIR/.testdir1/test file2" + ensure_dir_exists_backup_real "$TESTDIR"/root # Not accessible dirs and files ensure_dir_exists_backup_real /root/.testdir1 @@ -33,8 +37,8 @@ restore: | # shellcheck source=tests/lib/files.sh . "$TESTSLIB/files.sh" - clean_dir /testdir - clean_dir /root/.testdir1 + clean_dir "$TESTDIR" + clean_dir /root/.testdira1 clean_dir /root/.testfile1 execute: | @@ -48,18 +52,18 @@ execute: | snap disconnect test-snapd-sh:home echo "Then the snap is able to access all the files and dirs in /testdir" - test-snapd-sh.with-system-files-plug -c "cat /testdir/.testfile1" - test-snapd-sh.with-system-files-plug -c "cat /testdir/testfile1" - test-snapd-sh.with-system-files-plug -c "ls /testdir/.testdir1" - test-snapd-sh.with-system-files-plug -c "ls /testdir/testdir1" - test-snapd-sh.with-system-files-plug -c "cat /testdir/.testdir1/.testfilé2" - test-snapd-sh.with-system-files-plug -c "cat '/testdir/.testdir1/test file2'" - test-snapd-sh.with-system-files-plug -c "ls /testdir/root/" - - echo "Then the snap is able to write just /testdir/.testdir1 and /testdir/.testfile1" - test-snapd-sh.with-personal-files-plug -c "echo test >> /testdir/.testfile1" - test-snapd-sh.with-personal-files-plug -c "touch /testdir/.testfile1/testfilé2" - if test-snapd-sh.with-personal-files-plug -c "echo test >> /testdir/testfile1" 2> call.error; then + test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/.testfile1" | MATCH "content for $TESTDIR/.testfile1" + test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/testfile1" MATCH "content for $TESTDIR/testfile1" + test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/.testdir1" + test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/testdir1" + test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/.testdir1/.testfilé2" | MATCH "content for $TESTDIR/.testdir1/.testfilé2" + test-snapd-sh.with-system-files-plug -c "cat $TESTDIR'/.testdir1/test file2'" | MATCH "content for $TESTDIR/.testdir1/test file2" + test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/root/" + + echo "Then the snap is able to write just $TESTDIR/.testdir1 and $TESTDIR/.testfile1" + test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/.testfile1" + test-snapd-sh.with-system-files-plug -c "touch $TESTDIR/.testdir1/testfilé2" + if test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/testfile1" 2> call.error; then echo "Expected permission error writing the system file" exit 1 fi @@ -77,12 +81,12 @@ execute: | snap disconnect test-snapd-sh:system-files echo "Then the snap is not able to read files and dirs in $HOME" - if test-snapd-sh.with-system-files-plug -c "ls /testdir/.testdir1" 2> call.error; then + if test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/.testdir1" 2> call.error; then echo "Expected permission error accessing the system dir" exit 1 fi MATCH "Permission denied" < call.error - if test-snapd-sh.with-system-files-plug -c "cat /testdir/.testfile1" 2> call.error; then + if test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/.testfile1" 2> call.error; then echo "Expected permission error accessing the system file" exit 1 fi From b1dd80258f4a0430efb29469f1601b1caa247666 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 12:07:24 +0100 Subject: [PATCH 064/580] interfaces: update personal-files test --- interfaces/builtin/personal_files_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go index ca196ab42dd..dbf5637b69c 100644 --- a/interfaces/builtin/personal_files_test.go +++ b/interfaces/builtin/personal_files_test.go @@ -77,7 +77,8 @@ func (s *personalFilesInterfaceSuite) TestConnectedPlugAppArmor(c *C) { c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) c.Check(apparmorSpec.SnippetForTag("snap.other.app"), Equals, ` -# Description: Can access specific personal files or directories. +# Description: Can access specific personal files or directories in the +# users's home directory. # This is restricted because it gives file access to arbitrary locations. owner "@{HOME}/.read-dir{,/,/**}" rk, owner "@{HOME}/.read-file{,/,/**}" rk, From a689149730a2fad36d3ea115c1fd1f1a8f5664fe Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 19 Nov 2018 14:30:29 +0100 Subject: [PATCH 065/580] tests: move confinement check in interfaces-system-files up --- tests/main/interfaces-system-files/task.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/main/interfaces-system-files/task.yaml b/tests/main/interfaces-system-files/task.yaml index 535dfe7deae..90225273282 100644 --- a/tests/main/interfaces-system-files/task.yaml +++ b/tests/main/interfaces-system-files/task.yaml @@ -63,6 +63,11 @@ execute: | echo "Then the snap is able to write just $TESTDIR/.testdir1 and $TESTDIR/.testfile1" test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/.testfile1" test-snapd-sh.with-system-files-plug -c "touch $TESTDIR/.testdir1/testfilé2" + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + if test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/testfile1" 2> call.error; then echo "Expected permission error writing the system file" exit 1 @@ -73,10 +78,6 @@ execute: | test-snapd-sh.with-system-files-plug -c "ls /root/.testdir1" && exit 1 test-snapd-sh.with-system-files-plug -c "cat /root/.testfile1" && exit 1 - if [ "$(snap debug confinement)" = partial ] ; then - exit 0 - fi - echo "When the plug is disconnected" snap disconnect test-snapd-sh:system-files From 177f46d48f0668a0303aa28b660645cc006eca3b Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 21 Nov 2018 18:09:37 -0300 Subject: [PATCH 066/580] Using MATCH to validate files content --- tests/main/snapshot-users/task.yaml | 40 +++++++++++++---------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/tests/main/snapshot-users/task.yaml b/tests/main/snapshot-users/task.yaml index a8eefaf762e..cbd963dfd00 100644 --- a/tests/main/snapshot-users/task.yaml +++ b/tests/main/snapshot-users/task.yaml @@ -53,16 +53,16 @@ execute: | # restore the snapshot for the root user and check the files snap restore "$SET_ID_ROOT" test-snapd-tools - test -e /root/snap/test-snapd-tools/current/canary.txt - test -e /root/snap/test-snapd-tools/common/canary.txt + MATCH "hello versioned test-snapd-tools" < /root/snap/test-snapd-tools/current/canary.txt + MATCH "hello common test-snapd-tools" < /root/snap/test-snapd-tools/common/canary.txt test ! -e /home/test/snap/test-snapd-tools/current/canary.txt test ! -e /home/test/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary/canary.txt # restore the snapshot for the test user and check the files snap restore "$SET_ID_TEST" test-snapd-tools - test -e /home/test/snap/test-snapd-tools/current/canary.txt - test -e /home/test/snap/test-snapd-tools/common/canary.txt + MATCH "hello versioned test-snapd-tools" < /home/test/snap/test-snapd-tools/current/canary.txt + MATCH "hello common test-snapd-tools" < /home/test/snap/test-snapd-tools/common/canary.txt test ! -d /home/test/snap/test-snapd-tools/current/canary # remove the canaries for both users @@ -71,10 +71,10 @@ execute: | # restore the snapshot for both users and check the files snap restore "$SET_ID_BOTH" test-snapd-tools - test -e /root/snap/test-snapd-tools/current/canary.txt - test -e /root/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary.txt - test -e /home/test/snap/test-snapd-tools/common/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/common/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/common/canary.txt test ! -d /home/test/snap/test-snapd-tools/current/canary # remove the canaries for both users @@ -83,8 +83,8 @@ execute: | # restore the snapshot for root user and check the files snap restore "$SET_ID_BOTH" --users=root - test -e /root/snap/test-snapd-tools/current/canary.txt - test -e /root/snap/test-snapd-tools/common/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/common/canary.txt test ! -e /home/test/snap/test-snapd-tools/current/canary.txt test ! -e /home/test/snap/test-snapd-tools/common/canary.txt test ! -d /home/test/snap/test-snapd-tools/current/canary @@ -97,9 +97,9 @@ execute: | snap restore "$SET_ID_NONE" --users=test test ! -e /root/snap/test-snapd-tools/current/canary.txt test ! -e /root/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary.txt - test -e /home/test/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/common/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary/canary.txt # remove the canaries for both users rm -f /root/snap/test-snapd-tools/{current,common}/canary.txt @@ -107,15 +107,11 @@ execute: | # restore the snapshot for both user and check the files snap restore "$SET_ID_NONE" --users=test,root - test -e /root/snap/test-snapd-tools/current/canary.txt - test -e /root/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary.txt - test -e /home/test/snap/test-snapd-tools/common/canary.txt - test -e /home/test/snap/test-snapd-tools/current/canary/canary.txt - - # check files content - MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /root/snap/test-snapd-tools/common/canary.txt MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/common/canary.txt + MATCH "content updated" < /home/test/snap/test-snapd-tools/current/canary/canary.txt # check removal works snap forget "$SET_ID_NONE" From 3470990cf45e1f1616ed243bee2d4501a6e07733 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 15 Nov 2018 13:43:10 +0100 Subject: [PATCH 067/580] cmd/snap-confine: nvidia: pick up libnvidia-opencl.so Pick up Nvidia's OpenCL implementation from the host fs. Signed-off-by: Maciej Borzecki --- cmd/snap-confine/mount-support-nvidia.c | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/snap-confine/mount-support-nvidia.c b/cmd/snap-confine/mount-support-nvidia.c index a3be1adface..d53d5fe0dc4 100644 --- a/cmd/snap-confine/mount-support-nvidia.c +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -111,6 +111,7 @@ static const char *nvidia_globs[] = { "libnvidia-glvkspirv.so*", "libnvidia-ifr.so*", "libnvidia-ml.so*", + "libnvidia-opencl.so*", "libnvidia-ptxjitcompiler.so*", "libnvidia-tls.so*", "tls/libnvidia-tls.so*", From e49231de0b8c64f5b85225fe597c8dd67b369380 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 15 Nov 2018 13:43:42 +0100 Subject: [PATCH 068/580] interfaces/builtin/opengl: allow reading /etc/OpenCL/vendors /etc/OpenCL vendors contains vendor specific ICD files. Make sure that snap can read it to assemble a desired set of OpenCL vendor implementations. Signed-off-by: Maciej Borzecki --- interfaces/builtin/opengl.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interfaces/builtin/opengl.go b/interfaces/builtin/opengl.go index 7358a30150b..435b96dde69 100644 --- a/interfaces/builtin/opengl.go +++ b/interfaces/builtin/opengl.go @@ -79,6 +79,9 @@ unix (send, receive) type=dgram peer=(addr="@nvidia[0-9a-f]*"), unix (bind,listen) type=seqpacket addr="@cuda-uvmfd-[0-9a-f]*", /{dev,run}/shm/cuda.* rw, +# OpenCL ICD files +/etc/OpenCL/vendors/ r, +/etc/OpenCL/vendors/** r, # Parallels guest tools 3D acceleration (video toolgate) @{PROC}/driver/prl_vtg rw, From d1eb7efa2b94430a4dcc9ad1c821cb8f078c1142 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 15 Nov 2018 12:58:21 +0100 Subject: [PATCH 069/580] snap-update-ns: fix trailing slash bug on trespassing error The trespassing detector is handled unclean paths, belonging to subsequently growing substring of the constructed path. Such paths can easily end with a trailing slash. Fixes: https://bugs.launchpad.net/snapd/+bug/1803535 Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/trespassing.go | 2 +- cmd/snap-update-ns/utils_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/snap-update-ns/trespassing.go b/cmd/snap-update-ns/trespassing.go index 1169a2bfa65..f72adbe8248 100644 --- a/cmd/snap-update-ns/trespassing.go +++ b/cmd/snap-update-ns/trespassing.go @@ -178,7 +178,7 @@ func (rs *Restrictions) Check(dirFd int, dirName string) error { // kind of base snap. return fmt.Errorf("cannot recover from trespassing over /") } - return &TrespassingError{ViolatedPath: dirName, DesiredPath: rs.desiredPath} + return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} } // Lift lifts write restrictions for the desired path. diff --git a/cmd/snap-update-ns/utils_test.go b/cmd/snap-update-ns/utils_test.go index 96d9fb10b60..03cacd92090 100644 --- a/cmd/snap-update-ns/utils_test.go +++ b/cmd/snap-update-ns/utils_test.go @@ -741,8 +741,8 @@ func (s *utilsSuite) TestSecureMksymlinkAllDeepInEtc(c *C) { s.sys.InsertFault(`mkdirat 3 "etc" 0755`, syscall.EEXIST) rs := s.as.RestrictionsFor("/etc/some/other/stuff/symlink") err := update.MksymlinkAll("/etc/some/other/stuff/symlink", 0755, 0, 0, "/oldname", rs) - c.Assert(err, ErrorMatches, `cannot write to "/etc/some/other/stuff/symlink" because it would affect the host in "/etc/"`) - c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc/") + c.Assert(err, ErrorMatches, `cannot write to "/etc/some/other/stuff/symlink" because it would affect the host in "/etc"`) + c.Assert(err.(*update.TrespassingError).ViolatedPath, Equals, "/etc") c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3}, {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}}, From f4cb176725f9d9e2b9d1268242508f37a2e13a62 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 15 Nov 2018 13:05:50 +0100 Subject: [PATCH 070/580] tests: add regression test for LP: #1803535 Signed-off-by: Zygmunt Krynicki --- tests/lib/snaps/test-snapd-lp-1803535/bin/sh | 2 ++ .../test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd | 1 + tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml | 9 +++++++++ tests/regression/lp-1803535/task.yaml | 8 ++++++++ 4 files changed, 20 insertions(+) create mode 100755 tests/lib/snaps/test-snapd-lp-1803535/bin/sh create mode 100644 tests/lib/snaps/test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd create mode 100644 tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml create mode 100644 tests/regression/lp-1803535/task.yaml diff --git a/tests/lib/snaps/test-snapd-lp-1803535/bin/sh b/tests/lib/snaps/test-snapd-lp-1803535/bin/sh new file mode 100755 index 00000000000..d32c6d89945 --- /dev/null +++ b/tests/lib/snaps/test-snapd-lp-1803535/bin/sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec /bin/sh "$@" diff --git a/tests/lib/snaps/test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd b/tests/lib/snaps/test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd new file mode 100644 index 00000000000..be1bd41848a --- /dev/null +++ b/tests/lib/snaps/test-snapd-lp-1803535/etc/OpenCL/vendors/foo.icd @@ -0,0 +1 @@ +canary diff --git a/tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml b/tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml new file mode 100644 index 00000000000..b5c90508484 --- /dev/null +++ b/tests/lib/snaps/test-snapd-lp-1803535/meta/snap.yaml @@ -0,0 +1,9 @@ +name: test-snapd-lp-1803535 +version: 1.0 +summary: Regression test for https://bugs.launchpad.net/snapd/+bug/1803535 +apps: + sh: + command: bin/sh +layout: + /etc/OpenCL/vendors/foo.icd: + bind-file: $SNAP/etc/OpenCL/vendors/foo.icd diff --git a/tests/regression/lp-1803535/task.yaml b/tests/regression/lp-1803535/task.yaml new file mode 100644 index 00000000000..3a31c8c029b --- /dev/null +++ b/tests/regression/lp-1803535/task.yaml @@ -0,0 +1,8 @@ +summary: regression test for https://bugs.launchpad.net/snapd/+bug/1803535 +prepare: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + install_local test-snapd-lp-1803535 +execute: | + # If we can construct the layout and execute /bin/true we are fine. + test-snapd-lp-1803535.sh -c /bin/true From d180639cf155905c26ff2d62c926aef6ddf7033e Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 15 Nov 2018 18:07:35 +0100 Subject: [PATCH 071/580] cmd/snap: add nanosleep to blacklisted syscalls when running with --strace (#6155) * cmd/snap: add nanosleep to blacklisted syscalls when running with --strace Strace was observed to block on nanosleep when using `snap run --strace` with an app from snap built using core18 base on a 4.19.1 kernel host. This is how it looked like: $ snap run --strace test-snapd-tools-core18.echo foo [pid 4859] execve("/snap/test-snapd-tools-core18/2/bin/echo", ["/snap/test-snapd-tools-core18/2/"..., "foo"], 0xc0000c2700 /* 54 vars */ [pid 4861] <... nanosleep resumed> NULL) = 0 Blacklisting nanosleep() made it unblock. Signed-off-by: Maciej Borzecki * snap: run `go fmt` on cmd_run_test.go with go1.10 --- cmd/snap/cmd_run.go | 2 +- cmd/snap/cmd_run_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 2501eac76ab..0e0ff0a4f8f 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -684,7 +684,7 @@ func straceCmd() ([]string, error) { "-f", // these syscalls are excluded because they make strace hang // on all or some architectures (gettimeofday on arm64) - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", }, nil } diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 13a32d7e604..1f0fe2b80df 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -738,7 +738,7 @@ echo "stdout output 2" filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -761,7 +761,7 @@ echo "stdout output 2" filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -808,7 +808,7 @@ func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) { filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday", + "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", "-tt", "-o", "file with spaces", From c2576f0e54a0daf65ced50ffa6979aaa5ef0ec47 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 9 Nov 2018 12:43:30 +0100 Subject: [PATCH 072/580] cmd/snap-confine,snap-update-ns: discard quirks Recent discussion about LXD has resurrected the topic of quirks. Quirks were created in response to a bug where a snap in devmode being unable to interact with with LXD running as a classic package. At the time it was a big deal so we implemented the quirk and everything was fine. Time passed and LXD moved away from storing the socket in that place. It also moved away from shipping as a classic package as well. Since we never allowed access to /var/lib/lxd via any interfaces and it is unlikely anyone still relies on this quirk I think it should be dropped now. Removing should speed up execution of all snaps by a small amount, since we don't need to construct a writable mimic in /var/lib anymore. We also shrink snap-confine by removing a good chunk of C code. https://bugs.launchpad.net/snap-confine/+bug/1613845 Signed-off-by: Zygmunt Krynicki --- cmd/Makefile.am | 4 - cmd/snap-confine/README.mount_namespace | 46 ----- cmd/snap-confine/mount-support.c | 5 - cmd/snap-confine/mount-support.h | 1 - cmd/snap-confine/quirks.c | 230 ---------------------- cmd/snap-confine/quirks.h | 30 --- cmd/snap-confine/snap-confine.apparmor.in | 28 --- cmd/snap-confine/snap-confine.c | 1 - cmd/snap-confine/snap-confine.rst | 12 -- cmd/snap-update-ns/trespassing.go | 7 +- cmd/snap-update-ns/trespassing_test.go | 10 - tests/regression/lp-1613845/task.yaml | 28 --- 12 files changed, 1 insertion(+), 401 deletions(-) delete mode 100644 cmd/snap-confine/quirks.c delete mode 100644 cmd/snap-confine/quirks.h delete mode 100644 tests/regression/lp-1613845/task.yaml diff --git a/cmd/Makefile.am b/cmd/Makefile.am index 18a969a5498..ebe98c4f07e 100644 --- a/cmd/Makefile.am +++ b/cmd/Makefile.am @@ -211,8 +211,6 @@ snap_confine_snap_confine_SOURCES = \ snap-confine/mount-support.h \ snap-confine/ns-support.c \ snap-confine/ns-support.h \ - snap-confine/quirks.c \ - snap-confine/quirks.h \ snap-confine/snap-confine-args.c \ snap-confine/snap-confine-args.h \ snap-confine/snap-confine.c \ @@ -302,8 +300,6 @@ snap_confine_unit_tests_SOURCES = \ snap-confine/cookie-support-test.c \ snap-confine/mount-support-test.c \ snap-confine/ns-support-test.c \ - snap-confine/quirks.c \ - snap-confine/quirks.h \ snap-confine/snap-confine-args-test.c \ snap-confine/snap-device-helper-test.c snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS) diff --git a/cmd/snap-confine/README.mount_namespace b/cmd/snap-confine/README.mount_namespace index a44e145d303..fecf87cbd09 100644 --- a/cmd/snap-confine/README.mount_namespace +++ b/cmd/snap-confine/README.mount_namespace @@ -80,52 +80,6 @@ mount("none", "/tmp", NULL, MS_PRIVATE, NULL) = 0 mount("devpts", "/dev/pts", "devpts", MS_MGC_VAL, "newinstance,ptmxmode=0666,mode=0"...) mount("/dev/pts/ptmx", "/dev/ptmx", 0x5574dfe9a5c3, MS_BIND, NULL) -# Classic only - process quirks mounts by: -# creating temporary quirks directory for moving /var/lib/snapd aside -mkdir("/tmp/snapd.quirks_xKIzG3", 0700) = 0 -# moving /var/lib/snapd aside -mount("/var/lib/snapd", "/tmp/snapd.quirks_xKIzG3", NULL, MS_MOVE, NULL) = 0 -# creating a tmpfs on /var/lib for our mount points -mount("none", "/var/lib", "tmpfs", MS_NOSUID|MS_NODEV, NULL) = 0 -# mimicking the vanilla /var/lib/* from the core snap in /var/lib in tmpfs -# (the directories to mimic are dynamically determined and will vary as the -# core snap changes. Syscalls for finding what to mount and creating the -# mount points are omitted) -mount("/snap/ubuntu-core/current/var/lib/apparmor", "/var/lib/apparmor", NULL, MS_RDONLY|MS_NOSUID|MS_NODEV|MS_BIND|MS_REC|MS_SLAVE, NULL) = 0 -mount("/snap/ubuntu-core/current/var/lib/classic", "/var/lib/classic", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/console-conf", "/var/lib/console-conf", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/dbus", "/var/lib/dbus", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/dhcp", "/var/lib/dhcp", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/extrausers", "/var/lib/extrausers", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/initramfs-tools", "/var/lib/initramfs-tools", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/initscripts", "/var/lib/initscripts", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/insserv", "/var/lib/insserv", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/logrotate", "/var/lib/logrotate", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/machines", "/var/lib/machines", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/misc", "/var/lib/misc", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/pam", "/var/lib/pam", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/python", "/var/lib/python", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/resolvconf", "/var/lib/resolvconf", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/snapd", "/var/lib/snapd", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/sudo", "/var/lib/sudo", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/systemd", "/var/lib/systemd", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/ubuntu-fan", "/var/lib/ubuntu-fan", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/ucf", "/var/lib/ucf", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/update-rc.d", "/var/lib/update-rc.d", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/urandom", "/var/lib/urandom", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/vim", "/var/lib/vim", ...) = 0 -mount("/snap/ubuntu-core/current/var/lib/waagent", "/var/lib/waagent", ...) = 0 -# unmounting the /var/lib/snapd that was just mimicked -umount2("/var/lib/snapd", 0) -# moving back the /var/lib/snapd that was set aside -mount("/tmp/snapd.quirks_xKIzG3", "/var/lib/snapd", NULL, MS_MOVE, NULL) = 0 -# cleaning up the temporary directory -rmdir("/tmp/snapd.quirks_xKIzG3") = 0 -# applying the actual quirk mounts as needed (for now, lxd, but more may -# come). Eg: -mount("/var/lib/snapd/hostfs/var/lib/lxd", "/var/lib/lxd", NULL, MS_REC|MS_SLAVE|MS_NODEV|MS_NOSUID|MS_NOEXEC) = 0 -# End quirk mounts on classic - # Process snap-defined mounts (eg, for content interface, mount the source to # the target as defined in /var/lib/snapd/mount/snap...fstab) # Eg: diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 5098fe9c1d3..9f9c4bb0cb2 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -44,7 +44,6 @@ #include "../libsnap-confine-private/utils.h" #include "mount-support-nvidia.h" #include "apparmor-support.h" -#include "quirks.h" #define MAX_BUF 1000 @@ -702,10 +701,6 @@ void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd, // TODO: fold this into bootstrap setup_private_pts(); - // setup quirks for specific snaps *only* for the old core snap - if (distro == SC_DISTRO_CLASSIC && sc_streq(base_snap_name, "core")) { - sc_setup_quirks(); - } // setup the security backend bind mounts sc_setup_mount_profiles(apparmor, snap_update_ns_fd, snap_name); diff --git a/cmd/snap-confine/mount-support.h b/cmd/snap-confine/mount-support.h index 680efb315db..4f9b11e2c8b 100644 --- a/cmd/snap-confine/mount-support.h +++ b/cmd/snap-confine/mount-support.h @@ -36,7 +36,6 @@ int sc_open_snap_update_ns(void); * - prepares and chroots into the core snap (on classic systems) * - creates private /tmp * - creates private /dev/pts - * - applies quirks for specific snaps (like LXD) * - processes mount profiles * * The function will also try to preserve the current working directory but if diff --git a/cmd/snap-confine/quirks.c b/cmd/snap-confine/quirks.c deleted file mode 100644 index a7df77826a2..00000000000 --- a/cmd/snap-confine/quirks.c +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2016 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 . - * - */ -#include "config.h" -#include "quirks.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "../libsnap-confine-private/classic.h" -#include "../libsnap-confine-private/cleanup-funcs.h" -#include "../libsnap-confine-private/mount-opt.h" -#include "../libsnap-confine-private/string-utils.h" -#include "../libsnap-confine-private/utils.h" -// XXX: for smaller patch, this should be in utils.h later -#include "user-support.h" - -/** - * Get the path to the mounted core snap in the execution environment. - * - * The core snap may be named just "core" (preferred) or "ubuntu-core" - * (legacy). The mount point does not depend on build-time configuration and - * does not differ from distribution to distribution. - **/ -static const char *sc_get_inner_core_mount_point(void) -{ - const char *core_path = "/snap/core/current/"; - const char *ubuntu_core_path = "/snap/ubuntu-core/current/"; - static const char *result = NULL; - if (result == NULL) { - if (access(core_path, F_OK) == 0) { - // Use the "core" snap if available. - result = core_path; - } else if (access(ubuntu_core_path, F_OK) == 0) { - // If not try to fall back to the "ubuntu-core" snap. - result = ubuntu_core_path; - } else { - die("cannot locate the core snap"); - } - } - return result; -} - -/** - * Mount a tmpfs at a given directory. - * - * The empty tmpfs is used as a substrate to create additional directories and - * then bind mounts to other destinations. - * - * It is useful to poke unexpected holes in the read-only core snap. - **/ -static void sc_quirk_setup_tmpfs(const char *dirname) -{ - debug("mounting tmpfs at %s", dirname); - if (mount("none", dirname, "tmpfs", MS_NODEV | MS_NOSUID, NULL) != 0) { - die("cannot mount tmpfs at %s", dirname); - }; -} - -/** - * Create an empty directory and bind mount something there. - * - * The empty directory is created at destdir. The bind mount is - * done from srcdir to destdir. The bind mount is performed with - * caller-defined flags. - **/ -static void sc_quirk_mkdir_bind(const char *src_dir, const char *dest_dir, - unsigned flags) -{ - flags |= MS_BIND; - debug("creating empty directory at %s", dest_dir); - if (sc_nonfatal_mkpath(dest_dir, 0755) < 0) { - die("cannot create empty directory at %s", dest_dir); - } - char buf[1000] = { 0 }; - const char *flags_str = sc_mount_opt2str(buf, sizeof buf, flags); - debug("performing operation: mount %s %s -o %s", src_dir, dest_dir, - flags_str); - if (mount(src_dir, dest_dir, NULL, flags, NULL) != 0) { - die("cannot perform operation: mount %s %s -o %s", src_dir, - dest_dir, flags_str); - } -} - -/** - * Create a writable mimic directory based on reference directory. - * - * The mimic directory is a tmpfs populated with bind mounts to the (possibly - * read only) directories in the reference directory. While all the read-only - * content stays read-only the actual mimic directory is writable so additional - * content can be placed there. - * - * Flags are forwarded to sc_quirk_mkdir_bind() - **/ -static void sc_quirk_create_writable_mimic(const char *mimic_dir, - const char *ref_dir, unsigned flags) -{ - debug("creating writable mimic directory %s based on %s", mimic_dir, - ref_dir); - sc_quirk_setup_tmpfs(mimic_dir); - - // Now copy the ownership and permissions of the mimicked directory - struct stat stat_buf; - if (stat(ref_dir, &stat_buf) < 0) { - die("cannot stat %s", ref_dir); - } - if (chown(mimic_dir, stat_buf.st_uid, stat_buf.st_gid) < 0) { - die("cannot chown for %s", mimic_dir); - } - if (chmod(mimic_dir, stat_buf.st_mode) < 0) { - die("cannot chmod for %s", mimic_dir); - } - - debug("bind-mounting all the files from the reference directory"); - DIR *dirp SC_CLEANUP(sc_cleanup_closedir) = NULL; - dirp = opendir(ref_dir); - if (dirp == NULL) { - die("cannot open reference directory %s", ref_dir); - } - struct dirent *entryp = NULL; - do { - char src_name[PATH_MAX * 2] = { 0 }; - char dest_name[PATH_MAX * 2] = { 0 }; - // Set errno to zero, if readdir fails it will not only return null but - // set errno to a non-zero value. This is how we can differentiate - // end-of-directory from an actual error. - errno = 0; - entryp = readdir(dirp); - if (entryp == NULL && errno != 0) { - die("cannot read another directory entry"); - } - if (entryp == NULL) { - break; - } - if (strcmp(entryp->d_name, ".") == 0 - || strcmp(entryp->d_name, "..") == 0) { - continue; - } - if (entryp->d_type != DT_DIR && entryp->d_type != DT_REG) { - die("unsupported entry type of file %s (%d)", - entryp->d_name, entryp->d_type); - } - sc_must_snprintf(src_name, sizeof src_name, "%s/%s", ref_dir, - entryp->d_name); - sc_must_snprintf(dest_name, sizeof dest_name, "%s/%s", - mimic_dir, entryp->d_name); - sc_quirk_mkdir_bind(src_name, dest_name, flags); - } while (entryp != NULL); -} - -/** - * Setup a quirk for LXD. - * - * An existing LXD snap relies on pre-chroot behavior to access /var/lib/lxd - * while in devmode. Since that directory doesn't exist in the core snap the - * quirk punches a custom hole so that this directory shows the hostfs content - * if such directory exists on the host. - * - * See: https://bugs.launchpad.net/snap-confine/+bug/1613845 - **/ -static void sc_setup_lxd_quirk(void) -{ - const char *hostfs_lxd_dir = SC_HOSTFS_DIR "/var/lib/lxd"; - if (access(hostfs_lxd_dir, F_OK) == 0) { - const char *lxd_dir = "/var/lib/lxd"; - debug("setting up quirk for LXD (see LP: #1613845)"); - sc_quirk_mkdir_bind(hostfs_lxd_dir, lxd_dir, - MS_REC | MS_SLAVE | MS_NODEV | MS_NOSUID | - MS_NOEXEC); - } -} - -void sc_setup_quirks(void) -{ - // because /var/lib/snapd is essential let's move it to /tmp/snapd for a sec - char snapd_tmp[] = "/tmp/snapd.quirks_XXXXXX"; - if (mkdtemp(snapd_tmp) == 0) { - die("cannot create temporary directory for /var/lib/snapd mount point"); - } - debug("performing operation: mount --move %s %s", "/var/lib/snapd", - snapd_tmp); - if (mount("/var/lib/snapd", snapd_tmp, NULL, MS_MOVE, NULL) - != 0) { - die("cannot perform operation: mount --move %s %s", - "/var/lib/snapd", snapd_tmp); - } - // now let's make /var/lib the vanilla /var/lib from the core snap - char buf[PATH_MAX] = { 0 }; - sc_must_snprintf(buf, sizeof buf, "%s/var/lib", - sc_get_inner_core_mount_point()); - sc_quirk_create_writable_mimic("/var/lib", buf, - MS_RDONLY | MS_REC | MS_SLAVE | MS_NODEV - | MS_NOSUID); - // now let's move /var/lib/snapd (that was originally there) back - debug("performing operation: umount %s", "/var/lib/snapd"); - if (umount("/var/lib/snapd") != 0) { - die("cannot perform operation: umount %s", "/var/lib/snapd"); - } - debug("performing operation: mount --move %s %s", snapd_tmp, - "/var/lib/snapd"); - if (mount(snapd_tmp, "/var/lib/snapd", NULL, MS_MOVE, NULL) - != 0) { - die("cannot perform operation: mount --move %s %s", snapd_tmp, - "/var/lib/snapd"); - } - debug("performing operation: rmdir %s", snapd_tmp); - if (rmdir(snapd_tmp) != 0) { - die("cannot perform operation: rmdir %s", snapd_tmp); - } - // We are now ready to apply any quirks that relate to /var/lib - sc_setup_lxd_quirk(); -} diff --git a/cmd/snap-confine/quirks.h b/cmd/snap-confine/quirks.h deleted file mode 100644 index f707423bc8e..00000000000 --- a/cmd/snap-confine/quirks.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2016 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 . - * - */ - -#ifndef SNAP_QUIRKS_H -#define SNAP_QUIRKS_H - -/** - * Setup various quirks that have to exists for now. - * - * This function applies non-standard tweaks that are required - * because of requirement to stay compatible with certain snaps - * that were tested with pre-chroot layout. - **/ -void sc_setup_quirks(void); - -#endif diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index 2a60c2d1ef0..be34482bdf9 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -119,9 +119,6 @@ # reading seccomp filters /{tmp/snap.rootfs_*/,}var/lib/snapd/seccomp/bpf/*.bin r, - # ensuring correct permissions in sc_quirk_create_writable_mimic - /{tmp/snap.rootfs_*/,}var/lib/ rw, - # LP: #1668659 mount options=(rw rbind) /snap/ -> /snap/, mount options=(rw rshared) -> /snap/, @@ -341,31 +338,6 @@ # See LP: #1735459 for details. umount /, - # Support for the quirk system - /var/ r, - /var/lib/ r, - /var/lib/** rw, - /tmp/ r, - /tmp/snapd.quirks_*/ rw, - mount options=(move) /var/lib/snapd/ -> /tmp/snapd.quirks_*/, - mount fstype=tmpfs options=(rw nodev nosuid) none -> /var/lib/, - mount options=(ro rbind) /snap/{,ubuntu-}core/*/var/lib/** -> /var/lib/**, - umount /var/lib/snapd/, - mount options=(move) /tmp/snapd.quirks_*/ -> /var/lib/snapd/, - # On classic systems with a setuid root snap-confine when run by non-root - # user, the mimic_dir is created with the gid of the calling user (ie, - # not '0') so when setting the permissions (chmod) of the mimicked - # directory to that of the reference directory, a CAP_FSETID is triggered. - # snap-confine sets the directory up correctly, so simply silence the - # denial since we don't want to grant the capability as a whole to - # snap-confine. - deny capability fsetid, - - # support for the LXD quirk - mount options=(rw rbind nodev nosuid noexec) /var/lib/snapd/hostfs/var/lib/lxd/ -> /var/lib/lxd/, - /var/lib/lxd/ w, - /var/lib/snapd/hostfs/var/lib/lxd r, - # support for locking /run/snapd/lock/ rw, /run/snapd/lock/*.lock rwk, diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c index a9100c93be9..4d608d5a244 100644 --- a/cmd/snap-confine/snap-confine.c +++ b/cmd/snap-confine/snap-confine.c @@ -39,7 +39,6 @@ #include "apparmor-support.h" #include "mount-support.h" #include "ns-support.h" -#include "quirks.h" #include "udev-support.h" #include "user-support.h" #include "cookie-support.h" diff --git a/cmd/snap-confine/snap-confine.rst b/cmd/snap-confine/snap-confine.rst index ba62c0852dc..3141ef86289 100644 --- a/cmd/snap-confine/snap-confine.rst +++ b/cmd/snap-confine/snap-confine.rst @@ -92,18 +92,6 @@ options prefixed with `x-snapd-`. As a security precaution only `bind` mounts are supported at this time. -Quirks ------- - -`snap-confine` contains a quirk system that emulates some or the behavior of -the older versions of snap-confine that certain snaps (still in devmode but -useful and important) have grown to rely on. This section documents the list of -quirks: - -- The `/var/lib/lxd` directory, if it exists on the host, is made available in - the execution environment. This allows various snaps, while running in - devmode, to access the LXD socket. LP: #1613845 - Sharing of the mount namespace ------------------------------ diff --git a/cmd/snap-update-ns/trespassing.go b/cmd/snap-update-ns/trespassing.go index f72adbe8248..73670a44c37 100644 --- a/cmd/snap-update-ns/trespassing.go +++ b/cmd/snap-update-ns/trespassing.go @@ -244,10 +244,5 @@ func isPrivateTmpfsCreatedBySnapd(dirName string, fsData *syscall.Statfs_t, file return change.Action == Mount } } - // TODO: As a special exception, assume that a tmpfs over /var/lib is - // trusted. This tmpfs is created by snap-confine as a "quirk" to support - // a particular behavior of LXD. Once the quirk is migrated to a mount - // profile (or removed entirely if no longer necessary) the following code - // fragment can go away. - return dirName == "/var/lib" + return false } diff --git a/cmd/snap-update-ns/trespassing_test.go b/cmd/snap-update-ns/trespassing_test.go index 4208ce90e6f..c2f506380b5 100644 --- a/cmd/snap-update-ns/trespassing_test.go +++ b/cmd/snap-update-ns/trespassing_test.go @@ -426,13 +426,3 @@ func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdDeeper(c *C) { }) c.Assert(result, Equals, false) } - -func (s *trespassingSuite) TestIsPrivateTmpfsCreatedBySnapdViaVarLib(c *C) { - path := "/var/lib" - // A tmpfs in /var/lib is private because it is a special - // quirk applied by snap-confine, without having a change record. - statfs := &syscall.Statfs_t{Type: update.TmpfsMagic} - stat := &syscall.Stat_t{} - result := update.IsPrivateTmpfsCreatedBySnapd(path, statfs, stat, nil) - c.Assert(result, Equals, true) -} diff --git a/tests/regression/lp-1613845/task.yaml b/tests/regression/lp-1613845/task.yaml deleted file mode 100644 index deb63d1e8cf..00000000000 --- a/tests/regression/lp-1613845/task.yaml +++ /dev/null @@ -1,28 +0,0 @@ -summary: Check that /var/lib/lxd is bind mounted to the real thing if one exists - -details: | - After switching to the chroot-based snap-confine the LXD snap stopped - working (even in devmode) because it relied on access to /var/lib/lxd from - the host filesystem. While this would never work in an all-snap image it is - still important to ensure that it works in classic devmode environment. - -# This test only applies to classic systems -systems: [-ubuntu-core-*, -fedora-*, -arch-*] - -prepare: | - echo "Having installed the test snap in devmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-tools - echo "Having created a canary file in /var/lib/lxd" - mkdir -p /var/lib/lxd - echo "test" > /var/lib/lxd/canary - -execute: | - echo "We can see the canary file in /var/lib/lxd" - [ "$(test-snapd-tools.cmd cat /var/lib/lxd/canary)" = "test" ] - -restore: | - rm -f /var/lib/lxd/canary - # When the host already has LXD installed we cannot just remove this - rmdir /var/lib/lxd || : From 811f7d05e689850caae810fb4b31a1acf519a4be Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 9 Nov 2018 13:46:51 +0100 Subject: [PATCH 073/580] cmd/snap-confine: allow creating hostfs Signed-off-by: Zygmunt Krynicki --- cmd/snap-confine/snap-confine.apparmor.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index be34482bdf9..c8ca437c480 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -232,6 +232,9 @@ # Allow reading the os-release file (possibly a symlink to /usr/lib). /{etc/,usr/lib/}os-release r, + # Allow creating /var/lib/snapd/hostfs, if missing + /var/lib/snapd/hostfs/ rw, + # set up snap-specific private /tmp dir capability chown, /tmp/ w, From b104aa1c8714e7fac4cbe4ddff85798761fe6755 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 14 Nov 2018 13:13:41 +0100 Subject: [PATCH 074/580] cmd/snap-confine: allow reading snap cookies Signed-off-by: Zygmunt Krynicki --- cmd/snap-confine/snap-confine.apparmor.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index c8ca437c480..821d51db706 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -373,6 +373,9 @@ # https://forum.snapcraft.io/t/snapd-2-27-6-2-in-debian-sid-blocked-on-apparmor-in-kernel-4-13-0-1/2813/3 ptrace (trace, tracedby) peer=@LIBEXECDIR@/snap-confine, + # Allow reading snap cookies. + /var/lib/snapd/cookie/snap.* r, + # For aa_change_hat() to go into ^mount-namespace-capture-helper @{PROC}/[0-9]*/attr/current w, From e420add42ff4752dd52456a3ac062a40df519176 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 14 Nov 2018 15:44:56 +0100 Subject: [PATCH 075/580] cmd/snap-confine: allow writing to /tmp Signed-off-by: Zygmunt Krynicki --- cmd/snap-confine/snap-confine.apparmor.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/snap-confine/snap-confine.apparmor.in b/cmd/snap-confine/snap-confine.apparmor.in index 821d51db706..9b6bd5e06c2 100644 --- a/cmd/snap-confine/snap-confine.apparmor.in +++ b/cmd/snap-confine/snap-confine.apparmor.in @@ -237,7 +237,7 @@ # set up snap-specific private /tmp dir capability chown, - /tmp/ w, + /tmp/ rw, /tmp/snap.*/ w, /tmp/snap.*/tmp/ w, mount options=(rw private) -> /tmp/, From a3c952a5731f6370d356dc867bf7f01403dbbe44 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Tue, 13 Nov 2018 12:24:26 +0000 Subject: [PATCH 076/580] overlord/snapstate, store: always send epochs Without this change, epochs weren't being sent to the store, which meant the store couldn't know to tell us about snaps with non-default epochs (so we'd never do an epoched upgrade). This changes that. --- overlord/snapstate/backend_test.go | 1 + overlord/snapstate/snapstate_test.go | 96 +++++++++++++++++-- overlord/snapstate/storehelpers.go | 1 + store/store.go | 36 +++++-- store/store_test.go | 134 +++++++++++++++++++++++++++ 5 files changed, 250 insertions(+), 18 deletions(-) diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 71a079baf50..81f1ede471c 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -674,6 +674,7 @@ func (f *fakeSnappyBackend) ReadInfo(name string, si *snap.SideInfo) (*snap.Info SideInfo: *si, Architectures: []string{"all"}, Type: snap.TypeApp, + Epoch: snap.E("1*"), } if strings.Contains(snapName, "alias-snap") { // only for the switch below diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index f2a4a802bbb..3c05e8144cb 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -2803,6 +2803,7 @@ func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { Revision: snap.R(7), TrackingChannel: "stable", RefreshedDate: refreshedDate, + Epoch: snap.E("0"), }}, userID: 1, }, @@ -3020,6 +3021,7 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateRunThrough(c *C) { Revision: snap.R(7), TrackingChannel: "stable", RefreshedDate: refreshedDate, + Epoch: snap.E("0"), }}, userID: 1, }, @@ -3502,9 +3504,27 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsNoUserRunThrough(c *C) { case "storesvc-snap-action": ir++ c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{ - {InstanceName: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, - {InstanceName: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)}, - {InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)}, + { + InstanceName: "core", + SnapID: "core-snap-id", + Revision: snap.R(1), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1), + Epoch: snap.E("1*"), + }, + { + InstanceName: "services-snap", + SnapID: "services-snap-id", + Revision: snap.R(2), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2), + Epoch: snap.E("0"), + }, + { + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(5), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5), + Epoch: snap.E("1*"), + }, }) case "storesvc-snap-action:action": snapID := op.action.SnapID @@ -3596,9 +3616,27 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserRunThrough(c *C) { case "storesvc-snap-action": ir++ c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{ - {InstanceName: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, - {InstanceName: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)}, - {InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)}, + { + InstanceName: "core", + SnapID: "core-snap-id", + Revision: snap.R(1), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1), + Epoch: snap.E("1*"), + }, + { + InstanceName: "services-snap", + SnapID: "services-snap-id", + Revision: snap.R(2), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2), + Epoch: snap.E("0"), + }, + { + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(5), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5), + Epoch: snap.E("1*"), + }, }) case "storesvc-snap-action:action": snapID := op.action.SnapID @@ -3701,9 +3739,27 @@ func (s *snapmgrTestSuite) TestUpdateManyMultipleCredsUserWithNoStoreAuthRunThro case "storesvc-snap-action": ir++ c.Check(op.curSnaps, DeepEquals, []store.CurrentSnap{ - {InstanceName: "core", SnapID: "core-snap-id", Revision: snap.R(1), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, - {InstanceName: "services-snap", SnapID: "services-snap-id", Revision: snap.R(2), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2)}, - {InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5)}, + { + InstanceName: "core", + SnapID: "core-snap-id", + Revision: snap.R(1), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1), + Epoch: snap.E("1*"), + }, + { + InstanceName: "services-snap", + SnapID: "services-snap-id", + Revision: snap.R(2), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 2), + Epoch: snap.E("0"), + }, + { + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(5), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 5), + Epoch: snap.E("1*"), + }, }) case "storesvc-snap-action:action": snapID := op.action.SnapID @@ -3766,6 +3822,7 @@ func (s *snapmgrTestSuite) TestUpdateUndoRunThrough(c *C) { SnapID: "some-snap-id", Revision: snap.R(7), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }, @@ -3947,6 +4004,7 @@ func (s *snapmgrTestSuite) TestUpdateTotalUndoRunThrough(c *C) { Revision: snap.R(7), TrackingChannel: "stable", RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }, @@ -4240,6 +4298,7 @@ func (s *snapmgrTestSuite) TestUpdateSameRevisionSwitchChannelRunThrough(c *C) { Revision: snap.R(7), TrackingChannel: "other-channel", RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }, @@ -4513,6 +4572,7 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) { Revision: snap.R(7), IgnoreValidation: false, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4561,6 +4621,7 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) { TrackingChannel: "stable", IgnoreValidation: true, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 11), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4613,6 +4674,7 @@ func (s *snapmgrTestSuite) TestUpdateIgnoreValidationSticky(c *C) { TrackingChannel: "stable", IgnoreValidation: true, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 12), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4698,12 +4760,14 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateIgnoreValidationSticky(c *C Revision: snap.R(7), IgnoreValidation: false, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }, { InstanceName: "some-snap_instance", SnapID: "some-snap-id", Revision: snap.R(7), IgnoreValidation: false, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4790,6 +4854,7 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateIgnoreValidationSticky(c *C Revision: snap.R(7), IgnoreValidation: false, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }, { InstanceName: "some-snap_instance", SnapID: "some-snap-id", @@ -4797,6 +4862,7 @@ func (s *snapmgrTestSuite) TestParallelInstanceUpdateIgnoreValidationSticky(c *C TrackingChannel: "stable", IgnoreValidation: true, RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 11), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4939,6 +5005,7 @@ func (s *snapmgrTestSuite) TestSingleUpdateBlockedRevision(c *C) { SnapID: "some-snap-id", Revision: snap.R(7), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -4979,6 +5046,7 @@ func (s *snapmgrTestSuite) TestMultiUpdateBlockedRevision(c *C) { SnapID: "some-snap-id", Revision: snap.R(7), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -5019,6 +5087,7 @@ func (s *snapmgrTestSuite) TestAllUpdateBlockedRevision(c *C) { Revision: snap.R(7), RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), Block: []snap.Revision{snap.R(11)}, + Epoch: snap.E("1*"), }}, userID: 1, }) @@ -10526,7 +10595,14 @@ func (s *snapmgrTestSuite) TestTransitionCoreRunThrough(c *C) { { op: "storesvc-snap-action", curSnaps: []store.CurrentSnap{ - {InstanceName: "ubuntu-core", SnapID: "ubuntu-core-snap-id", Revision: snap.R(1), TrackingChannel: "beta", RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1)}, + { + InstanceName: "ubuntu-core", + SnapID: "ubuntu-core-snap-id", + Revision: snap.R(1), + TrackingChannel: "beta", + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1), + Epoch: snap.E("1*"), + }, }, }, { diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index 9d4dc68d00c..83d00951b61 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -315,6 +315,7 @@ func collectCurrentSnaps(snapStates map[string]*SnapState, consider func(*store. Revision: snapInfo.Revision, RefreshedDate: revisionDate(snapInfo), IgnoreValidation: snapst.IgnoreValidation, + Epoch: snapInfo.Epoch, } curSnaps = append(curSnaps, installed) diff --git a/store/store.go b/store/store.go index 12ab8a9c1fb..f61c1824635 100644 --- a/store/store.go +++ b/store/store.go @@ -1941,6 +1941,7 @@ type CurrentSnap struct { RefreshedDate time.Time IgnoreValidation bool Block []snap.Revision + Epoch snap.Epoch } type currentSnapV2JSON struct { @@ -1948,6 +1949,7 @@ type currentSnapV2JSON struct { InstanceKey string `json:"instance-key"` Revision int `json:"revision"` TrackingChannel string `json:"tracking-channel"` + Epoch snap.Epoch `json:"epoch"` RefreshedDate *time.Time `json:"refreshed-date,omitempty"` IgnoreValidation bool `json:"ignore-validation,omitempty"` } @@ -1966,6 +1968,7 @@ type SnapAction struct { Channel string Revision snap.Revision Flags SnapActionFlags + Epoch snap.Epoch } func isValidAction(action string) bool { @@ -1978,14 +1981,24 @@ func isValidAction(action string) bool { } type snapActionJSON struct { - Action string `json:"action"` - InstanceKey string `json:"instance-key"` - Name string `json:"name,omitempty"` - SnapID string `json:"snap-id,omitempty"` - Channel string `json:"channel,omitempty"` - Revision int `json:"revision,omitempty"` - IgnoreValidation *bool `json:"ignore-validation,omitempty"` -} + Action string `json:"action"` + InstanceKey string `json:"instance-key"` + Name string `json:"name,omitempty"` + SnapID string `json:"snap-id,omitempty"` + Channel string `json:"channel,omitempty"` + Revision int `json:"revision,omitempty"` + Epoch interface{} `json:"epoch,omitempty"` // see note + IgnoreValidation *bool `json:"ignore-validation,omitempty"` +} + +// NOTE about epoch in snapActionJSON: the store needs an epoch (even if null) +// for the "install" and "download" actions, to know the client handles epochs +// at all. "refresh" actions should send nothing, not even null -- the snap in +// the context should have the epoch already. We achieve this by making Epoch +// be an `interface{}` with omitempty, and then setting it to a (possibly nil) +// epoch for install and download. As a nil epoch is not an empty interface{}, +// you'll get the null in the json. +// Play with it: https://play.jsgo.io/b20765a68e5ace2c0befdcde94c872ef78cd5e8f type snapRelease struct { Architecture string `json:"architecture"` @@ -2133,6 +2146,7 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act TrackingChannel: channel, IgnoreValidation: curSnap.IgnoreValidation, RefreshedDate: refreshedDate, + Epoch: curSnap.Epoch, } } @@ -2169,6 +2183,7 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act if !a.Revision.Unset() { a.Channel = "" } + if a.Action == "install" { installNum++ instanceKey = fmt.Sprintf("install-%d", installNum) @@ -2187,6 +2202,11 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act if a.Action != "refresh" { aJSON.Name = snap.InstanceSnap(a.InstanceName) + if a.SnapID != "" { + aJSON.Epoch = &a.Epoch + } else { + aJSON.Epoch = (*snap.Epoch)(nil) + } } aJSON.InstanceKey = instanceKey diff --git a/store/store_test.go b/store/store_test.go index ca1220b89ff..df7ff8f97d6 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -64,6 +64,9 @@ type configTestSuite struct{} var _ = Suite(&configTestSuite{}) +// this is what snap.E("0") looks like when decoded into an interface{} (the /^i/ is for "interface") +var iZeroEpoch = map[string]interface{}{"read": []interface{}{0.}, "write": []interface{}{0.}} + func (suite *configTestSuite) TestSetBaseURL(c *C) { // Sanity check to prove at least one URI changes. cfg := store.DefaultConfig() @@ -4065,6 +4068,7 @@ func (s *storeTestSuite) TestSnapAction(c *C) { "revision": float64(1), "tracking-channel": "beta", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4155,6 +4159,7 @@ func (s *storeTestSuite) TestSnapActionNoResults(c *C) { "revision": float64(1), "tracking-channel": "beta", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 0) io.WriteString(w, `{ @@ -4218,6 +4223,7 @@ func (s *storeTestSuite) TestSnapActionRefreshedDateIsOptional(c *C) { "revision": float64(1), "tracking-channel": "beta", + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 0) io.WriteString(w, `{ @@ -4270,6 +4276,7 @@ func (s *storeTestSuite) TestSnapActionSkipBlocked(c *C) { "revision": float64(1), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4358,6 +4365,7 @@ func (s *storeTestSuite) TestSnapActionSkipCurrent(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4519,6 +4527,7 @@ func (s *storeTestSuite) TestSnapActionIgnoreValidation(c *C) { "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, "ignore-validation": true, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4607,6 +4616,7 @@ func (s *storeTestSuite) TestInstallFallbackChannelIsStable(c *C) { "revision": float64(1), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4700,6 +4710,7 @@ func (s *storeTestSuite) TestSnapActionNonDefaultsHeaders(c *C) { "revision": float64(1), "tracking-channel": "beta", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4794,6 +4805,7 @@ func (s *storeTestSuite) TestSnapActionWithDeltas(c *C) { "revision": float64(1), "tracking-channel": "beta", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4879,6 +4891,7 @@ func (s *storeTestSuite) TestSnapActionOptions(c *C) { "revision": float64(1), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -4984,6 +4997,7 @@ func (s *storeTestSuite) testSnapActionGet(action string, c *C) { "instance-key": action + "-1", "name": "hello-world", "channel": "beta", + "epoch": nil, }) fmt.Fprintf(w, `{ @@ -5037,6 +5051,102 @@ func (s *storeTestSuite) testSnapActionGet(action string, c *C) { // effective-channel c.Assert(results[0].Channel, Equals, "candidate") } + +func (s *storeTestSuite) TestSnapActionInstallAmend(c *C) { + // this is what amend would look like + restore := release.MockOnClassic(false) + defer restore() + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) + + c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") + + // no store ID by default + storeID := r.Header.Get("Snap-Device-Store") + c.Check(storeID, Equals, "") + + c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) + c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture()) + c.Check(r.Header.Get("Snap-Classic"), Equals, "false") + + jsonReq, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + var req struct { + Context []map[string]interface{} `json:"context"` + Actions []map[string]interface{} `json:"actions"` + } + + err = json.Unmarshal(jsonReq, &req) + c.Assert(err, IsNil) + + c.Assert(req.Context, HasLen, 0) + c.Assert(req.Actions, HasLen, 1) + c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ + "action": "install", + "instance-key": "install-1", + "name": "hello-world", + "channel": "beta", + "epoch": map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}}, + "snap-id": helloWorldSnapID, + }) + + fmt.Fprint(w, `{ + "results": [{ + "result": "install", + "instance-key": "install-1", + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "effective-channel": "candidate", + "snap": { + "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", + "name": "hello-world", + "revision": 26, + "version": "6.1", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical" + } + } + }] +}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := store.Config{ + StoreBaseURL: mockServerURL, + } + authContext := &testAuthContext{c: c, device: s.device} + sto := store.New(&cfg, authContext) + + results, err := sto.SnapAction(context.TODO(), nil, + []*store.SnapAction{ + { + Action: "install", + InstanceName: "hello-world", + Channel: "beta", + SnapID: helloWorldSnapID, + Epoch: snap.E("1*"), + }, + }, nil, nil) + c.Assert(err, IsNil) + c.Assert(results, HasLen, 1) + c.Assert(results[0].InstanceName(), Equals, "hello-world") + c.Assert(results[0].Revision, Equals, snap.R(26)) + c.Assert(results[0].Version, Equals, "6.1") + c.Assert(results[0].SnapID, Equals, helloWorldSnapID) + c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) + c.Assert(results[0].Deltas, HasLen, 0) + // effective-channel + c.Assert(results[0].Channel, Equals, "candidate") +} + func (s *storeTestSuite) TestSnapActionDownloadParallelInstanceKey(c *C) { // action here is one of install or download restore := release.MockOnClassic(false) @@ -5112,6 +5222,7 @@ func (s *storeTestSuite) testSnapActionGetWithRevision(action string, c *C) { "instance-key": action + "-1", "name": "hello-world", "revision": float64(28), + "epoch": nil, }) fmt.Fprintf(w, `{ @@ -5188,6 +5299,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailable(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ "snap-id": "snap2-id", @@ -5195,6 +5307,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailable(c *C) { "revision": float64(2), "tracking-channel": "edge", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 4) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -5213,12 +5326,14 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailable(c *C) { "instance-key": "install-1", "name": "foo", "channel": "stable", + "epoch": nil, }) c.Assert(req.Actions[3], DeepEquals, map[string]interface{}{ "action": "download", "instance-key": "download-1", "name": "bar", "revision": 42., + "epoch": nil, }) io.WriteString(w, `{ @@ -5365,6 +5480,7 @@ func (s *storeTestSuite) TestSnapActionSnapNotFound(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 3) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -5378,12 +5494,14 @@ func (s *storeTestSuite) TestSnapActionSnapNotFound(c *C) { "instance-key": "install-1", "name": "foo", "channel": "stable", + "epoch": nil, }) c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ "action": "download", "instance-key": "download-1", "name": "bar", "revision": 42., + "epoch": nil, }) io.WriteString(w, `{ @@ -5486,6 +5604,7 @@ func (s *storeTestSuite) TestSnapActionOtherErrors(c *C) { "instance-key": "install-1", "name": "foo", "channel": "stable", + "epoch": nil, }) io.WriteString(w, `{ @@ -5910,6 +6029,7 @@ func (s *storeTestSuite) TestSnapActionRefreshParallelInstall(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ "snap-id": helloWorldSnapID, @@ -5917,6 +6037,7 @@ func (s *storeTestSuite) TestSnapActionRefreshParallelInstall(c *C) { "revision": float64(2), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6011,6 +6132,7 @@ func (s *storeTestSuite) TestSnapActionRefreshStableInstanceKey(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ "snap-id": helloWorldSnapID, @@ -6018,6 +6140,7 @@ func (s *storeTestSuite) TestSnapActionRefreshStableInstanceKey(c *C) { "revision": float64(2), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6118,6 +6241,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ "snap-id": helloWorldSnapID, @@ -6125,6 +6249,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) "revision": float64(2), "tracking-channel": "edge", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 3) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6142,6 +6267,7 @@ func (s *storeTestSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) "instance-key": "install-1", "name": "other", "channel": "stable", + "epoch": nil, }) io.WriteString(w, `{ @@ -6261,6 +6387,7 @@ func (s *storeTestSuite) TestSnapActionInstallParallelInstall(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6268,6 +6395,7 @@ func (s *storeTestSuite) TestSnapActionInstallParallelInstall(c *C) { "instance-key": "install-1", "name": "hello-world", "channel": "stable", + "epoch": nil, }) io.WriteString(w, `{ @@ -6373,6 +6501,7 @@ func (s *storeTestSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6380,6 +6509,7 @@ func (s *storeTestSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) { "instance-key": "install-1", "name": "hello-world", "channel": "stable", + "epoch": nil, }) io.WriteString(w, `{ @@ -6455,6 +6585,7 @@ func (s *storeTestSuite) TestSnapActionRefreshUnexpectedInstanceKey(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ @@ -6538,6 +6669,7 @@ func (s *storeTestSuite) TestSnapActionUnexpectedErrorKey(c *C) { "revision": float64(26), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ "snap-id": helloWorldSnapID, @@ -6545,12 +6677,14 @@ func (s *storeTestSuite) TestSnapActionUnexpectedErrorKey(c *C) { "revision": float64(2), "tracking-channel": "stable", "refreshed-date": helloRefreshedDateStr, + "epoch": iZeroEpoch, }) c.Assert(req.Actions, HasLen, 1) c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ "action": "install", "instance-key": "install-1", "name": "foo-2", + "epoch": nil, }) io.WriteString(w, `{ From 585f9294b01afb35a4527f86b86aa778c2ccae49 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 15 Nov 2018 08:46:02 +0000 Subject: [PATCH 077/580] snap: add (snap.Epoch).Unset method --- snap/epoch.go | 9 +++++++-- snap/epoch_test.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/snap/epoch.go b/snap/epoch.go index 17df76b994a..d72085455de 100644 --- a/snap/epoch.go +++ b/snap/epoch.go @@ -146,9 +146,14 @@ func (e *Epoch) UnmarshalYAML(unmarshal func(interface{}) error) error { return e.fromStructured(structured) } +// Unset checks if a snap is not set +func (e *Epoch) Unset() bool { + return e == nil || (e.Read == nil && e.Write == nil) +} + // Validate checks that the epoch makes sense. func (e *Epoch) Validate() error { - if e == nil || (e.Read == nil && e.Write == nil) { + if e.Unset() { // (*Epoch)(nil) and &Epoch{} are valid epochs, equivalent to "0" return nil } @@ -169,7 +174,7 @@ func (e *Epoch) Validate() error { } func (e *Epoch) simplify() interface{} { - if e == nil || (e.Read == nil && e.Write == nil) { + if e.Unset() { return "0" } if len(e.Write) == 1 && len(e.Read) == 1 && e.Read[0] == e.Write[0] { diff --git a/snap/epoch_test.go b/snap/epoch_test.go index 063ba7a389f..7829095c3cc 100644 --- a/snap/epoch_test.go +++ b/snap/epoch_test.go @@ -230,6 +230,20 @@ func (s *epochSuite) TestE(c *check.C) { } } +func (s *epochSuite) TestUnset(c *check.C) { + for _, e := range []*snap.Epoch{nil, {}} { + c.Check(e.Unset(), check.Equals, true, check.Commentf("%#v", e)) + } + for _, e := range []*snap.Epoch{ + {Read: []uint32{0}, Write: []uint32{0}}, + {Read: []uint32{}, Write: []uint32{}}, // invalid + {Read: []uint32{0}}, // invalid + {Write: []uint32{0}}, // invalid + } { + c.Check(e.Unset(), check.Equals, false, check.Commentf("%#v", e)) + } +} + func (s *epochSuite) TestCanRead(c *check.C) { tests := []struct { a, b snap.Epoch From ee013621edb6a771661ac6136ad46d4417fbfce8 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 15 Nov 2018 08:48:38 +0000 Subject: [PATCH 078/580] store: fix amend test, and send epoch if unset (as opposed to if snapid != "") The amend case was broken. Silly me. --- store/store.go | 6 +++--- store/store_test.go | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/store/store.go b/store/store.go index f61c1824635..88f907494e9 100644 --- a/store/store.go +++ b/store/store.go @@ -2202,10 +2202,10 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act if a.Action != "refresh" { aJSON.Name = snap.InstanceSnap(a.InstanceName) - if a.SnapID != "" { - aJSON.Epoch = &a.Epoch - } else { + if a.Epoch.Unset() { aJSON.Epoch = (*snap.Epoch)(nil) + } else { + aJSON.Epoch = &a.Epoch } } diff --git a/store/store_test.go b/store/store_test.go index df7ff8f97d6..252ed268748 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -5090,7 +5090,6 @@ func (s *storeTestSuite) TestSnapActionInstallAmend(c *C) { "name": "hello-world", "channel": "beta", "epoch": map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}}, - "snap-id": helloWorldSnapID, }) fmt.Fprint(w, `{ @@ -5131,7 +5130,6 @@ func (s *storeTestSuite) TestSnapActionInstallAmend(c *C) { Action: "install", InstanceName: "hello-world", Channel: "beta", - SnapID: helloWorldSnapID, Epoch: snap.E("1*"), }, }, nil, nil) From fac88ebc03cf2b8509b69a9ff8db740728736fa9 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 15 Nov 2018 09:18:57 +0000 Subject: [PATCH 079/580] overlord/snapstate: add amend runthrough, fix amend epoch bug Thanks @pedronis. --- overlord/snapstate/snapstate_test.go | 120 +++++++++++++++++++++++++++ overlord/snapstate/storehelpers.go | 1 + 2 files changed, 121 insertions(+) diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 3c05e8144cb..a38b55e384c 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -2758,6 +2758,126 @@ func (s *snapmgrTestSuite) TestInstalling(c *C) { c.Check(snapstate.Installing(s.state), Equals, true) } +func (s *snapmgrTestSuite) TestUpdateAmendRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + snaptest.MockSnap(c, `name: some-snap`, &si) + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + Channel: "stable", + }) + + chg := s.state.NewChange("refresh", "refresh a snap") + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snap.R(0), s.user.ID, snapstate.Flags{Amend: true}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + // ensure all our tasks ran + c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ + macaroon: s.user.StoreMacaroon, + name: "some-snap", + target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap"), + }}) + c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true, Commentf("salts seen: %v", s.fakeStore.seenPrivacyKeys)) + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, []string{ + "storesvc-snap-action", + "storesvc-snap-action:action", + "storesvc-download", + "validate-snap:Doing", + "current", + "open-snap-file", + "setup-snap", + "remove-snap-aliases", + "unlink-snap", + "copy-data", + "setup-profiles:Doing", + "candidate", + "link-snap", + "auto-connect:Doing", + "update-aliases", + "cleanup-trash", + }) + // just check the interesting op + c.Check(s.fakeBackend.ops[1], DeepEquals, fakeOp{ + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "install", // we asked for an Update, but an amend is actually an Install + InstanceName: "some-snap", + Channel: "some-channel", + Epoch: snap.E("1*"), // in amend, epoch in the action is not nil! + Flags: store.SnapActionEnforceValidation, + }, + revno: snap.R(11), + userID: 1, + }) + + task := ts.Tasks()[1] + // verify snapSetup info + 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, + Type: snap.TypeApp, + PlugsOnly: true, + Flags: snapstate.Flags{Amend: true}, + }) + c.Assert(snapsup.SideInfo, DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(11), + Channel: "some-channel", + SnapID: "some-snap-id", + }) + + // verify services stop reason + verifyStopReason(c, ts, "refresh") + + // check post-refresh hook + task = ts.Tasks()[14] + c.Assert(task.Kind(), Equals, "run-hook") + c.Assert(task.Summary(), Matches, `Run post-refresh hook of "some-snap" snap if present`) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "", + Revision: snap.R(7), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "some-channel", + SnapID: "some-snap-id", + Revision: snap.R(11), + }) +} + func (s *snapmgrTestSuite) TestUpdateRunThrough(c *C) { // use services-snap here to make sure services would be stopped/started appropriately si := snap.SideInfo{ diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index 83d00951b61..cebe72a9ea3 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -167,6 +167,7 @@ func updateInfo(st *state.State, snapst *SnapState, opts *updateInfoOpts, userID if curInfo.SnapID == "" { // amend action.Action = "install" + action.Epoch = curInfo.Epoch } theStore := Store(st) From 386287e5977a8767d6614a39a1494a9294d55164 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 15 Nov 2018 09:23:57 +0000 Subject: [PATCH 080/580] store: tweak comments Move the big NOTE into the struct it's talking about. Also add a short comment close to where the subtlety in question is performed. Thanks @bboozzoo. --- store/store.go | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/store/store.go b/store/store.go index 88f907494e9..62d4d281f56 100644 --- a/store/store.go +++ b/store/store.go @@ -1981,24 +1981,22 @@ func isValidAction(action string) bool { } type snapActionJSON struct { - Action string `json:"action"` - InstanceKey string `json:"instance-key"` - Name string `json:"name,omitempty"` - SnapID string `json:"snap-id,omitempty"` - Channel string `json:"channel,omitempty"` - Revision int `json:"revision,omitempty"` - Epoch interface{} `json:"epoch,omitempty"` // see note - IgnoreValidation *bool `json:"ignore-validation,omitempty"` -} - -// NOTE about epoch in snapActionJSON: the store needs an epoch (even if null) -// for the "install" and "download" actions, to know the client handles epochs -// at all. "refresh" actions should send nothing, not even null -- the snap in -// the context should have the epoch already. We achieve this by making Epoch -// be an `interface{}` with omitempty, and then setting it to a (possibly nil) -// epoch for install and download. As a nil epoch is not an empty interface{}, -// you'll get the null in the json. -// Play with it: https://play.jsgo.io/b20765a68e5ace2c0befdcde94c872ef78cd5e8f + Action string `json:"action"` + InstanceKey string `json:"instance-key"` + Name string `json:"name,omitempty"` + SnapID string `json:"snap-id,omitempty"` + Channel string `json:"channel,omitempty"` + Revision int `json:"revision,omitempty"` + IgnoreValidation *bool `json:"ignore-validation,omitempty"` + + // NOTE the store needs an epoch (even if null) for the "install" and "download" + // actions, to know the client handles epochs at all. "refresh" actions should + // send nothing, not even null -- the snap in the context should have the epoch + // already. We achieve this by making Epoch be an `interface{}` with omitempty, + // and then setting it to a (possibly nil) epoch for install and download. As a + // nil epoch is not an empty interface{}, you'll get the null in the json. + Epoch interface{} `json:"epoch,omitempty"` +} type snapRelease struct { Architecture string `json:"architecture"` @@ -2203,8 +2201,12 @@ func (s *Store) snapAction(ctx context.Context, currentSnaps []*CurrentSnap, act if a.Action != "refresh" { aJSON.Name = snap.InstanceSnap(a.InstanceName) if a.Epoch.Unset() { + // Let the store know we can handle epochs, by sending the `epoch` + // field in the request. A nil epoch is not an empty interface{}, + // you'll get the null in the json. See comment in snapActionJSON. aJSON.Epoch = (*snap.Epoch)(nil) } else { + // this is the amend case aJSON.Epoch = &a.Epoch } } From 1b3d4158c14c43a282c9ddbc955688738083b25a Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 12:12:07 +0100 Subject: [PATCH 081/580] userd: force zenity width if the text displayed is long Gtk is not doing a good job with long labels. It will create a very narrow and long window (minimal width to fit the buttons). So force a bigger width when there is a lot of text. See also LP: 1778484 --- userd/ui/zenity.go | 9 +++++++++ userd/ui/zenity_test.go | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/userd/ui/zenity.go b/userd/ui/zenity.go index 68b24119c0f..93e790bd506 100644 --- a/userd/ui/zenity.go +++ b/userd/ui/zenity.go @@ -42,6 +42,15 @@ func (*Zenity) YesNo(primary, secondary string, options *DialogOptions) bool { if options.Timeout > 0 { args = append(args, fmt.Sprintf("--timeout=%d", int(options.Timeout/time.Second))) } + // Gtk is not doing a good job with long labels. It will + // create a very narrow and long window (minimal width to fit + // the buttons). So force a bigger width here. See also LP: + // 1778484. The primary number is lower because the header is + // in a bigger font. + if len(primary) > 10 || len(secondary) > 20 { + args = append(args, "--width=500") + } + cmd := exec.Command("zenity", args...) if err := cmd.Start(); err != nil { return false diff --git a/userd/ui/zenity_test.go b/userd/ui/zenity_test.go index ffe463396b4..c846bde7d6f 100644 --- a/userd/ui/zenity_test.go +++ b/userd/ui/zenity_test.go @@ -62,6 +62,17 @@ func (s *zenitySuite) TestYesNoSimpleNo(c *C) { }) } +func (s *zenitySuite) TestYesNoLong(c *C) { + mock := testutil.MockCommand(c, "zenity", "true") + defer mock.Restore() + + z := &ui.Zenity{} + _ = z.YesNo("01234567890", "01234567890", nil) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", "--text=01234567890\n\n01234567890", "--width=500"}, + }) +} + func (s *zenitySuite) TestYesNoSimpleFooter(c *C) { mock := testutil.MockCommand(c, "zenity", "") defer mock.Restore() From cce17301fbafb00c2b5fdb36e95b1118716cde38 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 16 Nov 2018 12:30:39 +0100 Subject: [PATCH 082/580] cmd/snap-update-ns: extra debugging of trespassing events When trespassing is detected it may be difficult to understand exactly what was the context of the event. The extra debugging dumps useful information right next to the spot where it happens. Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/trespassing.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cmd/snap-update-ns/trespassing.go b/cmd/snap-update-ns/trespassing.go index 73670a44c37..7dbae492bc4 100644 --- a/cmd/snap-update-ns/trespassing.go +++ b/cmd/snap-update-ns/trespassing.go @@ -24,6 +24,8 @@ import ( "path/filepath" "strings" "syscall" + + "github.com/snapcore/snapd/logger" ) // Assumptions track the assumptions about the state of the filesystem. @@ -178,6 +180,11 @@ func (rs *Restrictions) Check(dirFd int, dirName string) error { // kind of base snap. return fmt.Errorf("cannot recover from trespassing over /") } + logger.Debugf("trespassing violated %q while striving to %q", dirName, rs.desiredPath) + logger.Debugf("restricted mode: %#v", rs.restricted) + logger.Debugf("unrestricted paths: %q", rs.assumptions.unrestrictedPaths) + logger.Debugf("verified devices: %v", rs.assumptions.verifiedDevices) + logger.Debugf("past changes: %v", rs.assumptions.pastChanges) return &TrespassingError{ViolatedPath: filepath.Clean(dirName), DesiredPath: rs.desiredPath} } From 4512eb2c599369bb69b54743c7e839b67d4e5fd4 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 14 Nov 2018 11:22:11 +0100 Subject: [PATCH 083/580] cmd/libsnap: add sc_verify_snap_lock This new API allows ensuring that a lock is held. This is needed by tools that are invoked from snap-confine that would otherwise normally grab a lock themselves. Instead when they are invoked from snap-confine they check that a lock is held. Signed-off-by: Zygmunt Krynicki --- cmd/libsnap-confine-private/locking-test.c | 27 ++++++++++ cmd/libsnap-confine-private/locking.c | 62 ++++++++++++++++++---- cmd/libsnap-confine-private/locking.h | 8 +++ 3 files changed, 87 insertions(+), 10 deletions(-) diff --git a/cmd/libsnap-confine-private/locking-test.c b/cmd/libsnap-confine-private/locking-test.c index 641b7c6f545..49b0527e2f4 100644 --- a/cmd/libsnap-confine-private/locking-test.c +++ b/cmd/libsnap-confine-private/locking-test.c @@ -92,6 +92,29 @@ static void test_sc_lock_unlock(void) g_assert_cmpint(err, ==, 0); } +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__locked(void) +{ + (void)sc_test_use_fake_lock_dir(); + int fd = sc_lock_snap("foo"); + sc_verify_snap_lock("foo"); + sc_unlock(fd); +} + +// Check that holding a lock is properly detected. +static void test_sc_verify_snap_lock__unlocked(void) +{ + (void)sc_test_use_fake_lock_dir(); + if (g_test_subprocess()) { + sc_verify_snap_lock("foo"); + return; + } + g_test_trap_subprocess(NULL, 0, 0); + g_test_trap_assert_failed(); + g_test_trap_assert_stderr + ("unexpectedly managed to acquire exclusive lock over snap foo\n"); +} + static void test_sc_enable_sanity_timeout(void) { if (g_test_subprocess()) { @@ -112,4 +135,8 @@ static void __attribute__ ((constructor)) init(void) g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock); g_test_add_func("/locking/sc_enable_sanity_timeout", test_sc_enable_sanity_timeout); + g_test_add_func("/locking/sc_verify_snap_lock__locked", + test_sc_verify_snap_lock__locked); + g_test_add_func("/locking/sc_verify_snap_lock__unlocked", + test_sc_verify_snap_lock__unlocked); } diff --git a/cmd/libsnap-confine-private/locking.c b/cmd/libsnap-confine-private/locking.c index 5439dcc97fe..1cd567997cd 100644 --- a/cmd/libsnap-confine-private/locking.c +++ b/cmd/libsnap-confine-private/locking.c @@ -21,6 +21,7 @@ #include "locking.h" +#include #include #include #include @@ -84,7 +85,7 @@ void sc_disable_sanity_timeout(void) static const char *sc_lock_dir = SC_LOCK_DIR; -static int sc_lock_generic(const char *scope, uid_t uid) +static int get_lock_directory(void) { // Create (if required) and open the lock directory. debug("creating lock directory %s (if missing)", sc_lock_dir); @@ -92,33 +93,50 @@ static int sc_lock_generic(const char *scope, uid_t uid) die("cannot create lock directory %s", sc_lock_dir); } debug("opening lock directory %s", sc_lock_dir); - int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; - dir_fd = + int dir_fd = open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); if (dir_fd < 0) { die("cannot open lock directory"); } - // Construct the name of the lock file. - char lock_fname[PATH_MAX] = { 0 }; + return dir_fd; +} + +static void get_lock_name(char *lock_fname, size_t size, const char *scope, + uid_t uid) +{ if (uid == 0) { // The root user doesn't have a per-user mount namespace. // Doing so would be confusing for services which use $SNAP_DATA // as home, and not in $SNAP_USER_DATA. - sc_must_snprintf(lock_fname, sizeof lock_fname, "%s.lock", - scope ? : ""); + sc_must_snprintf(lock_fname, size, "%s.lock", scope ? : ""); } else { - sc_must_snprintf(lock_fname, sizeof lock_fname, "%s.%d.lock", + sc_must_snprintf(lock_fname, size, "%s.%d.lock", scope ? : "", uid); } +} + +static int open_lock(const char *scope, uid_t uid) +{ + int dir_fd SC_CLEANUP(sc_cleanup_close) = -1; + char lock_fname[PATH_MAX] = { 0 }; + int lock_fd; + + dir_fd = get_lock_directory(); + get_lock_name(lock_fname, sizeof lock_fname, scope, uid); // Open the lock file and acquire an exclusive lock. debug("opening lock file: %s/%s", sc_lock_dir, lock_fname); - int lock_fd = openat(dir_fd, lock_fname, - O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + lock_fd = openat(dir_fd, lock_fname, + O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); if (lock_fd < 0) { die("cannot open lock file: %s/%s", sc_lock_dir, lock_fname); } + return lock_fd; +} +static int sc_lock_generic(const char *scope, uid_t uid) +{ + int lock_fd = open_lock(scope, uid); sc_enable_sanity_timeout(); debug("acquiring exclusive lock (scope %s, uid %d)", scope ? : "(global)", uid); @@ -143,6 +161,30 @@ int sc_lock_snap(const char *snap_name) return sc_lock_generic(snap_name, 0); } +void sc_verify_snap_lock(const char *snap_name) +{ + int lock_fd, retval; + + lock_fd = open_lock(snap_name, 0); + debug("trying to acquire exclusive lock over snap %s", snap_name); + retval = flock(lock_fd, LOCK_EX | LOCK_NB); + if (retval == 0) { + /* We managed to grab the lock, the lock was not held! */ + flock(lock_fd, LOCK_UN); + close(lock_fd); + errno = 0; + die("unexpectedly managed to acquire exclusive lock over snap %s", snap_name); + } + if (retval < 0) { + if (errno == EWOULDBLOCK) { + /* We tried but failed to grab the lock because the file is already + * locked. Good, this is what we expected. */ + return; + } + die("cannot acquire exclusive lock over snap %s", snap_name); + } +} + int sc_lock_snap_user(const char *snap_name, uid_t uid) { return sc_lock_generic(snap_name, uid); diff --git a/cmd/libsnap-confine-private/locking.h b/cmd/libsnap-confine-private/locking.h index 27eae6a60a8..b900528a09e 100644 --- a/cmd/libsnap-confine-private/locking.h +++ b/cmd/libsnap-confine-private/locking.h @@ -53,6 +53,14 @@ int sc_lock_global(void); **/ int sc_lock_snap(const char *snap_name); +/** + * Verify that a flock-based, exclusive, snap-scoped, lock is held. + * + * If the lock is not held the process dies. The details about the lock + * are exactly the same as for sc_lock_snap(). + **/ +void sc_verify_snap_lock(const char *snap_name); + /** * Obtain a flock-based, exclusive, snap-scoped, lock. * From 21946850f81b5deedf346e1e84697a5256a54512 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 16 Nov 2018 12:40:48 +0100 Subject: [PATCH 084/580] cmd/libsnap: tweak wording Signed-off-by: Zygmunt Krynicki --- cmd/libsnap-confine-private/locking.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/libsnap-confine-private/locking.c b/cmd/libsnap-confine-private/locking.c index 1cd567997cd..964ea247ab7 100644 --- a/cmd/libsnap-confine-private/locking.c +++ b/cmd/libsnap-confine-private/locking.c @@ -166,7 +166,8 @@ void sc_verify_snap_lock(const char *snap_name) int lock_fd, retval; lock_fd = open_lock(snap_name, 0); - debug("trying to acquire exclusive lock over snap %s", snap_name); + debug("trying to verify whether exclusive lock over snap %s is held", + snap_name); retval = flock(lock_fd, LOCK_EX | LOCK_NB); if (retval == 0) { /* We managed to grab the lock, the lock was not held! */ From f297d5796586dbd5dde9e62100befd155659a5d2 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 16 Nov 2018 12:41:05 +0100 Subject: [PATCH 085/580] cmd/libsnap: tweak logic to decrease nesting Signed-off-by: Zygmunt Krynicki --- cmd/libsnap-confine-private/locking.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cmd/libsnap-confine-private/locking.c b/cmd/libsnap-confine-private/locking.c index 964ea247ab7..4140d7e695c 100644 --- a/cmd/libsnap-confine-private/locking.c +++ b/cmd/libsnap-confine-private/locking.c @@ -176,14 +176,11 @@ void sc_verify_snap_lock(const char *snap_name) errno = 0; die("unexpectedly managed to acquire exclusive lock over snap %s", snap_name); } - if (retval < 0) { - if (errno == EWOULDBLOCK) { - /* We tried but failed to grab the lock because the file is already - * locked. Good, this is what we expected. */ - return; - } - die("cannot acquire exclusive lock over snap %s", snap_name); + if (retval < 0 && errno != EWOULDBLOCK) { + die("cannot verify exclusive lock over snap %s", snap_name); } + /* We tried but failed to grab the lock because the file is already locked. + * Good, this is what we expected. */ } int sc_lock_snap_user(const char *snap_name, uid_t uid) From e430b0457da41f0a361b3cdb33f18f40b9e478fb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 14 Nov 2018 17:29:12 +0100 Subject: [PATCH 086/580] snap: enforce minimal snap name len of 2 Trivial drive-by after looking at LP: #1763048 --- snap/validate.go | 2 +- snap/validate_test.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/snap/validate.go b/snap/validate.go index a122a1f57c3..8c52fe1f71c 100644 --- a/snap/validate.go +++ b/snap/validate.go @@ -93,7 +93,7 @@ func ValidateInstanceName(instanceName string) error { func ValidateName(name string) error { // NOTE: This function should be synchronized with the two other // implementations: sc_snap_name_validate and validate_snap_name . - if len(name) > 40 || !isValidName(name) { + if len(name) < 2 || len(name) > 40 || !isValidName(name) { return fmt.Errorf("invalid snap name: %q", name) } return nil diff --git a/snap/validate_test.go b/snap/validate_test.go index 223f15f78b8..ea83fc67b63 100644 --- a/snap/validate_test.go +++ b/snap/validate_test.go @@ -70,7 +70,7 @@ func (s *ValidateSuite) TearDownTest(c *C) { func (s *ValidateSuite) TestValidateName(c *C) { validNames := []string{ - "a", "aa", "aaa", "aaaa", + "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "01game", "1-or-2", @@ -84,6 +84,8 @@ func (s *ValidateSuite) TestValidateName(c *C) { invalidNames := []string{ // name cannot be empty "", + // too short (min 2 chars) + "a", // names cannot be too long "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", @@ -114,7 +116,7 @@ func (s *ValidateSuite) TestValidateName(c *C) { func (s *ValidateSuite) TestValidateInstanceName(c *C) { validNames := []string{ // plain names are also valid instance names - "a", "aa", "aaa", "aaaa", + "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", // snap instance "foo_bar", @@ -130,6 +132,7 @@ func (s *ValidateSuite) TestValidateInstanceName(c *C) { // invalid names are also invalid instance names, just a few // samples "", + "a", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", "a--a", From 490563de0f59498ba842f8664ec9105b1e78c1eb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 09:21:35 +0100 Subject: [PATCH 087/580] snap-confine: update sc_snap_name_validate() to check min length --- cmd/libsnap-confine-private/snap-test.c | 10 +++++++--- cmd/libsnap-confine-private/snap.c | 7 +++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/libsnap-confine-private/snap-test.c b/cmd/libsnap-confine-private/snap-test.c index b605e1257e0..e5fe9917e10 100644 --- a/cmd/libsnap-confine-private/snap-test.c +++ b/cmd/libsnap-confine-private/snap-test.c @@ -188,7 +188,7 @@ static void test_sc_snap_or_instance_name_validate(gconstpointer data) sc_error_free(err); const char *valid_names[] = { - "a", "aa", "aaa", "aaaa", + "aa", "aaa", "aaaa", "a-a", "aa-a", "a-aa", "a-b-c", "a0", "a-0", "a-0a", "01game", "1-or-2" @@ -201,6 +201,8 @@ static void test_sc_snap_or_instance_name_validate(gconstpointer data) const char *invalid_names[] = { // name cannot be empty "", + // too short + "a", // names cannot be too long "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx", @@ -317,8 +319,8 @@ static void test_sc_instance_name_validate(void) sc_error_free(err); const char *valid_names[] = { - "a", "aa", "aaa", "aaaa", - "a_a", "aa_1", "a_123", "a_0123456789", + "aa", "aaa", "aaaa", + "aa_a", "aa_1", "aa_123", "aa_0123456789", }; for (size_t i = 0; i < sizeof valid_names / sizeof *valid_names; ++i) { g_test_message("checking valid instance name: %s", @@ -327,6 +329,8 @@ static void test_sc_instance_name_validate(void) g_assert_null(err); } const char *invalid_names[] = { + // too short + "a", // only letters and digits in the instance key "a_--23))", "a_ ", "a_091234#", "a_123_456", // up to 10 characters for the instance key diff --git a/cmd/libsnap-confine-private/snap.c b/cmd/libsnap-confine-private/snap.c index f4fb8507f94..d78942b345b 100644 --- a/cmd/libsnap-confine-private/snap.c +++ b/cmd/libsnap-confine-private/snap.c @@ -248,10 +248,17 @@ void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) if (!got_letter) { err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, "snap name must contain at least one letter"); + goto out; + } + if (n < 2) { + err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, + "snap name must be longer than 1 character"); + goto out; } if (n > 40) { err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME, "snap name must be shorter than 40 characters"); + goto out; } out: From 2ae7c2475252c985da5f17d169157dba1b046a76 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 09:28:43 +0100 Subject: [PATCH 088/580] snap-update-ns: check min length in validate_snap_name() as well and updat test --- cmd/snap-update-ns/bootstrap.c | 4 ++ cmd/snap-update-ns/bootstrap_test.go | 65 +++++++++++++++++----------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c index 4e0e60ac163..282f6a8198e 100644 --- a/cmd/snap-update-ns/bootstrap.c +++ b/cmd/snap-update-ns/bootstrap.c @@ -239,6 +239,10 @@ int validate_snap_name(const char *snap_name) bootstrap_msg = "snap name must contain at least one letter"; return -1; } + if (n < 2) { + bootstrap_msg = "snap name must be longer than 1 character"; + return -1; + } if (n > 40) { bootstrap_msg = "snap name must be shorter than 40 characters"; return -1; diff --git a/cmd/snap-update-ns/bootstrap_test.go b/cmd/snap-update-ns/bootstrap_test.go index 4324e785079..92eda827d51 100644 --- a/cmd/snap-update-ns/bootstrap_test.go +++ b/cmd/snap-update-ns/bootstrap_test.go @@ -30,32 +30,47 @@ type bootstrapSuite struct{} var _ = Suite(&bootstrapSuite{}) // Check that ValidateSnapName rejects "/" and "..". -func (s *bootstrapSuite) TestValidateSnapName(c *C) { - c.Assert(update.ValidateInstanceName("hello-world"), Equals, 0) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789"), Equals, 0) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789"), Equals, 0) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_01234567890"), Equals, -1) - c.Assert(update.ValidateInstanceName("hello/world"), Equals, -1) - c.Assert(update.ValidateInstanceName("hello..world"), Equals, -1) - c.Assert(update.ValidateInstanceName("hello-world_foo"), Equals, 0) - c.Assert(update.ValidateInstanceName("foo_0123456789"), Equals, 0) - c.Assert(update.ValidateInstanceName("foo_1234abcd"), Equals, 0) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789"), Equals, 0) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789"), Equals, 0) +func (s *bootstrapSuite) TestValidateInstanceName(c *C) { + validNames := []string{ + "aa", + "aa_a", + "hello-world", + "a123456789012345678901234567890123456789", + "a123456789012345678901234567890123456789_0123456789", + "hello-world_foo", + "foo_0123456789", + "foo_1234abcd", + "a123456789012345678901234567890123456789", + "a123456789012345678901234567890123456789_0123456789", + } + for _, name := range validNames { + c.Check(update.ValidateInstanceName(name), Equals, 0, Commentf("name %q should be valid but is not", name)) + } + + invalidNames := []string{ + "", + "a", + "a_a", + "a123456789012345678901234567890123456789_01234567890", + "hello/world", + "hello..world", + "INVALID", + "-invalid", + "hello-world_", + "_foo", + "foo_01234567890", + "foo_123_456", + "foo__456", + "foo_", + "hello-world_foo_foo", + "foo01234567890012345678900123456789001234567890", + "foo01234567890012345678900123456789001234567890_foo", + "a123456789012345678901234567890123456789_0123456789_", + } + for _, name := range invalidNames { + c.Check(update.ValidateInstanceName(name), Equals, -1, Commentf("name %q should be invalid but is valid", name)) + } - c.Assert(update.ValidateInstanceName("INVALID"), Equals, -1) - c.Assert(update.ValidateInstanceName("-invalid"), Equals, -1) - c.Assert(update.ValidateInstanceName(""), Equals, -1) - c.Assert(update.ValidateInstanceName("hello-world_"), Equals, -1) - c.Assert(update.ValidateInstanceName("_foo"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo_01234567890"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo_123_456"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo__456"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo_"), Equals, -1) - c.Assert(update.ValidateInstanceName("hello-world_foo_foo"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo01234567890012345678900123456789001234567890"), Equals, -1) - c.Assert(update.ValidateInstanceName("foo01234567890012345678900123456789001234567890_foo"), Equals, -1) - c.Assert(update.ValidateInstanceName("a123456789012345678901234567890123456789_0123456789_"), Equals, -1) } // Test various cases of command line handling. From 968762570fccd0c93b0ee1b61b0d161c4d502db9 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 11:49:21 +0100 Subject: [PATCH 089/580] interfaces: update test to follow new minimal snap name length rule --- interfaces/repo_test.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index 5b9b61eab99..e4ef0e775b4 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -350,24 +350,24 @@ func (s *RepositorySuite) TestPlug(c *C) { func (s *RepositorySuite) TestPlugSearch(c *C) { addPlugsSlotsFromInstances(c, s.testRepo, []instanceNameAndYaml{ - {Name: "x", Yaml: ` -name: x + {Name: "xx", Yaml: ` +name: xx version: 0 plugs: a: interface b: interface c: interface `}, - {Name: "y", Yaml: ` -name: y + {Name: "yy", Yaml: ` +name: yy version: 0 plugs: a: interface b: interface c: interface `}, - {Name: "z_instance", Yaml: ` -name: z + {Name: "zz_instance", Yaml: ` +name: zz version: 0 plugs: a: interface @@ -376,15 +376,15 @@ plugs: `}, }) // Plug() correctly finds plugs - c.Assert(s.testRepo.Plug("x", "a"), Not(IsNil)) - c.Assert(s.testRepo.Plug("x", "b"), Not(IsNil)) - c.Assert(s.testRepo.Plug("x", "c"), Not(IsNil)) - c.Assert(s.testRepo.Plug("y", "a"), Not(IsNil)) - c.Assert(s.testRepo.Plug("y", "b"), Not(IsNil)) - c.Assert(s.testRepo.Plug("y", "c"), Not(IsNil)) - c.Assert(s.testRepo.Plug("z_instance", "a"), Not(IsNil)) - c.Assert(s.testRepo.Plug("z_instance", "b"), Not(IsNil)) - c.Assert(s.testRepo.Plug("z_instance", "c"), Not(IsNil)) + c.Assert(s.testRepo.Plug("xx", "a"), Not(IsNil)) + c.Assert(s.testRepo.Plug("xx", "b"), Not(IsNil)) + c.Assert(s.testRepo.Plug("xx", "c"), Not(IsNil)) + c.Assert(s.testRepo.Plug("yy", "a"), Not(IsNil)) + c.Assert(s.testRepo.Plug("yy", "b"), Not(IsNil)) + c.Assert(s.testRepo.Plug("yy", "c"), Not(IsNil)) + c.Assert(s.testRepo.Plug("zz_instance", "a"), Not(IsNil)) + c.Assert(s.testRepo.Plug("zz_instance", "b"), Not(IsNil)) + c.Assert(s.testRepo.Plug("zz_instance", "c"), Not(IsNil)) } // Tests for Repository.RemovePlug() From bce0e54bc58299219b56e9102ccc750fa9fadc74 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 15 Nov 2018 18:16:13 +0100 Subject: [PATCH 090/580] snap-update-ns: run make fmt --- cmd/snap-update-ns/bootstrap.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/snap-update-ns/bootstrap.c b/cmd/snap-update-ns/bootstrap.c index 282f6a8198e..70a33cfd650 100644 --- a/cmd/snap-update-ns/bootstrap.c +++ b/cmd/snap-update-ns/bootstrap.c @@ -239,10 +239,10 @@ int validate_snap_name(const char *snap_name) bootstrap_msg = "snap name must contain at least one letter"; return -1; } - if (n < 2) { + if (n < 2) { bootstrap_msg = "snap name must be longer than 1 character"; return -1; - } + } if (n > 40) { bootstrap_msg = "snap name must be shorter than 40 characters"; return -1; From 3464ae16e05bd8c6d691c13e2a66c5be19708fe8 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 22 Nov 2018 11:29:25 +0100 Subject: [PATCH 091/580] Conflict check fix for hotplug-disconnect. --- overlord/ifacestate/handlers.go | 40 +++++++++- overlord/ifacestate/ifacestate_test.go | 104 +++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index ce0c58fc355..f28f1f8c5b9 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -797,6 +797,44 @@ func checkDisconnectConflicts(st *state.State, disconnectingSnap, plugSnap, slot return nil } +func checkHotplugDisconnectConflicts(st *state.State, plugSnap, slotSnap string) error { + for _, task := range st.Tasks() { + if task.Status().Ready() { + continue + } + + k := task.Kind() + if k == "connect" || k == "disconnect" { + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap { + return &state.Retry{After: connectRetryTimeout} + } + continue + } + + snapsup, err := snapstate.TaskSnapSetup(task) + // e.g. hook tasks don't have task snap setup + if err != nil { + continue + } + otherSnapName := snapsup.InstanceName() + + // different snaps - no conflict + if otherSnapName != plugSnap && otherSnapName != slotSnap { + continue + } + + if k == "link-snap" || k == "setup-profiles" || k == "unlink-snap" { + // other snap is getting installed/refreshed/removed - temporary conflict + return &state.Retry{After: connectRetryTimeout} + } + } + return nil +} + // inSameChangeWaitChains returns true if there is a wait chain so // that `startT` is run before `searchT` in the same state.Change. func inSameChangeWaitChain(startT, searchT *state.Task) bool { @@ -1244,7 +1282,7 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e // check for conflicts on all connections first before creating disconnect hooks for _, connRef := range connections { - if err := checkDisconnectConflicts(st, syssnap.InstanceName(), connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { + if err := checkHotplugDisconnectConflicts(st, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { if _, retry := err.(*state.Retry); retry { logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", syssnap.InstanceName(), connRef.PlugRef.Snap, connRef.SlotRef.Snap) task.Logf("Waiting for conflicting change in progress...") diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 4ddb9c72f6a..6db75a6ab6e 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -217,6 +217,8 @@ func (s *interfaceManagerSuite) SetUpTest(c *C) { buf, restore := logger.MockLogger() s.BaseTest.AddCleanup(restore) s.log = buf + + s.BaseTest.AddCleanup(ifacestate.MockConnectRetryTimeout(0)) } func (s *interfaceManagerSuite) TearDownTest(c *C) { @@ -5039,3 +5041,105 @@ func (s *interfaceManagerSuite) TestHotplugDisconnect(c *C) { "hotplug-gone": true, }}) } + +func (s *interfaceManagerSuite) testHotplugDisconnectWaitsForCoreRefresh(c *C, taskKind string) { + coreInfo := s.mockSnap(c, coreSnapYaml) + + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // mock the consumer + si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)} + testSnap := snaptest.MockSnapInstance(c, "", consumerYaml, si) + c.Assert(testSnap.Plugs["plug"], NotNil) + c.Assert(repo.AddPlug(testSnap.Plugs["plug"]), IsNil) + snapstate.Set(s.state, "consumer", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + }}) + _, err = repo.Connect(&interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"}}, + nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-disconnect", "") + t.Set("hotplug-key", "1234") + t.Set("interface", "test") + chg.AddTask(t) + + chg2 := s.state.NewChange("other-chg", "...") + t2 := s.state.NewTask(taskKind, "...") + t2.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "core"}, + }) + chg2.AddTask(t2) + t3 := s.state.NewTask("other", "") + t2.WaitFor(t3) + t3.SetStatus(state.HoldStatus) + chg2.AddTask(t3) + + s.state.Unlock() + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + c.Assert(chg.Err(), IsNil) + + c.Assert(strings.Join(t.Log(), ""), Matches, `.*Waiting for conflicting change in progress...`) + c.Assert(chg.Status(), Equals, state.DoingStatus) + + t2.SetStatus(state.DoneStatus) + t3.SetStatus(state.DoneStatus) + + s.state.Unlock() + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus) +} + +func (s *interfaceManagerSuite) TestHotplugDisconnectWaitsForCoreSetupProfiles(c *C) { + s.testHotplugDisconnectWaitsForCoreRefresh(c, "setup-profiles") +} + +func (s *interfaceManagerSuite) TestHotplugDisconnectWaitsForCoreLnkSnap(c *C) { + s.testHotplugDisconnectWaitsForCoreRefresh(c, "link-snap") +} + +func (s *interfaceManagerSuite) TestHotplugDisconnectWaitsForCoreUnlinkSnap(c *C) { + s.testHotplugDisconnectWaitsForCoreRefresh(c, "unlink-snap") +} From 497b74ae46be1ed57ccf70223cc407cbdf6591d1 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 26 Nov 2018 11:24:58 +0100 Subject: [PATCH 092/580] tests: improve interfaces-{personal,system}-files tests (thanks to jdstrand) --- tests/main/interfaces-personal-files/task.yaml | 10 +++++----- tests/main/interfaces-system-files/task.yaml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/main/interfaces-personal-files/task.yaml b/tests/main/interfaces-personal-files/task.yaml index 29b78501c2d..54636f2a9e2 100644 --- a/tests/main/interfaces-personal-files/task.yaml +++ b/tests/main/interfaces-personal-files/task.yaml @@ -76,11 +76,11 @@ execute: | MATCH "Permission denied" < call.error echo "Then the snap is not able to to access files and dirs outside $HOME" - test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/.testdir1" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/.testfile1" && exit 1 - test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/root/" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/testfile2" && exit 1 - test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/.testfile2" && exit 1 + test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/.testdir1" 2>&1| MATCH "Permission denied" + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/.testfile1" 2>&1| MATCH "Permission denied" + test-snapd-sh.with-personal-files-plug -c "ls /var/tmp/root/" 2>&1| MATCH "Permission denied" + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/testfile2" 2>&1| MATCH "Permission denied" + test-snapd-sh.with-personal-files-plug -c "cat /var/tmp/root/.testfile2" 2>&1| MATCH "Permission denied" echo "When the plug is disconnected" snap disconnect test-snapd-sh:personal-files diff --git a/tests/main/interfaces-system-files/task.yaml b/tests/main/interfaces-system-files/task.yaml index 90225273282..92049d31e1d 100644 --- a/tests/main/interfaces-system-files/task.yaml +++ b/tests/main/interfaces-system-files/task.yaml @@ -18,7 +18,7 @@ prepare: | # Fist layer of dirs and files ensure_dir_exists_backup_real "$TESTDIR" ensure_file_exists_backup_real "$TESTDIR"/.testfile1 - ensure_file_exists_backup_real "$TESTDIR"/testfile1 + ensure_file_exists_backup_real "$TESTDIR"/readonly_file1 ensure_dir_exists_backup_real "$TESTDIR"/.testdir1 ensure_dir_exists_backup_real "$TESTDIR"/testdir1 @@ -53,7 +53,7 @@ execute: | echo "Then the snap is able to access all the files and dirs in /testdir" test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/.testfile1" | MATCH "content for $TESTDIR/.testfile1" - test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/testfile1" MATCH "content for $TESTDIR/testfile1" + test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/readonly_file1" MATCH "content for $TESTDIR/readonly_file1" test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/.testdir1" test-snapd-sh.with-system-files-plug -c "ls $TESTDIR/testdir1" test-snapd-sh.with-system-files-plug -c "cat $TESTDIR/.testdir1/.testfilé2" | MATCH "content for $TESTDIR/.testdir1/.testfilé2" @@ -68,15 +68,15 @@ execute: | exit 0 fi - if test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/testfile1" 2> call.error; then + if test-snapd-sh.with-system-files-plug -c "echo test >> $TESTDIR/readonly_file1" 2> call.error; then echo "Expected permission error writing the system file" exit 1 fi MATCH "Permission denied" < call.error echo "Then the snap is not able to to access files and dirs in $HOME" - test-snapd-sh.with-system-files-plug -c "ls /root/.testdir1" && exit 1 - test-snapd-sh.with-system-files-plug -c "cat /root/.testfile1" && exit 1 + test-snapd-sh.with-system-files-plug -c "ls /root/.testdir1" 2>&1| MATCH "Permission denied" + test-snapd-sh.with-system-files-plug -c "cat /root/.testfile1" 2>&1| MATCH "Permission denied" echo "When the plug is disconnected" snap disconnect test-snapd-sh:system-files From e8436185b348f5f796758b7ae41ec7d522329c1c Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 26 Nov 2018 12:34:38 +0100 Subject: [PATCH 093/580] interfaces: address review feedback (thanks to Zyga) --- interfaces/apparmor/apparmor.go | 6 +++--- interfaces/builtin/common_files.go | 21 ++++++++++++--------- interfaces/builtin/personal_files.go | 2 +- interfaces/builtin/personal_files_test.go | 13 ++++++++----- interfaces/builtin/system_files_test.go | 9 +++++---- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/interfaces/apparmor/apparmor.go b/interfaces/apparmor/apparmor.go index 2cf8a852265..0efccce390a 100644 --- a/interfaces/apparmor/apparmor.go +++ b/interfaces/apparmor/apparmor.go @@ -35,13 +35,13 @@ import ( "github.com/snapcore/snapd/osutil" ) -// ValidateFreeFromAARE will check that the given string does not -// contain AppArmor regular expressions (AARE) or double quotes +// ValidateNoAppArmorRegexp will check that the given string does not +// contain AppArmor regular expressions (AARE), double quotes or \0. func ValidateNoAppArmorRegexp(s string) error { const AARE = `?*[]{}^"` + "\x00" if strings.ContainsAny(s, AARE) { - return fmt.Errorf("%q contains a reserved apparmor char from %s ", s, AARE) + return fmt.Errorf("%q contains a reserved apparmor char from %s", s, AARE) } return nil } diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index e0201adfb17..a125d143be3 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -49,9 +49,9 @@ const ( func (a filesAAPerm) String() string { switch a { case filesRead: - return "rk," + return "rk" case filesWrite: - return "rwkl," + return "rwkl" } panic(fmt.Sprintf("invalid perm: %d", a)) } @@ -62,8 +62,11 @@ func formatPath(ip interface{}) (string, error) { return "", fmt.Errorf("%[1]v (%[1]T) is not a string", ip) } prefix := "" - if strings.HasPrefix(p, "$HOME") && strings.Count(p, "$HOME") == 1 { - p = strings.Replace(p, "$HOME", "@{HOME}", 1) + // Note that the {personal,system}-files interface impose + // limitations on the $HOME usage - system-files forbids it, + // personal only allows starting with $HOME in the path. + if strings.Contains(p, "$HOME") { + p = strings.Replace(p, "$HOME", "@{HOME}", -1) prefix = "owner " } p += "{,/,/**}" @@ -77,7 +80,7 @@ func allowPathAccess(buf *bytes.Buffer, perm filesAAPerm, paths []interface{}) e if err != nil { return err } - fmt.Fprintf(buf, "%s %s\n", p, perm) + fmt.Fprintf(buf, "%s %s,\n", p, perm) } return nil } @@ -101,10 +104,10 @@ func (iface *commonFilesInterface) validateSinglePath(np string) error { } p := filepath.Clean(np) if p != np { - return fmt.Errorf("%q must be clean", np) + return fmt.Errorf("cannot use %q: try %q", np, filepath.Clean(np)) } if strings.Contains(p, "~") { - return fmt.Errorf(`%q contains invalid "~"`, p) + return fmt.Errorf(`%q cannot contain "~"`, p) } if err := apparmor.ValidateNoAppArmorRegexp(p); err != nil { return err @@ -127,11 +130,11 @@ func (iface *commonFilesInterface) BeforePreparePlug(plug *snap.PlugInfo) error if _, ok := plug.Attrs[att]; !ok { continue } - il, ok := plug.Attrs[att].([]interface{}) + paths, ok := plug.Attrs[att].([]interface{}) if !ok { return fmt.Errorf("cannot add %s plug: %q must be a list of strings", iface.name, att) } - if err := iface.validatePaths(att, il); err != nil { + if err := iface.validatePaths(att, paths); err != nil { return fmt.Errorf("cannot add %s plug: %s", iface.name, err) } hasValidAttr = true diff --git a/interfaces/builtin/personal_files.go b/interfaces/builtin/personal_files.go index b36a6ef1cf5..042c7043aaa 100644 --- a/interfaces/builtin/personal_files.go +++ b/interfaces/builtin/personal_files.go @@ -52,7 +52,7 @@ type personalFilesInterface struct { func validateSinglePathHome(np string) error { if !strings.HasPrefix(np, "$HOME/") { - return fmt.Errorf(`%q must start with "$HOME"`, np) + return fmt.Errorf(`%q must start with "$HOME/"`, np) } if strings.Count(np, "$HOME") > 1 { return fmt.Errorf(`$HOME must only be used at the start of the path of %q`, np) diff --git a/interfaces/builtin/personal_files_test.go b/interfaces/builtin/personal_files_test.go index dbf5637b69c..332e2e100f3 100644 --- a/interfaces/builtin/personal_files_test.go +++ b/interfaces/builtin/personal_files_test.go @@ -129,17 +129,20 @@ plugs: }{ {`read: ""`, `"read" must be a list of strings`}, {`read: [ 123 ]`, `"read" must be a list of strings`}, - {`read: [ "$HOME/foo/./bar" ]`, `"\$HOME/foo/./bar" must be clean`}, - {`read: [ "../foo" ]`, `"../foo" must start with "\$HOME"`}, + {`read: [ "$HOME/foo/./bar" ]`, `cannot use "\$HOME/foo/./bar": try "\$HOME/foo/bar"`}, + {`read: [ "../foo" ]`, `"../foo" must start with "\$HOME/"`}, {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, {`write: ""`, `"write" must be a list of strings`}, {`write: bar`, `"write" must be a list of strings`}, - {`read: [ "~/foo" ]`, `"~/foo" contains invalid "~"`}, - {`read: [ "$HOME/foo/~/foo" ]`, `"\$HOME/foo/~/foo" contains invalid "~"`}, - {`read: [ "$HOME/foo/../foo" ]`, `"\$HOME/foo/../foo" must be clean`}, + {`read: [ "~/foo" ]`, `"~/foo" cannot contain "~"`}, + {`read: [ "$HOME/foo/~/foo" ]`, `"\$HOME/foo/~/foo" cannot contain "~"`}, + {`read: [ "$HOME/foo/../foo" ]`, `cannot use "\$HOME/foo/../foo": try "\$HOME/foo"`}, {`read: [ "$HOME/home/$HOME/foo" ]`, `\$HOME must only be used at the start of the path of "\$HOME/home/\$HOME/foo"`}, + {`read: [ "$HOME/sweet/$HOME" ]`, `\$HOME must only be used at the start of the path of "\$HOME/sweet/\$HOME"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, + {`read: [ "${HOME}/foo" ]`, `"\${HOME}/foo" contains a reserved apparmor char from .*`}, + {`read: [ "$HOME" ]`, `"\$HOME" must start with "\$HOME/"`}, } for _, t := range testCases { diff --git a/interfaces/builtin/system_files_test.go b/interfaces/builtin/system_files_test.go index 4b3834aa507..6b13bdbd387 100644 --- a/interfaces/builtin/system_files_test.go +++ b/interfaces/builtin/system_files_test.go @@ -128,15 +128,16 @@ plugs: }{ {`read: ""`, `"read" must be a list of strings`}, {`read: [ 123 ]`, `"read" must be a list of strings`}, - {`read: [ "/foo/./bar" ]`, `"/foo/./bar" must be clean`}, + {`read: [ "/foo/./bar" ]`, `cannot use "/foo/./bar": try "/foo/bar"`}, {`read: [ "../foo" ]`, `"../foo" must start with "/"`}, {`read: [ "/foo[" ]`, `"/foo\[" contains a reserved apparmor char from .*`}, {`write: ""`, `"write" must be a list of strings`}, {`write: bar`, `"write" must be a list of strings`}, - {`read: [ "~/foo" ]`, `"~/foo" contains invalid "~"`}, - {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" contains invalid "~"`}, - {`read: [ "/foo/../foo" ]`, `"/foo/../foo" must be clean`}, + {`read: [ "~/foo" ]`, `"~/foo" cannot contain "~"`}, + {`read: [ "/foo/~/foo" ]`, `"/foo/~/foo" cannot contain "~"`}, + {`read: [ "/foo/../foo" ]`, `cannot use "/foo/../foo": try "/foo"`}, {`read: [ "/home/$HOME/foo" ]`, `\$HOME cannot be used in "/home/\$HOME/foo"`}, + {`read: [ "$HOME/sweet/$HOME" ]`, `"\$HOME/sweet/\$HOME" must start with "/"`}, {`read: [ "/@{FOO}" ]`, `"/@{FOO}" contains a reserved apparmor char from .*`}, {`read: [ "/home/@{HOME}/foo" ]`, `"/home/@{HOME}/foo" contains a reserved apparmor char from .*`}, } From 27a497e0e804fe287f2ddb20f56b5113d118a880 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 26 Nov 2018 23:46:19 -0300 Subject: [PATCH 094/580] Reset snapd state as part of the tests restore This splits the reset.sh script in functions and make sure the snapd state is reset during restore and all the tests run with the same state. --- spread.yaml | 8 +- tests/lib/prepare-restore.sh | 9 +- tests/lib/prepare.sh | 6 + tests/lib/reset.sh | 141 ++++++++++-------- .../main/classic-custom-device-reg/task.yaml | 4 +- 5 files changed, 101 insertions(+), 67 deletions(-) diff --git a/spread.yaml b/spread.yaml index 5d9f383cd20..dec1d4c8edd 100644 --- a/spread.yaml +++ b/spread.yaml @@ -663,12 +663,16 @@ suites: . "$TESTSLIB"/prepare.sh prepare_classic prepare-each: | - "$TESTSLIB"/reset.sh --reuse-core + #shellcheck source=tests/lib/reset.sh + . "$TESTSLIB"/reset.sh + reset_snapd --reuse-core #shellcheck source=tests/lib/prepare.sh . "$TESTSLIB"/prepare.sh prepare_each_classic restore: | - "$TESTSLIB"/reset.sh --store + #shellcheck source=tests/lib/reset.sh + . "$TESTSLIB"/reset.sh + reset_snapd --store #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh index 158108167ee..3bd2bc4d6ce 100755 --- a/tests/lib/prepare-restore.sh +++ b/tests/lib/prepare-restore.sh @@ -37,6 +37,8 @@ set -o pipefail # shellcheck source=tests/lib/systems.sh . "$TESTSLIB/systems.sh" +# shellcheck source=tests/lib/reset.sh +. "$TESTSLIB/reset.sh" ### ### Utility functions reused below. @@ -417,8 +419,6 @@ prepare_suite() { prepare_suite_each() { # save the job which is going to be executed in the system echo -n "$SPREAD_JOB " >> "$RUNTIME_STATE_PATH/runs" - # shellcheck source=tests/lib/reset.sh - "$TESTSLIB"/reset.sh --reuse-core # Reset systemd journal cursor. start_new_journalctl_log # shellcheck source=tests/lib/prepare.sh @@ -431,12 +431,11 @@ prepare_suite_each() { } restore_suite_each() { - true + reset_snapd --reuse-core } restore_suite() { - # shellcheck source=tests/lib/reset.sh - "$TESTSLIB"/reset.sh --store + reset_snapd --store if is_classic_system; then # shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 5d997984135..1ecc7f747be 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -16,6 +16,8 @@ set -eux . "$TESTSLIB/state.sh" # shellcheck source=tests/lib/systems.sh . "$TESTSLIB/systems.sh" +# shellcheck source=tests/lib/reset.sh +. "$TESTSLIB/reset.sh" disable_kernel_rate_limiting() { @@ -278,6 +280,8 @@ prepare_classic() { systemctl stop snapd.{service,socket} save_snapd_state systemctl start snapd.socket + else + init_state_classic --reuse-core fi disable_kernel_rate_limiting @@ -613,6 +617,8 @@ prepare_ubuntu_core() { systemctl stop snapd.service snapd.socket save_snapd_state systemctl start snapd.socket + else + init_state_all_snap --reuse-core fi disable_kernel_rate_limiting diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 12dff8f2db7..0a5d127857b 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -1,20 +1,55 @@ #!/bin/bash -set -e -x - # shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" + # shellcheck source=tests/lib/state.sh . "$TESTSLIB/state.sh" - # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" #shellcheck source=tests/lib/systems.sh -. "$TESTSLIB"/systems.sh +. "$TESTSLIB/systems.sh" + +init_state_classic() { + if [ "$1" = "--reuse-core" ]; then + # Restore snapd state and start systemd service units + restore_snapd_state + escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" + all_units="$(systemctl list-unit-files --full | cut -f1 -d ' ')" + mounts="" + if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount"; then + mounts="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount")" + fi + services="" + if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service"; then + services="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service")" + fi + systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ + for unit in $mounts $services; do + systemctl start "$unit" + done + + # force all profiles to be re-generated + rm -f /var/lib/snapd/system-key + fi + + if [ "$1" != "--keep-stopped" ]; then + systemctl start snapd.socket -reset_classic() { + # wait for snapd listening + EXTRA_NC_ARGS="-q 1" + case "$SPREAD_SYSTEM" in + fedora-*|amazon-*|centos-*) + EXTRA_NC_ARGS="" + ;; + esac + while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done + fi +} + +clean_classic() { # Reload all service units as in some situations the unit might # have changed on the disk. systemctl daemon-reload @@ -67,37 +102,19 @@ reset_classic() { rm -rf /root/.snap/gnupg rm -f /tmp/core* /tmp/ubuntu-core* +} - if [ "$1" = "--reuse-core" ]; then - # Restore snapd state and start systemd service units - restore_snapd_state - escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" - mounts="$(systemctl list-unit-files --full | grep "^${escaped_snap_mount_dir}[-.].*\\.mount" | cut -f1 -d ' ')" - services="$(systemctl list-unit-files --full | grep "^${escaped_snap_mount_dir}[-.].*\\.service" | cut -f1 -d ' ')" - systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ - for unit in $mounts $services; do - systemctl start "$unit" - done - - # force all profiles to be re-generated - rm -f /var/lib/snapd/system-key - fi - +init_state_all_snap() { + # ensure we have the same state as initially + systemctl stop snapd.service snapd.socket + restore_snapd_state + rm -rf /root/.snap if [ "$1" != "--keep-stopped" ]; then - systemctl start snapd.socket - - # wait for snapd listening - EXTRA_NC_ARGS="-q 1" - case "$SPREAD_SYSTEM" in - fedora-*|amazon-*|centos-*) - EXTRA_NC_ARGS="" - ;; - esac - while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done + systemctl start snapd.service snapd.socket fi } -reset_all_snap() { +clean_all_snap() { # remove all leftover snaps # shellcheck source=tests/lib/names.sh . "$TESTSLIB/names.sh" @@ -128,35 +145,41 @@ reset_all_snap() { if [ -n "$remove_bases" ]; then snap remove "$remove_bases" fi +} - # ensure we have the same state as initially - systemctl stop snapd.service snapd.socket - restore_snapd_state - rm -rf /root/.snap - if [ "$1" != "--keep-stopped" ]; then - systemctl start snapd.service snapd.socket +discard_ns() { + # Discard all mount namespaces and active mount profiles. + # This is duplicating logic in snap-discard-ns but it doesn't + # support --all switch yet so we cannot use it. + if [ -d /run/snapd/ns ]; then + for mnt in /run/snapd/ns/*.mnt; do + umount -l "$mnt" || true + rm -f "$mnt" + done + rm -f /run/snapd/ns/*.fstab fi } -if is_core_system; then - reset_all_snap "$@" -else - reset_classic "$@" -fi - -# Discard all mount namespaces and active mount profiles. -# This is duplicating logic in snap-discard-ns but it doesn't -# support --all switch yet so we cannot use it. -if [ -d /run/snapd/ns ]; then - for mnt in /run/snapd/ns/*.mnt; do - umount -l "$mnt" || true - rm -f "$mnt" - done - rm -f /run/snapd/ns/*.fstab -fi - -if [ "$REMOTE_STORE" = staging ] && [ "$1" = "--store" ]; then - # shellcheck source=tests/lib/store.sh - . "$TESTSLIB"/store.sh - teardown_staging_store -fi +tear_down_store() { + if [ "$REMOTE_STORE" = staging ] && [ "$1" = "--store" ]; then + # shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_staging_store + fi +} + +reset_snapd() { + if is_core_system; then + clean_all_snap "$@" + init_state_all_snap "$@" + discard_ns + tear_down_store "$@" + else + clean_classic "$@" + init_state_classic "$@" + discard_ns + tear_down_store "$@" + fi +} + + diff --git a/tests/main/classic-custom-device-reg/task.yaml b/tests/main/classic-custom-device-reg/task.yaml index 6476b3d01bb..3742a9165cf 100644 --- a/tests/main/classic-custom-device-reg/task.yaml +++ b/tests/main/classic-custom-device-reg/task.yaml @@ -19,7 +19,9 @@ prepare: | snap pack "$TESTSLIB/snaps/classic-gadget" snap download "--$CORE_CHANNEL" core - "$TESTSLIB/reset.sh" --keep-stopped + #shellcheck source=tests/lib/reset.sh + . "$TESTSLIB/reset.sh" + reset_snapd --keep-stopped mkdir -p "$SEED_DIR/snaps" mkdir -p "$SEED_DIR/assertions" cat > "$SEED_DIR/seed.yaml" < Date: Tue, 27 Nov 2018 08:17:48 -0300 Subject: [PATCH 095/580] Fix classic-firstboot test using new reset --- tests/main/classic-firstboot/task.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/main/classic-firstboot/task.yaml b/tests/main/classic-firstboot/task.yaml index b5356f551ea..6e2777c24f6 100644 --- a/tests/main/classic-firstboot/task.yaml +++ b/tests/main/classic-firstboot/task.yaml @@ -14,7 +14,10 @@ prepare: | snap pack "$TESTSLIB/snaps/basic" snap download "--$CORE_CHANNEL" core - "$TESTSLIB/reset.sh" --keep-stopped + #shellcheck source=tests/lib/reset.sh + . "$TESTSLIB/reset.sh" + reset_snapd --keep-stopped + mkdir -p "$SEED_DIR/snaps" mkdir -p "$SEED_DIR/assertions" cat > "$SEED_DIR/seed.yaml" < Date: Tue, 27 Nov 2018 15:33:06 -0300 Subject: [PATCH 096/580] Update prepare/restore for unit test suite and minot updates on prepare --- spread.yaml | 30 +++++------------------------- tests/lib/prepare.sh | 12 ++++++------ 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/spread.yaml b/spread.yaml index dec1d4c8edd..191725d671c 100644 --- a/spread.yaml +++ b/spread.yaml @@ -659,33 +659,13 @@ suites: TRAVIS_TAG: "$(HOST: echo $TRAVIS_TAG)" COVERMODE: "$(HOST: echo $COVERMODE)" prepare: | - #shellcheck source=tests/lib/prepare.sh - . "$TESTSLIB"/prepare.sh - prepare_classic + "$TESTSLIB"/prepare-restore.sh --prepare-suite prepare-each: | - #shellcheck source=tests/lib/reset.sh - . "$TESTSLIB"/reset.sh - reset_snapd --reuse-core - #shellcheck source=tests/lib/prepare.sh - . "$TESTSLIB"/prepare.sh - prepare_each_classic + "$TESTSLIB"/prepare-restore.sh --prepare-suite-each + restore-each: | + "$TESTSLIB"/prepare-restore.sh --restore-suite-each restore: | - #shellcheck source=tests/lib/reset.sh - . "$TESTSLIB"/reset.sh - reset_snapd --store - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB"/pkgdb.sh - - distro_purge_package snapd - case "$SPREAD_SYSTEM" in - arch-*) - # there is no snap-confine and ubuntu-core-launcher - # in Arch - ;; - *) - distro_purge_package snap-confine ubuntu-core-launcher - ;; - esac + "$TESTSLIB"/prepare-restore.sh --restore-suite tests/nightly/: summary: Suite for nightly, expensive, tests diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 1ecc7f747be..765f723437b 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -234,7 +234,9 @@ prepare_classic() { fi # Snapshot the state including core. - if ! is_snapd_state_saved; then + if is_snapd_state_saved; then + init_state_classic --reuse-core + else # need to be seeded to proceed with snap install # also make sure the captured state is seeded snap wait system seed.loaded @@ -280,8 +282,6 @@ prepare_classic() { systemctl stop snapd.{service,socket} save_snapd_state systemctl start snapd.socket - else - init_state_classic --reuse-core fi disable_kernel_rate_limiting @@ -613,12 +613,12 @@ prepare_ubuntu_core() { setup_systemd_snapd_overrides # Snapshot the fresh state (including boot/bootenv) - if ! is_snapd_state_saved; then + if is_snapd_state_saved; then + init_state_all_snap --reuse-core + else systemctl stop snapd.service snapd.socket save_snapd_state systemctl start snapd.socket - else - init_state_all_snap --reuse-core fi disable_kernel_rate_limiting From d51f7e6d6b6c0191cdc6228da91f31fa206b2f4b Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 27 Nov 2018 17:10:17 -0300 Subject: [PATCH 097/580] Fix for failover test on how logs are checked Otherwise logs used are since last reboot. error: https://api.travis-ci.org/v3/job/460209244/log.txt --- tests/main/failover/task.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/main/failover/task.yaml b/tests/main/failover/task.yaml index f8f209397fc..7d2d7af9b99 100644 --- a/tests/main/failover/task.yaml +++ b/tests/main/failover/task.yaml @@ -68,6 +68,9 @@ execute: | . "$TESTSLIB"/names.sh #shellcheck source=tests/lib/boot.sh . "$TESTSLIB"/boot.sh + #shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB"/journalctl.sh + if [ "$TARGET_SNAP" = kernel ]; then TARGET_SNAP_NAME=$kernel_name else @@ -87,10 +90,17 @@ execute: | # repack new target snap snap pack "$BUILD_DIR/unpack" && mv ${TARGET_SNAP_NAME}_*.snap failing.snap + if check_journalctl_log "Waiting for system reboot"; then + echo "Already waiting for system reboot, exiting..." + exit 1 + fi + # install new target snap snap install --dangerous failing.snap - while ! journalctl -b -u snapd|grep -q "Waiting for system reboot" ; do sleep 1 ; done + while ! check_journalctl_log "Waiting for system reboot"; do + sleep 1 + done # check boot env vars readlink /snap/core/current > failBoot From a7ea32ae00f66030f177ff416cef85d77ca9b366 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 27 Nov 2018 18:33:35 -0300 Subject: [PATCH 098/580] Revert "Fix for failover test on how logs are checked" This reverts commit d51f7e6d6b6c0191cdc6228da91f31fa206b2f4b. --- tests/main/failover/task.yaml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/main/failover/task.yaml b/tests/main/failover/task.yaml index 7d2d7af9b99..f8f209397fc 100644 --- a/tests/main/failover/task.yaml +++ b/tests/main/failover/task.yaml @@ -68,9 +68,6 @@ execute: | . "$TESTSLIB"/names.sh #shellcheck source=tests/lib/boot.sh . "$TESTSLIB"/boot.sh - #shellcheck source=tests/lib/journalctl.sh - . "$TESTSLIB"/journalctl.sh - if [ "$TARGET_SNAP" = kernel ]; then TARGET_SNAP_NAME=$kernel_name else @@ -90,17 +87,10 @@ execute: | # repack new target snap snap pack "$BUILD_DIR/unpack" && mv ${TARGET_SNAP_NAME}_*.snap failing.snap - if check_journalctl_log "Waiting for system reboot"; then - echo "Already waiting for system reboot, exiting..." - exit 1 - fi - # install new target snap snap install --dangerous failing.snap - while ! check_journalctl_log "Waiting for system reboot"; do - sleep 1 - done + while ! journalctl -b -u snapd|grep -q "Waiting for system reboot" ; do sleep 1 ; done # check boot env vars readlink /snap/core/current > failBoot From 32a4892219098cd85ee95239c681a4d6761ad04c Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 28 Nov 2018 10:48:42 -0300 Subject: [PATCH 099/580] Reset the run the first test as the rest of the tests --- tests/lib/prepare.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 765f723437b..02c7e9acdfb 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -235,7 +235,7 @@ prepare_classic() { # Snapshot the state including core. if is_snapd_state_saved; then - init_state_classic --reuse-core + reset_snapd --reuse-core else # need to be seeded to proceed with snap install # also make sure the captured state is seeded @@ -614,7 +614,7 @@ prepare_ubuntu_core() { # Snapshot the fresh state (including boot/bootenv) if is_snapd_state_saved; then - init_state_all_snap --reuse-core + reset_snapd --reuse-core else systemctl stop snapd.service snapd.socket save_snapd_state From 53d7f81a40ea75fbeb341695259f7d4a37d78c3a Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 28 Nov 2018 16:41:04 +0100 Subject: [PATCH 100/580] overlord: don't write system key if security setup fails (2.36) Currently the snapd interface manager, when starting the overlord, computes and compares the system key with the one on disk. If the system key is absent or differs then all security profiles are re-generated. We found out that restarting systemd can kill snap-seccomp or apparmor-parser running in the same systemd service control group. We have observed system traces using forkstat and strace on pid 1 (systemd) that indicate that exact thing occurring. When a security profile regeneration fails it the failure is logged but otherwise ignored. This can mean that we have attempted to compile an apparmor or seccomp profile, failed, but carried on anyway and *wrote the system key*. When this happens, on the next startup of snapd the system key will match but the security setup will be incomplete. This patch changes that logic as follows. When system key mismatch is detected then the old system key is immediately unlinked. Profile re-generation proceeds. Errors are logged but they do not prevent snapd form starting up. This is done so that snapd can, if all else fails, refresh or revert itself. If any error occurs the system key is *not* written. This means that on next startup the procedure will be attempted again. The reason the system key is unlinked is to prevent snapd from believing that an old system key is valid and represents security setup established in the system. If snapd is reverted following a failed startup then system key may match the system key that used to be on disk but some of the system security may have been changed by the new snapd, the one that was reverted. Unlinking avoids such possibility, forcing old snapd to re-establish proper security view. Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index 21340f398d3..55a6a52bde5 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -23,9 +23,11 @@ import ( "bytes" "encoding/json" "fmt" + "os" "strings" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/backends" "github.com/snapcore/snapd/interfaces/builtin" @@ -163,6 +165,9 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { } } + writeSystemKey := true + os.Remove(dirs.SnapSystemKeyFile) + // For each snap: for _, snapInfo := range snaps { snapName := snapInfo.InstanceName() @@ -182,15 +187,18 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { } // Refresh security of this snap and backend if err := backend.Setup(snapInfo, opts, m.repo); err != nil { - // Let's log this but carry on + // Let's log this but carry on without writing the system key. logger.Noticef("cannot regenerate %s profile for snap %q: %s", backend.Name(), snapName, err) + writeSystemKey = false } } } - if err := interfaces.WriteSystemKey(); err != nil { - logger.Noticef("cannot write system key: %v", err) + if writeSystemKey { + if err := interfaces.WriteSystemKey(); err != nil { + logger.Noticef("cannot write system key: %v", err) + } } return nil } From cbc6e3e029e36e2d120a3dbb2edad39f689da479 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 28 Nov 2018 21:31:20 +0100 Subject: [PATCH 101/580] interfaces/apparmor: fail on snap-confine profile errors When the apparmor backend sets up security for the "core" or "snapd" snaps it performs additional special setup of the snap-confine security profile. This operation may fail, for example when apparmor_parser is killed by systemd or when apparmor_parser cannot handle the syntax we are using in our profiles. When this happens we used to just log the error and carry on anyway. This patch changes this behavior in agreement with the prior change of the system-key profile re-generation logic. Note that this patch will surface another error, one where openSUSE Leap 42.3, using apparmor_parser 2.10.3 cannot compile the profile for snap-confine at all. That issue is addressed separately and is tracked at https://bugs.launchpad.net/snapd/+bug/1805485 Signed-off-by: Zygmunt Krynicki --- interfaces/apparmor/backend.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index 196a8552dd8..716e440b742 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -309,7 +309,7 @@ func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, // TODO: we need to deal with the "snapd" snap here soon if snapName == "core" && release.OnClassic && release.AppArmorLevel() != release.NoAppArmor { if err := setupSnapConfineReexec(snapInfo); err != nil { - logger.Noticef("cannot create host snap-confine apparmor configuration: %s", err) + return fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) } } @@ -318,7 +318,7 @@ func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, // systems but /etc/apparmor.d is not writable on core18 systems if snapName == "snapd" && release.AppArmorLevel() != release.NoAppArmor { if err := setupSnapConfineReexec(snapInfo); err != nil { - logger.Noticef("cannot create host snap-confine apparmor configuration: %s", err) + return fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) } } From 15cae44908738b6c8e1814563c0cae727594a3d7 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 28 Nov 2018 23:17:00 +0100 Subject: [PATCH 102/580] spread: disable openSUSE Leap 42.3 openSUSE Leap 42.3 uses apparmor_parser version 2.10.3 which doesn't support the "unsafe" profile transition syntax required by snap-confine. change_profile unsafe /** -> [^u/]**, change_profile unsafe /** -> u[^n]**, change_profile unsafe /** -> un[^c]**, change_profile unsafe /** -> unc[^o]**, change_profile unsafe /** -> unco[^n]**, change_profile unsafe /** -> uncon[^f]**, change_profile unsafe /** -> unconf[^i]**, change_profile unsafe /** -> unconfi[^n]**, change_profile unsafe /** -> unconfin[^e]**, change_profile unsafe /** -> unconfine[^d]**, change_profile unsafe /** -> unconfined?**, Once the patches that handle this and disable the apparmor backend on suitably ancient version of apparmor are merged this patch should be reverted. Signed-off-by: Zygmunt Krynicki --- spread.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spread.yaml b/spread.yaml index 8cbd83227e1..8b09a1d2796 100644 --- a/spread.yaml +++ b/spread.yaml @@ -87,6 +87,8 @@ backends: - opensuse-42.3-64: workers: 4 + # Re-enable once "ancient" apparmor branches are merged. + manual: true - arch-linux-64: workers: 4 From 12e16ef8d1e702d9821142b84d6359414f4a16f3 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 30 Nov 2018 09:40:13 +0100 Subject: [PATCH 103/580] interfaces/backends: detect too old apparmor_parser (2.36) This patch changes logic conditionally adding the apparmor backend to not do so if the kernel has some support in the case that the userspace parser is too old to parse the profiles we generate. In practical terms this means we run without apparmor on openSUSE Leap 42.3 which in turn fixes a bug that prevented snapd from working correctly with stricter error reporting during startup phase when system key is considered. A simple spread test ensures that openSUSE Leap 42.3 is not using apparmor anymore. Fixes: https://bugs.launchpad.net/snapd/+bug/1805485 Signed-off-by: Zygmunt Krynicki --- interfaces/backends/backends.go | 6 +++++- interfaces/backends/backends_test.go | 14 +++++++++++--- tests/regression/lp-1805485/task.yaml | 10 ++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 tests/regression/lp-1805485/task.yaml diff --git a/interfaces/backends/backends.go b/interfaces/backends/backends.go index 311d41701dc..4dbb870193f 100644 --- a/interfaces/backends/backends.go +++ b/interfaces/backends/backends.go @@ -67,7 +67,11 @@ func backends() []interfaces.SecurityBackend { // When some features are missing the backend will generate more permissive // profiles that keep applications operational, in forced-devmode. switch release.AppArmorLevel() { - case release.FullAppArmor, release.PartialAppArmor: + case release.PartialAppArmor: + if len(release.AppArmorParserFeatures()) != 0 { + all = append(all, &apparmor.Backend{}) + } + case release.FullAppArmor: all = append(all, &apparmor.Backend{}) } return all diff --git a/interfaces/backends/backends_test.go b/interfaces/backends/backends_test.go index 17a8091ccd6..b8d71c351f5 100644 --- a/interfaces/backends/backends_test.go +++ b/interfaces/backends/backends_test.go @@ -45,12 +45,20 @@ func (s *backendsSuite) TestIsAppArmorEnabled(c *C) { for i, backend := range all { names[i] = string(backend.Name()) } - - if level == release.NoAppArmor { + switch level { + case release.NoAppArmor: c.Assert(names, Not(testutil.Contains), "apparmor") - } else { + case release.PartialAppArmor: + // Partial support depends on modern apparmor_parser. + if len(release.AppArmorParserFeatures()) == 0 { + c.Assert(names, Not(testutil.Contains), "apparmor") + } else { + c.Assert(names, testutil.Contains, "apparmor") + } + case release.FullAppArmor: c.Assert(names, testutil.Contains, "apparmor") } + } } diff --git a/tests/regression/lp-1805485/task.yaml b/tests/regression/lp-1805485/task.yaml new file mode 100644 index 00000000000..04fdcd87ad5 --- /dev/null +++ b/tests/regression/lp-1805485/task.yaml @@ -0,0 +1,10 @@ +summary: apparmor backend is not used on openSUSE Leap 42.3 +details: | + AppArmor on openSUSE Leap 43.2 is too old to parse the snap-confine + apparmor profile. Because of "relaxed" error handling snapd was enabling + apparmor backend even though it resulted in constant problems with setting + up security for the core snap. With this bug fixed the old version of + apparmor_parser makes snapd disable the relevant security backend. +systems: [opensuse-42.3-64] +execute: | + snap debug sandbox-features | MATCH -v "^apparmor:" From 48ade9554c8612094a1c680d35b49b6143e403ad Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 30 Nov 2018 09:40:13 +0100 Subject: [PATCH 104/580] interfaces/backends: detect too old apparmor_parser (2.36) This patch changes logic conditionally adding the apparmor backend to not do so if the kernel has some support in the case that the userspace parser is too old to parse the profiles we generate. In practical terms this means we run without apparmor on openSUSE Leap 42.3 which in turn fixes a bug that prevented snapd from working correctly with stricter error reporting during startup phase when system key is considered. A simple spread test ensures that openSUSE Leap 42.3 is not using apparmor anymore. Fixes: https://bugs.launchpad.net/snapd/+bug/1805485 Signed-off-by: Zygmunt Krynicki --- interfaces/backends/backends.go | 6 +++++- interfaces/backends/backends_test.go | 14 +++++++++++--- tests/regression/lp-1805485/task.yaml | 10 ++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 tests/regression/lp-1805485/task.yaml diff --git a/interfaces/backends/backends.go b/interfaces/backends/backends.go index 311d41701dc..4dbb870193f 100644 --- a/interfaces/backends/backends.go +++ b/interfaces/backends/backends.go @@ -67,7 +67,11 @@ func backends() []interfaces.SecurityBackend { // When some features are missing the backend will generate more permissive // profiles that keep applications operational, in forced-devmode. switch release.AppArmorLevel() { - case release.FullAppArmor, release.PartialAppArmor: + case release.PartialAppArmor: + if len(release.AppArmorParserFeatures()) != 0 { + all = append(all, &apparmor.Backend{}) + } + case release.FullAppArmor: all = append(all, &apparmor.Backend{}) } return all diff --git a/interfaces/backends/backends_test.go b/interfaces/backends/backends_test.go index 17a8091ccd6..b8d71c351f5 100644 --- a/interfaces/backends/backends_test.go +++ b/interfaces/backends/backends_test.go @@ -45,12 +45,20 @@ func (s *backendsSuite) TestIsAppArmorEnabled(c *C) { for i, backend := range all { names[i] = string(backend.Name()) } - - if level == release.NoAppArmor { + switch level { + case release.NoAppArmor: c.Assert(names, Not(testutil.Contains), "apparmor") - } else { + case release.PartialAppArmor: + // Partial support depends on modern apparmor_parser. + if len(release.AppArmorParserFeatures()) == 0 { + c.Assert(names, Not(testutil.Contains), "apparmor") + } else { + c.Assert(names, testutil.Contains, "apparmor") + } + case release.FullAppArmor: c.Assert(names, testutil.Contains, "apparmor") } + } } diff --git a/tests/regression/lp-1805485/task.yaml b/tests/regression/lp-1805485/task.yaml new file mode 100644 index 00000000000..04fdcd87ad5 --- /dev/null +++ b/tests/regression/lp-1805485/task.yaml @@ -0,0 +1,10 @@ +summary: apparmor backend is not used on openSUSE Leap 42.3 +details: | + AppArmor on openSUSE Leap 43.2 is too old to parse the snap-confine + apparmor profile. Because of "relaxed" error handling snapd was enabling + apparmor backend even though it resulted in constant problems with setting + up security for the core snap. With this bug fixed the old version of + apparmor_parser makes snapd disable the relevant security backend. +systems: [opensuse-42.3-64] +execute: | + snap debug sandbox-features | MATCH -v "^apparmor:" From 46864627343164f7cfd3b2ea1bfecb044cea6a4c Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 30 Nov 2018 11:57:55 +0100 Subject: [PATCH 105/580] Revert "spread: disable openSUSE Leap 42.3" This reverts commit 15cae44908738b6c8e1814563c0cae727594a3d7. --- spread.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/spread.yaml b/spread.yaml index 8b09a1d2796..8cbd83227e1 100644 --- a/spread.yaml +++ b/spread.yaml @@ -87,8 +87,6 @@ backends: - opensuse-42.3-64: workers: 4 - # Re-enable once "ancient" apparmor branches are merged. - manual: true - arch-linux-64: workers: 4 From d3bcd1a147e2f9cadf21fb4c3241f6ad38a6b24b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 30 Nov 2018 13:05:10 +0100 Subject: [PATCH 106/580] tests: check that apparmor is not disabled on some systems Signed-off-by: Zygmunt Krynicki --- tests/regression/lp-1805485/task.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/regression/lp-1805485/task.yaml b/tests/regression/lp-1805485/task.yaml index 04fdcd87ad5..3069adf440f 100644 --- a/tests/regression/lp-1805485/task.yaml +++ b/tests/regression/lp-1805485/task.yaml @@ -5,6 +5,11 @@ details: | apparmor backend even though it resulted in constant problems with setting up security for the core snap. With this bug fixed the old version of apparmor_parser makes snapd disable the relevant security backend. -systems: [opensuse-42.3-64] +systems: [ubuntu-*, opensuse-*, arch-linux-*] execute: | - snap debug sandbox-features | MATCH -v "^apparmor:" + if [[ "$SPREAD_SYSTEM" == opensuse-42.3* ]]; then + # openSUSE 43.2 has AppArmor parser in version too old to parse snap-confine profile + snap debug sandbox-features | MATCH -v "^apparmor:" + else + snap debug sandbox-features | MATCH "^apparmor:" + fi From d5bd330b86898093b5128a84df384f73aaf907ff Mon Sep 17 00:00:00 2001 From: Samuele Pedroni Date: Fri, 30 Nov 2018 17:51:21 +0100 Subject: [PATCH 107/580] refactor shared precond checks, add test --- overlord/snapstate/snapstate.go | 35 ++++++++++++---------------- overlord/snapstate/snapstate_test.go | 9 +++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 846d3ce62c3..a4415788855 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -522,6 +522,16 @@ func validateFeatureFlags(st *state.State, info *snap.Info) error { return nil } +func checkInstallPreconditions(st *state.State, info *snap.Info, flags Flags, snapst *SnapState) error { + if err := validateInfoAndFlags(info, snapst, flags); err != nil { + return err + } + if err := validateFeatureFlags(st, info); err != nil { + return err + } + return nil +} + // InstallPath returns a set of tasks for installing a snap from a file path // and the snap.Info for the given snap. // @@ -581,10 +591,7 @@ func InstallPath(st *state.State, si *snap.SideInfo, path, instanceName, channel } info.InstanceKey = instanceKey - if err := validateInfoAndFlags(info, &snapst, flags); err != nil { - return nil, nil, err - } - if err := validateFeatureFlags(st, info); err != nil { + if err := checkInstallPreconditions(st, info, flags, &snapst); err != nil { return nil, nil, err } @@ -639,10 +646,7 @@ func Install(st *state.State, name, channel string, revision snap.Revision, user return nil, err } - if err := validateInfoAndFlags(info, &snapst, flags); err != nil { - return nil, err - } - if err := validateFeatureFlags(st, info); err != nil { + if err := checkInstallPreconditions(st, info, flags, &snapst); err != nil { return nil, err } @@ -693,10 +697,7 @@ func InstallMany(st *state.State, names []string, userID int) ([]string, []*stat var snapst SnapState var flags Flags - if err := validateInfoAndFlags(info, &snapst, flags); err != nil { - return nil, nil, err - } - if err := validateFeatureFlags(st, info); err != nil { + if err := checkInstallPreconditions(st, info, flags, &snapst); err != nil { return nil, nil, err } @@ -842,14 +843,8 @@ func doUpdate(ctx context.Context, st *state.State, names []string, updates []*s for _, update := range updates { channel, flags, snapst := params(update) flags.IsAutoRefresh = globalFlags.IsAutoRefresh - if err := validateInfoAndFlags(update, snapst, flags); err != nil { - if refreshAll { - logger.Noticef("cannot update %q: %v", update.InstanceName(), err) - continue - } - return nil, nil, err - } - if err := validateFeatureFlags(st, update); err != nil { + + if err := checkInstallPreconditions(st, update, flags, snapst); err != nil { if refreshAll { logger.Noticef("cannot update %q: %v", update.InstanceName(), err) continue diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index cad953cef9e..69a5570de08 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -10262,6 +10262,15 @@ func (s *snapmgrTestSuite) TestInstallMany(c *C) { } } +func (s *snapmgrTestSuite) TestInstallManyChecksPreconditions(c *C) { + s.state.Lock() + defer s.state.Unlock() + + _, _, err := snapstate.InstallMany(s.state, []string{"some-snap-now-classic"}, 0) + c.Assert(err, NotNil) + c.Check(err, DeepEquals, &snapstate.SnapNeedsClassicError{Snap: "some-snap-now-classic"}) +} + func verifyStopReason(c *C, ts *state.TaskSet, reason string) { tl := tasksWithKind(ts, "stop-snap-services") c.Check(tl, HasLen, 1) From 7b336b44d7449ecd55110acb01f1bc8d17e35e6f Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 30 Nov 2018 14:45:04 -0300 Subject: [PATCH 108/580] Unify init and clean as it is not needed to have independent methods anymore --- tests/lib/reset.sh | 95 +++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 0a5d127857b..3be2b442883 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -12,44 +12,7 @@ #shellcheck source=tests/lib/systems.sh . "$TESTSLIB/systems.sh" -init_state_classic() { - if [ "$1" = "--reuse-core" ]; then - # Restore snapd state and start systemd service units - restore_snapd_state - escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" - all_units="$(systemctl list-unit-files --full | cut -f1 -d ' ')" - mounts="" - if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount"; then - mounts="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount")" - fi - services="" - if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service"; then - services="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service")" - fi - systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ - for unit in $mounts $services; do - systemctl start "$unit" - done - - # force all profiles to be re-generated - rm -f /var/lib/snapd/system-key - fi - - if [ "$1" != "--keep-stopped" ]; then - systemctl start snapd.socket - - # wait for snapd listening - EXTRA_NC_ARGS="-q 1" - case "$SPREAD_SYSTEM" in - fedora-*|amazon-*|centos-*) - EXTRA_NC_ARGS="" - ;; - esac - while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done - fi -} - -clean_classic() { +reset_classic() { # Reload all service units as in some situations the unit might # have changed on the disk. systemctl daemon-reload @@ -102,19 +65,43 @@ clean_classic() { rm -rf /root/.snap/gnupg rm -f /tmp/core* /tmp/ubuntu-core* -} -init_state_all_snap() { - # ensure we have the same state as initially - systemctl stop snapd.service snapd.socket - restore_snapd_state - rm -rf /root/.snap - if [ "$1" != "--keep-stopped" ]; then - systemctl start snapd.service snapd.socket + if [ "$1" = "--reuse-core" ]; then + # Restore snapd state and start systemd service units + restore_snapd_state + escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" + all_units="$(systemctl list-unit-files --full | cut -f1 -d ' ')" + mounts="" + if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount"; then + mounts="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount")" + fi + services="" + if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service"; then + services="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service")" + fi + systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ + for unit in $mounts $services; do + systemctl start "$unit" + done + + # force all profiles to be re-generated + rm -f /var/lib/snapd/system-key fi + + if [ "$1" != "--keep-stopped" ]; then + systemctl start snapd.socket + + # wait for snapd listening + EXTRA_NC_ARGS="-q 1" + case "$SPREAD_SYSTEM" in + fedora-*|amazon-*|centos-*) + EXTRA_NC_ARGS="" + ;; + esac + while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done } -clean_all_snap() { +reset_all_snap() { # remove all leftover snaps # shellcheck source=tests/lib/names.sh . "$TESTSLIB/names.sh" @@ -145,6 +132,14 @@ clean_all_snap() { if [ -n "$remove_bases" ]; then snap remove "$remove_bases" fi + + # ensure we have the same state as initially + systemctl stop snapd.service snapd.socket + restore_snapd_state + rm -rf /root/.snap + if [ "$1" != "--keep-stopped" ]; then + systemctl start snapd.service snapd.socket + fi } discard_ns() { @@ -170,13 +165,11 @@ tear_down_store() { reset_snapd() { if is_core_system; then - clean_all_snap "$@" - init_state_all_snap "$@" + reset_all_snap "$@" discard_ns tear_down_store "$@" else - clean_classic "$@" - init_state_classic "$@" + reset_classic "$@" discard_ns tear_down_store "$@" fi From 370d45958dfa14fb7c02a09ebd2acb22b94860a7 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Fri, 30 Nov 2018 15:17:34 -0300 Subject: [PATCH 109/580] Fix code error on reset.sh --- tests/lib/reset.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 3be2b442883..4e44665396d 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -98,7 +98,10 @@ reset_classic() { EXTRA_NC_ARGS="" ;; esac - while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done + while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do + sleep 0.5 + done + fi } reset_all_snap() { From 94d4452fced6f7d25ba4a2804d584d3d55b0a560 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 3 Dec 2018 15:26:08 +0100 Subject: [PATCH 110/580] snap: add new `snap run --trace-exec` call (#6185) * snap: add new `snap run --perf` call This new argument will allow to trace what is exec()ed when a snap runs. This allows to e.g. run `snap run vlc --play-and-exit nothing` to see how much overhead the various wrappers generate. Sample output: ``` $ snap run --perf test-snapd-tools.echo hello Slowest 2 exec calls during snap run: 0.011: /usr/lib/snapd/snap-confine 0.016: /usr/lib/snapd/snap-exec ``` * snap: fix go vet warning * snap: add TestStraceExtractExecRuntime test * snap: handle execveat() and fix review issues * snap: show total runtime * snap: rename exec tracing to `snap run --trace-exec` * snap: fix silly comment out line * snap: refactor straceExtractExecRuntime and sort descending * add test for TestDisplaySortedExecRuntimes * snap: keep only nSlowestSamples in SnapTrace * snap: show slowest calls in order of them getting called * snap: use named pipe for strace log to avoid spaming the disk * snap: address some review feedback (thanks to Samuele) * snap: add test TestRunCmdWithTraceExecUnhappy * snap,osutil: add new osutil/strace package This extract the strace usage in cmd/snap/cmd_run.go into a new osutil/strace package for better separation and testing. * snap,osutil: move all timing related helpers to the new strace pkg * strace: simplify code * strace: improve comments for TraceExecveTimings * osutil/strace: add new TraceExecCommand() helper * strace: improve comments in runCmdWithTraceExec() * strace: address review feedback * strace: fix imports ordering --- cmd/snap/cmd_run.go | 132 ++++++++++--------- cmd/snap/cmd_run_test.go | 23 ++++ osutil/strace/export_test.go | 32 +++++ osutil/strace/strace.go | 93 ++++++++++++++ osutil/strace/strace_test.go | 118 +++++++++++++++++ osutil/strace/timing.go | 233 ++++++++++++++++++++++++++++++++++ osutil/strace/timing_test.go | 137 ++++++++++++++++++++ tests/main/snap-run/task.yaml | 5 + testutil/exec.go | 5 + 9 files changed, 719 insertions(+), 59 deletions(-) create mode 100644 osutil/strace/export_test.go create mode 100644 osutil/strace/strace.go create mode 100644 osutil/strace/strace_test.go create mode 100644 osutil/strace/timing.go create mode 100644 osutil/strace/timing_test.go diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index 0e0ff0a4f8f..f272fdd0c7d 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -43,6 +43,7 @@ import ( "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/strace" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" "github.com/snapcore/snapd/strutil/shlex" @@ -67,8 +68,9 @@ type cmdRun struct { // This options is both a selector (use or don't use strace) and it // can also carry extra options for strace. This is why there is // "default" and "optional-value" to distinguish this. - Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` - Gdb bool `long:"gdb"` + Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` + Gdb bool `long:"gdb"` + TraceExec bool `long:"trace-exec"` // not a real option, used to check if cmdRun is initialized by // the parser @@ -99,7 +101,9 @@ and environment. // TRANSLATORS: This should not start with a lowercase letter. "gdb": i18n.G("Run the command with gdb"), // TRANSLATORS: This should not start with a lowercase letter. - "timer": i18n.G("Run as a timer service with given schedule"), + "timer": i18n.G("Run as a timer service with given schedule"), + // TRANSLATORS: This should not start with a lowercase letter. + "trace-exec": i18n.G("Display exec calls timing data"), "parser-ran": "", }, nil) } @@ -645,49 +649,6 @@ func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { return nil } -func straceCmd() ([]string, error) { - current, err := user.Current() - if err != nil { - return nil, err - } - sudoPath, err := exec.LookPath("sudo") - if err != nil { - return nil, fmt.Errorf("cannot use strace without sudo: %s", err) - } - - // Try strace from the snap first, we use new syscalls like - // "_newselect" that are known to not work with the strace of e.g. - // ubuntu 14.04. - // - // TODO: some architectures do not have some syscalls (e.g. - // s390x does not have _newselect). In - // https://github.com/strace/strace/issues/57 options are - // discussed. We could use "-e trace=?syscall" but that is - // only available since strace 4.17 which is not even in - // ubutnu 17.10. - var stracePath string - cand := filepath.Join(dirs.SnapMountDir, "strace-static", "current", "bin", "strace") - if osutil.FileExists(cand) { - stracePath = cand - } - if stracePath == "" { - stracePath, err = exec.LookPath("strace") - if err != nil { - return nil, fmt.Errorf("cannot find an installed strace, please try 'snap install strace-static'") - } - } - - return []string{ - sudoPath, "-E", - stracePath, - "-u", current.Username, - "-f", - // these syscalls are excluded because they make strace hang - // on all or some architectures (gettimeofday on arm64) - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", - }, nil -} - func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { env = append(env, "SNAP_CONFINE_RUN_UNDER_GDB=1") @@ -702,25 +663,76 @@ func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { return gcmd.Run() } +func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error { + // setup private tmp dir with strace fifo + straceTmp, err := ioutil.TempDir("", "exec-trace") + if err != nil { + return err + } + defer os.RemoveAll(straceTmp) + straceLog := filepath.Join(straceTmp, "strace.fifo") + if err := syscall.Mkfifo(straceLog, 0640); err != nil { + return err + } + // ensure we have one writer on the fifo so that if strace fails + // nothing blocks + fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640) + if err != nil { + return err + } + defer fw.Close() + + // read strace data from fifo async + var slg *strace.ExecveTiming + var straceErr error + doneCh := make(chan bool, 1) + go func() { + // FIXME: make this configurable? + nSlowest := 10 + slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest) + close(doneCh) + }() + + cmd, err := strace.TraceExecCommand(straceLog, origCmd...) + if err != nil { + return err + } + // run + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + cmd.Stderr = Stderr + err = cmd.Run() + // ensure we close the fifo here so that the strace.TraceExecCommand() + // helper gets a EOF from the fifo (i.e. all writers must be closed + // for this) + fw.Close() + + // wait for strace reader + <-doneCh + if straceErr == nil { + slg.Display(Stderr) + } else { + logger.Noticef("cannot extract runtime data: %v", straceErr) + } + return err +} + func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { - // prepend strace magic - cmd, err := straceCmd() + extraStraceOpts, raw, err := x.straceOpts() if err != nil { return err } - straceOpts, raw, err := x.straceOpts() + cmd, err := strace.Command(extraStraceOpts, origCmd...) if err != nil { return err } - cmd = append(cmd, straceOpts...) - cmd = append(cmd, origCmd...) // run with filter - gcmd := exec.Command(cmd[0], cmd[1:]...) - gcmd.Env = env - gcmd.Stdin = Stdin - gcmd.Stdout = Stdout - stderr, err := gcmd.StderrPipe() + cmd.Env = env + cmd.Stdin = Stdin + cmd.Stdout = Stdout + stderr, err := cmd.StderrPipe() if err != nil { return err } @@ -784,11 +796,11 @@ func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { } io.Copy(Stderr, r) }() - if err := gcmd.Start(); err != nil { + if err := cmd.Start(); err != nil { return err } <-filterDone - err = gcmd.Wait() + err = cmd.Wait() return err } @@ -883,7 +895,9 @@ func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook stri } env := snapenv.ExecEnv(info, extraEnv) - if x.Gdb { + if x.TraceExec { + return x.runCmdWithTraceExec(cmd, env) + } else if x.Gdb { return x.runCmdUnderGdb(cmd, env) } else if x.useStrace() { return x.runCmdUnderStrace(cmd, env) diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index 1f0fe2b80df..ce39da62eed 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -907,3 +907,26 @@ func (s *SnapSuite) TestSnapRunAppTimer(c *check.C) { filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) } + +func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("1"), + }) + + // pretend we have sudo + sudoCmd := testutil.MockCommand(c, "sudo", "echo unhappy; exit 12") + defer sudoCmd.Restore() + + // pretend we have strace + straceCmd := testutil.MockCommand(c, "strace", "") + defer straceCmd.Restore() + + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--trace-exec", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, "exit status 12") + c.Assert(rest, check.DeepEquals, []string{"--", "snapname.app", "--arg1", "arg2"}) + c.Check(s.Stdout(), check.Equals, "unhappy\n") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/osutil/strace/export_test.go b/osutil/strace/export_test.go new file mode 100644 index 00000000000..ac065ea5933 --- /dev/null +++ b/osutil/strace/export_test.go @@ -0,0 +1,32 @@ +// -*- 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 . + * + */ + +package strace + +var ( + ExcludedSyscalls = excludedSyscalls +) + +func (stt *ExecveTiming) ExeRuntimes() []ExeRuntime { + return stt.exeRuntimes +} + +func (stt *ExecveTiming) AddExeRuntime(exe string, totalSec float64) { + stt.addExeRuntime(exe, totalSec) +} diff --git a/osutil/strace/strace.go b/osutil/strace/strace.go new file mode 100644 index 00000000000..540d07b4904 --- /dev/null +++ b/osutil/strace/strace.go @@ -0,0 +1,93 @@ +// -*- 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 . + * + */ + +package strace + +import ( + "fmt" + "os/exec" + "os/user" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// These syscalls are excluded because they make strace hang on all or +// some architectures (gettimeofday on arm64). +var excludedSyscalls = "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep" + +// Command returns how to run strace in the users context with the +// right set of excluded system calls. +func Command(extraStraceOpts []string, traceeCmd ...string) (*exec.Cmd, error) { + current, err := user.Current() + if err != nil { + return nil, err + } + sudoPath, err := exec.LookPath("sudo") + if err != nil { + return nil, fmt.Errorf("cannot use strace without sudo: %s", err) + } + + // Try strace from the snap first, we use new syscalls like + // "_newselect" that are known to not work with the strace of e.g. + // ubuntu 14.04. + // + // TODO: some architectures do not have some syscalls (e.g. + // s390x does not have _newselect). In + // https://github.com/strace/strace/issues/57 options are + // discussed. We could use "-e trace=?syscall" but that is + // only available since strace 4.17 which is not even in + // ubutnu 17.10. + var stracePath string + cand := filepath.Join(dirs.SnapMountDir, "strace-static", "current", "bin", "strace") + if osutil.FileExists(cand) { + stracePath = cand + } + if stracePath == "" { + stracePath, err = exec.LookPath("strace") + if err != nil { + return nil, fmt.Errorf("cannot find an installed strace, please try 'snap install strace-static'") + } + } + + args := []string{ + sudoPath, + "-E", + stracePath, + "-u", current.Username, + "-f", + "-e", excludedSyscalls, + } + args = append(args, extraStraceOpts...) + args = append(args, traceeCmd...) + + return &exec.Cmd{ + Path: sudoPath, + Args: args, + }, nil +} + +// TraceExecCommand returns an exec.Cmd suitable for tracking timings of +// execve{,at}() calls +func TraceExecCommand(straceLogPath string, origCmd ...string) (*exec.Cmd, error) { + extraStraceOpts := []string{"-ttt", "-e", "trace=execve,execveat", "-o", fmt.Sprintf("%s", straceLogPath)} + + return Command(extraStraceOpts, origCmd...) +} diff --git a/osutil/strace/strace_test.go b/osutil/strace/strace_test.go new file mode 100644 index 00000000000..bec7bb25dcb --- /dev/null +++ b/osutil/strace/strace_test.go @@ -0,0 +1,118 @@ +// -*- 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 . + * + */ + +package strace_test + +import ( + "io/ioutil" + "os" + "os/user" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil/strace" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type straceSuite struct { + rootdir string + mockSudo *testutil.MockCmd + mockStrace *testutil.MockCmd +} + +var _ = Suite(&straceSuite{}) + +func (s *straceSuite) SetUpTest(c *C) { + s.rootdir = c.MkDir() + dirs.SetRootDir(s.rootdir) + + s.mockSudo = testutil.MockCommand(c, "sudo", "") + s.mockStrace = testutil.MockCommand(c, "strace", "") +} + +func (s *straceSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") + s.mockSudo.Restore() + s.mockStrace.Restore() +} + +func (s *straceSuite) TestStraceCommandHappy(c *C) { + u, err := user.Current() + c.Assert(err, IsNil) + + cmd, err := strace.Command(nil, "foo") + c.Assert(err, IsNil) + c.Assert(cmd.Path, Equals, s.mockSudo.Exe()) + c.Assert(cmd.Args, DeepEquals, []string{ + s.mockSudo.Exe(), "-E", + s.mockStrace.Exe(), "-u", u.Username, "-f", + "-e", strace.ExcludedSyscalls, + // the command + "foo", + }) +} + +func (s *straceSuite) TestStraceCommandNoSudo(c *C) { + origPath := os.Getenv("PATH") + defer func() { os.Setenv("PATH", origPath) }() + + os.Setenv("PATH", "/not-exists") + _, err := strace.Command(nil, "foo") + c.Assert(err, ErrorMatches, `cannot use strace without sudo: exec: "sudo": executable file not found in \$PATH`) +} + +func (s *straceSuite) TestStraceCommandNoStrace(c *C) { + origPath := os.Getenv("PATH") + defer func() { os.Setenv("PATH", origPath) }() + + tmp := c.MkDir() + os.Setenv("PATH", tmp) + err := ioutil.WriteFile(filepath.Join(tmp, "sudo"), nil, 0755) + c.Assert(err, IsNil) + + _, err = strace.Command(nil, "foo") + c.Assert(err, ErrorMatches, `cannot find an installed strace, please try 'snap install strace-static'`) +} + +func (s *straceSuite) TestTraceExecCommand(c *C) { + u, err := user.Current() + c.Assert(err, IsNil) + + cmd, err := strace.TraceExecCommand("/run/snapd/strace.log", "cmd") + c.Assert(err, IsNil) + c.Assert(cmd.Path, Equals, s.mockSudo.Exe()) + c.Assert(cmd.Args, DeepEquals, []string{ + s.mockSudo.Exe(), "-E", + s.mockStrace.Exe(), "-u", u.Username, "-f", + "-e", strace.ExcludedSyscalls, + // timing specific trace + "-ttt", + "-e", "trace=execve,execveat", + "-o", "/run/snapd/strace.log", + // the command + "cmd", + }) + +} diff --git a/osutil/strace/timing.go b/osutil/strace/timing.go new file mode 100644 index 00000000000..1ffd6ca5ee1 --- /dev/null +++ b/osutil/strace/timing.go @@ -0,0 +1,233 @@ +// -*- 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 . + * + */ + +package strace + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strconv" +) + +// ExeRuntime is the runtime of an individual executable +type ExeRuntime struct { + Exe string + // FIXME: move to time.Duration + TotalSec float64 +} + +// ExecveTiming measures the execve calls timings under strace. This is +// useful for performance analysis. It keeps the N slowest samples. +type ExecveTiming struct { + TotalTime float64 + exeRuntimes []ExeRuntime + + nSlowestSamples int +} + +// NewExecveTiming returns a new ExecveTiming struct that keeps +// the given amount of the slowest exec samples. +func NewExecveTiming(nSlowestSamples int) *ExecveTiming { + return &ExecveTiming{nSlowestSamples: nSlowestSamples} +} + +func (stt *ExecveTiming) addExeRuntime(exe string, totalSec float64) { + stt.exeRuntimes = append(stt.exeRuntimes, ExeRuntime{ + Exe: exe, + TotalSec: totalSec, + }) + stt.prune() +} + +// prune() ensures the number of exeRuntimes stays with the nSlowestSamples +// limit +func (stt *ExecveTiming) prune() { + for len(stt.exeRuntimes) > stt.nSlowestSamples { + fastest := 0 + for idx, rt := range stt.exeRuntimes { + if rt.TotalSec < stt.exeRuntimes[fastest].TotalSec { + fastest = idx + } + } + // delete fastest element + stt.exeRuntimes = append(stt.exeRuntimes[:fastest], stt.exeRuntimes[fastest+1:]...) + } +} + +func (stt *ExecveTiming) Display(w io.Writer) { + if len(stt.exeRuntimes) == 0 { + return + } + fmt.Fprintf(w, "Slowest %d exec calls during snap run:\n", len(stt.exeRuntimes)) + for _, rt := range stt.exeRuntimes { + fmt.Fprintf(w, " %2.3fs %s\n", rt.TotalSec, rt.Exe) + } + fmt.Fprintf(w, "Total time: %2.3fs\n", stt.TotalTime) +} + +type exeStart struct { + start float64 + exe string +} + +type pidTracker struct { + pidToExeStart map[string]exeStart +} + +func newPidTracker() *pidTracker { + return &pidTracker{ + pidToExeStart: make(map[string]exeStart), + } + +} + +func (pt *pidTracker) Get(pid string) (startTime float64, exe string) { + if exeStart, ok := pt.pidToExeStart[pid]; ok { + return exeStart.start, exeStart.exe + } + return 0, "" +} + +func (pt *pidTracker) Add(pid string, startTime float64, exe string) { + pt.pidToExeStart[pid] = exeStart{start: startTime, exe: exe} +} + +func (pt *pidTracker) Del(pid string) { + delete(pt.pidToExeStart, pid) +} + +// lines look like: +// PID TIME SYSCALL +// 17363 1542815326.700248 execve("/snap/brave/44/usr/bin/update-mime-database", ["update-mime-database", "/home/egon/snap/brave/44/.local/"...], 0x1566008 /* 69 vars */) = 0 +var execveRE = regexp.MustCompile(`([0-9]+)\ +([0-9.]+) execve\(\"([^"]+)\"`) + +// lines look like: +// PID TIME SYSCALL +// 14157 1542875582.816782 execveat(3, "", ["snap-update-ns", "--from-snap-confine", "test-snapd-tools"], 0x7ffce7dd6160 /* 0 vars */, AT_EMPTY_PATH) = 0 +var execveatRE = regexp.MustCompile(`([0-9]+)\ +([0-9.]+) execveat\(.*\["([^"]+)"`) + +// lines look like (both SIGTERM and SIGCHLD need to be handled): +// PID TIME SIGNAL +// 17559 1542815330.242750 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=17643, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- +var sigChldTermRE = regexp.MustCompile(`[0-9]+\ +([0-9.]+).*SIG(CHLD|TERM)\ {.*si_pid=([0-9]+),`) + +// all lines start with this: +// PID TIME +// 21616 1542882400.198907 .... +var timeRE = regexp.MustCompile(`[0-9]+\ +([0-9.]+).*`) + +func handleExecMatch(trace *ExecveTiming, pt *pidTracker, match []string) error { + if len(match) == 0 { + return nil + } + // the pid of the process that does the execve{,at}() + pid := match[1] + execStart, err := strconv.ParseFloat(match[2], 64) + if err != nil { + return err + } + exe := match[3] + // deal with subsequent execve() + if start, exe := pt.Get(pid); exe != "" { + trace.addExeRuntime(exe, execStart-start) + } + pt.Add(pid, execStart, exe) + return nil +} + +func handleSignalMatch(trace *ExecveTiming, pt *pidTracker, match []string) error { + if len(match) == 0 { + return nil + } + sigTime, err := strconv.ParseFloat(match[1], 64) + if err != nil { + return err + } + sigPid := match[3] + if start, exe := pt.Get(sigPid); exe != "" { + trace.addExeRuntime(exe, sigTime-start) + pt.Del(sigPid) + } + return nil +} + +func TraceExecveTimings(straceLog string, nSlowest int) (*ExecveTiming, error) { + slog, err := os.Open(straceLog) + if err != nil { + return nil, err + } + defer slog.Close() + + // pidTracker maps the "pid" string to the executable + pidTracker := newPidTracker() + + var line string + var start, end, tmp float64 + trace := NewExecveTiming(nSlowest) + r := bufio.NewScanner(slog) + for r.Scan() { + line = r.Text() + if start == 0.0 { + if _, err := fmt.Sscanf(line, "%f %f ", &tmp, &start); err != nil { + return nil, fmt.Errorf("cannot parse start of exec profile: %s", err) + } + } + // handleExecMatch looks for execve{,at}() calls and + // uses the pidTracker to keep track of execution of + // things. Because of fork() we may see many pids and + // within each pid we can see multiple execve{,at}() + // calls. + // An example of pids/exec transitions: + // $ snap run --trace-exec test-snapd-sh -c "/bin/true" + // pid 20817 execve("snap-confine") + // pid 20817 execve("snap-exec") + // pid 20817 execve("/snap/test-snapd-sh/x2/bin/sh") + // pid 20817 execve("/bin/sh") + // pid 2023 execve("/bin/true") + match := execveRE.FindStringSubmatch(line) + if err := handleExecMatch(trace, pidTracker, match); err != nil { + return nil, err + } + match = execveatRE.FindStringSubmatch(line) + if err := handleExecMatch(trace, pidTracker, match); err != nil { + return nil, err + } + // handleSignalMatch looks for SIG{CHLD,TERM} signals and + // maps them via the pidTracker to the execve{,at}() calls + // of the terminating PID to calculate the total time of + // a execve{,at}() call. + match = sigChldTermRE.FindStringSubmatch(line) + if err := handleSignalMatch(trace, pidTracker, match); err != nil { + return nil, err + } + } + if _, err := fmt.Sscanf(line, "%f %f", &tmp, &end); err != nil { + return nil, fmt.Errorf("cannot parse end of exec profile: %s", err) + } + trace.TotalTime = end - start + + if r.Err() != nil { + return nil, r.Err() + } + + return trace, nil +} diff --git a/osutil/strace/timing_test.go b/osutil/strace/timing_test.go new file mode 100644 index 00000000000..2fb1b28866e --- /dev/null +++ b/osutil/strace/timing_test.go @@ -0,0 +1,137 @@ +// -*- 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 . + * + */ + +package strace_test + +import ( + "bytes" + "io/ioutil" + "os" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil/strace" +) + +type timingSuite struct{} + +var _ = Suite(&timingSuite{}) + +func (s *timingSuite) TestNewExecveTiming(c *C) { + et := strace.NewExecveTiming(10) + c.Assert(et, FitsTypeOf, &strace.ExecveTiming{}) +} + +func (s *timingSuite) TestDisplayExeRuntimes(c *C) { + // setup mock traces + stt := strace.NewExecveTiming(3) + stt.TotalTime = 2.71828 + stt.AddExeRuntime("slow", 1.0001) + stt.AddExeRuntime("fast", 0.1002) + stt.AddExeRuntime("really-fast", 0.0301) + stt.AddExeRuntime("medium", 0.5003) + + // display works and shows the slowest 3 calls in order + buf := bytes.NewBuffer(nil) + stt.Display(buf) + c.Check(buf.String(), Equals, `Slowest 3 exec calls during snap run: + 1.000s slow + 0.100s fast + 0.500s medium +Total time: 2.718s +`) +} + +func (s *timingSuite) TestExecveTimingPrunes(c *C) { + stt := strace.NewExecveTiming(3) + + // simple cases + stt.AddExeRuntime("t0", 2) + c.Check(stt.ExeRuntimes(), HasLen, 1) + stt.AddExeRuntime("t1", 1) + c.Check(stt.ExeRuntimes(), HasLen, 2) + stt.AddExeRuntime("t2", 5) + c.Check(stt.ExeRuntimes(), HasLen, 3) + + // starts pruing the fastest call, keeps order otherwise + stt.AddExeRuntime("t3", 4) + c.Check(stt.ExeRuntimes(), HasLen, 3) + c.Check(stt.ExeRuntimes(), DeepEquals, []strace.ExeRuntime{ + {Exe: "t0", TotalSec: 2}, + {Exe: "t2", TotalSec: 5}, + {Exe: "t3", TotalSec: 4}, + }) + +} + +// generated with: +// sudo /usr/lib/snapd/snap-discard-ns test-snapd-tools && sudo strace -u $USER -o strace.log -f -e trace=execve,execveat -ttt test-snapd-tools.echo foo && cat strace.log +var sampleStraceSimple = []byte(`21616 1542882400.198907 execve("/snap/bin/test-snapd-tools.echo", ["test-snapd-tools.echo", "foo"], 0x7fff7f275f48 /* 27 vars */) = 0 +21616 1542882400.204710 execve("/snap/core/current/usr/bin/snap", ["test-snapd-tools.echo", "foo"], 0xc42011c8c0 /* 27 vars */ +21621 1542882400.204845 +++ exited with 0 +++ +21620 1542882400.204853 +++ exited with 0 +++ +21619 1542882400.204857 +++ exited with 0 +++ +21618 1542882400.204861 +++ exited with 0 +++ +21617 1542882400.204875 +++ exited with 0 +++ +21616 1542882400.205199 <... execve resumed> ) = 0 +21616 1542882400.220845 execve("/snap/core/5976/usr/lib/snapd/snap-confine", ["/snap/core/5976/usr/lib/snapd/sn"..., "snap.test-snapd-tools.echo", "/usr/lib/snapd/snap-exec", "test-snapd-tools.echo", "foo"], 0xc8200a3600 /* 41 vars */ +21625 1542882400.220994 +++ exited with 0 +++ +21624 1542882400.220999 +++ exited with 0 +++ +21623 1542882400.221002 +++ exited with 0 +++ +21622 1542882400.221005 +++ exited with 0 +++ +21616 1542882400.221634 <... execve resumed> ) = 0 +21629 1542882400.356625 execveat(3, "", ["snap-update-ns", "--from-snap-confine", "test-snapd-tools"], 0x7ffeaf4faa40 /* 0 vars */, AT_EMPTY_PATH) = 0 +21631 1542882400.360708 +++ exited with 0 +++ +21632 1542882400.360723 +++ exited with 0 +++ +21630 1542882400.360727 +++ exited with 0 +++ +21633 1542882400.360842 +++ exited with 0 +++ +21629 1542882400.360848 +++ exited with 0 +++ +21616 1542882400.360869 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21629, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- +21626 1542882400.375793 +++ exited with 0 +++ +21616 1542882400.375836 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21626, si_uid=1000, si_status=0, si_utime=0, si_stime=0} --- +21616 1542882400.377349 execve("/usr/lib/snapd/snap-exec", ["/usr/lib/snapd/snap-exec", "test-snapd-tools.echo", "foo"], 0x23ebc80 /* 45 vars */) = 0 +21616 1542882400.383698 execve("/snap/test-snapd-tools/6/bin/echo", ["/snap/test-snapd-tools/6/bin/ech"..., "foo"], 0xc420072f00 /* 47 vars */ +21638 1542882400.383855 +++ exited with 0 +++ +21637 1542882400.383862 +++ exited with 0 +++ +21636 1542882400.383877 +++ exited with 0 +++ +21634 1542882400.383884 +++ exited with 0 +++ +21635 1542882400.383890 +++ exited with 0 +++ +21616 1542882400.384105 <... execve resumed> ) = 0 +21616 1542882400.384974 +++ exited with 0 +++ +`) + +func (s *timingSuite) TestTraceExecveTimings(c *C) { + f, err := ioutil.TempFile("", "strace-extract-test-") + c.Assert(err, IsNil) + defer os.Remove(f.Name()) + _, err = f.Write(sampleStraceSimple) + c.Assert(err, IsNil) + f.Sync() + + st, err := strace.TraceExecveTimings(f.Name(), 10) + c.Assert(err, IsNil) + c.Assert(st.TotalTime, Equals, 0.1860671043395996) + c.Assert(st.ExeRuntimes(), DeepEquals, []strace.ExeRuntime{ + {Exe: "/snap/bin/test-snapd-tools.echo", TotalSec: 0.005803108215332031}, + {Exe: "/snap/core/current/usr/bin/snap", TotalSec: 0.016134977340698242}, + {Exe: "snap-update-ns", TotalSec: 0.0042438507080078125}, + {Exe: "/snap/core/5976/usr/lib/snapd/snap-confine", TotalSec: 0.15650391578674316}, + {Exe: "/usr/lib/snapd/snap-exec", TotalSec: 0.006349086761474609}, + }) +} diff --git a/tests/main/snap-run/task.yaml b/tests/main/snap-run/task.yaml index a5e31be9a49..7dc2c86806f 100644 --- a/tests/main/snap-run/task.yaml +++ b/tests/main/snap-run/task.yaml @@ -62,3 +62,8 @@ execute: | MATCH hello < stdout fi + snap run --trace-exec test-snapd-tools.echo hello 2> stderr + MATCH "Slowest [0-9]+ exec calls during snap run" < stderr + MATCH " [0-9.]+s .*/snap-exec" < stderr + MATCH " [0-9.]+s .*/snap-confine" < stderr + MATCH "Total time: [0-9.]+s" < stderr diff --git a/testutil/exec.go b/testutil/exec.go index c5b67ed1913..281096a8727 100644 --- a/testutil/exec.go +++ b/testutil/exec.go @@ -147,3 +147,8 @@ func (cmd *MockCmd) ForgetCalls() { func (cmd *MockCmd) BinDir() string { return cmd.binDir } + +// Exe return the full path of the mock binary +func (cmd *MockCmd) Exe() string { + return filepath.Join(cmd.exeFile) +} From e1fcd79a01fbae69a78459d868ed27fdd9c1c08c Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 12:57:30 +0100 Subject: [PATCH 111/580] overlord/ifacestate: demote profilesNeedRegeneration to plain function I want to mock this function for testing and this way it is much easier to do. It doesn't need any interaction with the interface manager anyway. Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index 55a6a52bde5..e631283b590 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -75,7 +75,7 @@ func (m *InterfaceManager) initialize(extraInterfaces []interfaces.Interface, ex if _, err := m.reloadConnections(""); err != nil { return err } - if m.profilesNeedRegeneration() { + if profilesNeedRegeneration() { if err := m.regenerateAllSecurityProfiles(); err != nil { return err } @@ -138,7 +138,7 @@ func (m *InterfaceManager) addSnaps(snaps []*snap.Info) error { return nil } -func (m *InterfaceManager) profilesNeedRegeneration() bool { +func profilesNeedRegeneration() bool { mismatch, err := interfaces.SystemKeyMismatch() if err != nil { logger.Noticef("error trying to compare the snap system key: %v", err) From 3066fe414a0fcc0312ec87c55f76f6a2ad2cc812 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 13:43:30 +0100 Subject: [PATCH 112/580] overlord/ifacestate: allow mocking profilesNeedRegeneration Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/export_test.go | 7 +++++++ overlord/ifacestate/helpers.go | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go index 2cdb0a048b9..2b0c3744cd7 100644 --- a/overlord/ifacestate/export_test.go +++ b/overlord/ifacestate/export_test.go @@ -108,3 +108,10 @@ func GetConnStateAttrs(conns map[string]connState, connID string) (plugStatic, p } return conn.StaticPlugAttrs, conn.DynamicPlugAttrs, conn.StaticSlotAttrs, conn.DynamicSlotAttrs, true } + +// MockProfilesNeedRegeneration mocks the function checking if profiles need regeneration. +func MockProfilesNeedRegeneration(fn func() bool) func() { + old := profilesNeedRegeneration + profilesNeedRegeneration = fn + return func() { profilesNeedRegeneration = old } +} diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index e631283b590..a7f801a1c7f 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -138,7 +138,7 @@ func (m *InterfaceManager) addSnaps(snaps []*snap.Info) error { return nil } -func profilesNeedRegeneration() bool { +func profilesNeedRegenerationImpl() bool { mismatch, err := interfaces.SystemKeyMismatch() if err != nil { logger.Noticef("error trying to compare the snap system key: %v", err) @@ -147,6 +147,8 @@ func profilesNeedRegeneration() bool { return mismatch } +var profilesNeedRegeneration = profilesNeedRegenerationImpl + // regenerateAllSecurityProfiles will regenerate all security profiles. func (m *InterfaceManager) regenerateAllSecurityProfiles() error { // Get all the security backends From 0d92cf1061dda09c9d546483e874615a84d783ac Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 13:44:19 +0100 Subject: [PATCH 113/580] overlord/ifacestate: rename writeSystemKey to shouldWriteSystemKey Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index a7f801a1c7f..40105116ac8 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -167,7 +167,7 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { } } - writeSystemKey := true + shouldWriteSystemKey := true os.Remove(dirs.SnapSystemKeyFile) // For each snap: @@ -192,12 +192,12 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { // Let's log this but carry on without writing the system key. logger.Noticef("cannot regenerate %s profile for snap %q: %s", backend.Name(), snapName, err) - writeSystemKey = false + shouldWriteSystemKey = false } } } - if writeSystemKey { + if shouldWriteSystemKey { if err := interfaces.WriteSystemKey(); err != nil { logger.Noticef("cannot write system key: %v", err) } From 02b5a0c9c8a84f4ca9eddd82b8c4c9bc2251fee0 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 13:44:35 +0100 Subject: [PATCH 114/580] overlord/ifacestate: allow mocking WriteSystemKey Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/export_test.go | 7 +++++++ overlord/ifacestate/helpers.go | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go index 2b0c3744cd7..48c0396517e 100644 --- a/overlord/ifacestate/export_test.go +++ b/overlord/ifacestate/export_test.go @@ -115,3 +115,10 @@ func MockProfilesNeedRegeneration(fn func() bool) func() { profilesNeedRegeneration = fn return func() { profilesNeedRegeneration = old } } + +// MockWriteSystemKey mocks the function responsible for writing the system key. +func MockWriteSystemKey(fn func() error) func() { + old := writeSystemKey + writeSystemKey = fn + return func() { writeSystemKey = old } +} diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index 40105116ac8..c7efe25264e 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -148,6 +148,7 @@ func profilesNeedRegenerationImpl() bool { } var profilesNeedRegeneration = profilesNeedRegenerationImpl +var writeSystemKey = interfaces.WriteSystemKey // regenerateAllSecurityProfiles will regenerate all security profiles. func (m *InterfaceManager) regenerateAllSecurityProfiles() error { @@ -198,7 +199,7 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { } if shouldWriteSystemKey { - if err := interfaces.WriteSystemKey(); err != nil { + if err := writeSystemKey(); err != nil { logger.Noticef("cannot write system key: %v", err) } } From e0bbc9c3a689c6fac8662a2c1bc1aaafee2271a5 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 13:44:58 +0100 Subject: [PATCH 115/580] overlord/ifacestate: add test for failing regeneration of security profiles Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers_test.go | 86 +++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/overlord/ifacestate/helpers_test.go b/overlord/ifacestate/helpers_test.go index 107b020b888..ee092c50a07 100644 --- a/overlord/ifacestate/helpers_test.go +++ b/overlord/ifacestate/helpers_test.go @@ -20,12 +20,26 @@ package ifacestate_test import ( + "errors" + "io/ioutil" + "os" + "path/filepath" "strings" . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" ) type helpersSuite struct { @@ -213,3 +227,75 @@ func (s *helpersSuite) TestHotplugSlotInfo(c *C) { c.Assert(err, IsNil) c.Assert(slots, DeepEquals, defs) } + +var snapdYaml = `name: snapd +version: 1.0 +` + +// Check what happens with system-key when security profile regeneration fails. +func (s *helpersSuite) TestSystemKeyAndFailingProfileRegeneration(c *C) { + dirs.SetRootDir(c.MkDir()) + defer dirs.SetRootDir("") + + // Create a fake security backend with failing Setup method and mock all + // security backends away so that we only use this special one. Note that + // the backend is given a non-empty name as the interface manager skips + // test backends with empty name for convenience. + backend := &ifacetest.TestSecurityBackend{ + BackendName: "BROKEN", + SetupCallback: func(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error { + return errors.New("cannot setup security profile") + }, + } + restore := ifacestate.MockSecurityBackends([]interfaces.SecurityBackend{backend}) + defer restore() + + // Create a mock overlord, mainly to have state. + ovld := overlord.Mock() + st := ovld.State() + + // Put a fake snap in the state, we need to setup security for at least one + // snap to give the fake security backend a chance to fail. + yamlText := ` +name: test-snapd-canary +version: 1 +apps: + test-snapd-canary: + command: bin/canary +` + si := &snap.SideInfo{Revision: snap.R(1), RealName: "test-snapd-canary"} + snapInfo := snaptest.MockSnap(c, yamlText, si) + st.Lock() + snapst := &snapstate.SnapState{ + SnapType: string(snap.TypeApp), + Sequence: []*snap.SideInfo{si}, + Active: true, + Current: snap.R(1), + } + snapstate.Set(st, snapInfo.InstanceName(), snapst) + st.Unlock() + + // Pretend that security profiles are out of date and mock the + // function that writes the new system key with one always panics. + restore = ifacestate.MockProfilesNeedRegeneration(func() bool { return true }) + defer restore() + restore = ifacestate.MockWriteSystemKey(func() error { panic("system key should not be written") }) + defer restore() + // Put a fake system key in place, we just want to see that file being removed. + err := os.MkdirAll(filepath.Dir(dirs.SnapSystemKeyFile), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapSystemKeyFile, []byte("system-key"), 0755) + c.Assert(err, IsNil) + + // Put up a fake logger to capture logged messages. + log, restore := logger.MockLogger() + defer restore() + + // Construct the interface manager. + _, err = ifacestate.Manager(st, nil, ovld.TaskRunner(), nil, nil) + c.Assert(err, IsNil) + + // Check that system key is not on disk. + c.Check(log.String(), testutil.Contains, `cannot regenerate BROKEN profile for snap "test-snapd-canary": cannot setup security profile`) + c.Check(osutil.FileExists(dirs.SnapSystemKeyFile), Equals, false) +} From 74b2915f6884e105af069ef47c79016b2548c73d Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 14:26:56 +0100 Subject: [PATCH 116/580] tests: add test for failing regeneration of security profiles Signed-off-by: Zygmunt Krynicki --- tests/regression/lp-1805838/task.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/regression/lp-1805838/task.yaml diff --git a/tests/regression/lp-1805838/task.yaml b/tests/regression/lp-1805838/task.yaml new file mode 100644 index 00000000000..75153a64373 --- /dev/null +++ b/tests/regression/lp-1805838/task.yaml @@ -0,0 +1,26 @@ +summary: system key is not written if security setup fails +details: | + When snapd is started up and the system key needs regeneration errors from + establishing the new security profiles should not stop snapd from running + but should prevent snapd from writing the system key. +prepare: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh + mount --bind /bin/false "$LIBEXECDIR/snapd/snap-seccomp" + test -d "$SNAP_MOUNT_DIR/core" && mount --bind /bin/false "$SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-seccomp" + test -d "$SNAP_MOUNT_DIR/snapd" && mount --bind /bin/false "$SNAP_MOUNT_DIR/snapd/current/usr/lib/snapd/snap-seccomp" + mv /var/lib/snapd/system-key /var/lib/snapd/system-key.orig + echo '{}' > /var/lib/snapd/system-key +restore: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh + umount "$LIBEXECDIR/snapd/snap-seccomp" || true + test -d "$SNAP_MOUNT_DIR/core" && ( umount "$SNAP_MOUNT_DIR/core/current/usr/lib/snapd/snap-seccomp" || true ) + test -d "$SNAP_MOUNT_DIR/snapd" && ( umount "$SNAP_MOUNT_DIR/snapd/current/usr/lib/snapd/snap-seccomp" || true ) + mv /var/lib/snapd/system-key.orig /var/lib/snapd/system-key || true +execute: | + systemctl stop snapd.socket snapd.service + systemctl start snapd.socket snapd.service + journalctl -u snapd | MATCH 'cannot regenerate seccomp profile for snap "(core|snapd|test-snapd-rsync-core18)"' + snap list | MATCH "(core|snapd)" + test ! -e /var/lib/snapd/system-key From 8ccadf7484a5c8061284dea2377ca3b199a8bdd6 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 15:20:57 +0100 Subject: [PATCH 117/580] overlord/ifacestate: add rationale for removing system key Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index c7efe25264e..9d14b97d6ed 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -168,6 +168,13 @@ func (m *InterfaceManager) regenerateAllSecurityProfiles() error { } } + // The reason the system key is unlinked is to prevent snapd from believing + // that an old system key is valid and represents security setup + // established in the system. If snapd is reverted following a failed + // startup then system key may match the system key that used to be on disk + // but some of the system security may have been changed by the new snapd, + // the one that was reverted. Unlinking avoids such possibility, forcing + // old snapd to re-establish proper security view. shouldWriteSystemKey := true os.Remove(dirs.SnapSystemKeyFile) From b73b6b7facb0d61ed7b0a42cc9cc9b042d7b5976 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 29 Nov 2018 15:21:35 +0100 Subject: [PATCH 118/580] overlord/ifacestate: tweak unreachable code panic msg Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/helpers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overlord/ifacestate/helpers_test.go b/overlord/ifacestate/helpers_test.go index ee092c50a07..284416646c3 100644 --- a/overlord/ifacestate/helpers_test.go +++ b/overlord/ifacestate/helpers_test.go @@ -279,7 +279,7 @@ apps: // function that writes the new system key with one always panics. restore = ifacestate.MockProfilesNeedRegeneration(func() bool { return true }) defer restore() - restore = ifacestate.MockWriteSystemKey(func() error { panic("system key should not be written") }) + restore = ifacestate.MockWriteSystemKey(func() error { panic("should not attempt to write system key") }) defer restore() // Put a fake system key in place, we just want to see that file being removed. err := os.MkdirAll(filepath.Dir(dirs.SnapSystemKeyFile), 0755) From 8ee224f98445c2c1f60caf76c23974755671804d Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 4 Dec 2018 09:59:38 -0300 Subject: [PATCH 119/580] Cosmetic update for reset.sh based on review comments --- tests/lib/reset.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 4e44665396d..b3e719ed371 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -70,13 +70,13 @@ reset_classic() { # Restore snapd state and start systemd service units restore_snapd_state escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" - all_units="$(systemctl list-unit-files --full | cut -f1 -d ' ')" + all_units="$(systemctl list-unit-files --full --no-legend | cut -f1 -d ' ')" mounts="" - if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount"; then + if echo "$all_units" | grep -q "^${escaped_snap_mount_dir}[-.].*\\.mount"; then mounts="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount")" fi services="" - if echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service"; then + if echo "$all_units" | grep -q "^${escaped_snap_mount_dir}[-.].*\\.service"; then services="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service")" fi systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ @@ -169,13 +169,11 @@ tear_down_store() { reset_snapd() { if is_core_system; then reset_all_snap "$@" - discard_ns - tear_down_store "$@" else reset_classic "$@" - discard_ns - tear_down_store "$@" fi + discard_ns + tear_down_store "$@" } From 54ecad94157e55b0e0958ba753c39bbf006f9aa3 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 6 Dec 2018 11:42:46 +0100 Subject: [PATCH 120/580] cmd, dirs, interfaces/apparmor: update distro identification to support "archlinux" In filesystem-2018.12-1 Arch is switching its /etc/os-release to: NAME="Arch Linux" PRETTY_NAME="Arch Linux" ID=archlinux ANSI_COLOR="0;36" HOME_URL="https://www.archlinux.org/" SUPPORT_URL="https://bbs.archlinux.org/" BUG_REPORT_URL="https://bugs.archlinux.org/" Before we had: ID=arch ID_LIKE=archlinux Update relevant locations to support the new IDs. Signed-off-by: Maciej Borzecki --- cmd/cmd_test.go | 2 +- cmd/snap/cmd_paths_test.go | 21 ++++++++++++++-- dirs/dirs.go | 2 +- dirs/dirs_test.go | 2 ++ interfaces/apparmor/backend.go | 2 +- interfaces/apparmor/backend_test.go | 38 ++++++++++++++++++++++++++++- 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 615b31121a4..c73dcfae01b 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -148,7 +148,7 @@ func (s *cmdSuite) TestNonClassicDistroNoSupportsReExec(c *C) { // no distro supports re-exec when not on classic :-) for _, id := range []string{ "fedora", "centos", "rhel", "opensuse", "suse", "poky", - "debian", "ubuntu", "arch", + "debian", "ubuntu", "arch", "archlinux", } { restore = release.MockReleaseInfo(&release.OS{ID: id}) defer restore() diff --git a/cmd/snap/cmd_paths_test.go b/cmd/snap/cmd_paths_test.go index 31e6260b32d..0eb9c4302cb 100644 --- a/cmd/snap/cmd_paths_test.go +++ b/cmd/snap/cmd_paths_test.go @@ -58,10 +58,12 @@ func (s *SnapSuite) TestPathsFedora(c *C) { } func (s *SnapSuite) TestPathsArch(c *C) { - restore := release.MockReleaseInfo(&release.OS{IDLike: []string{"arch"}}) - defer restore() defer dirs.SetRootDir("/") + // old /etc/os-release contents + restore := release.MockReleaseInfo(&release.OS{ID: "arch", IDLike: []string{"archlinux"}}) + defer restore() + dirs.SetRootDir("/") _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) c.Assert(err, IsNil) @@ -70,4 +72,19 @@ func (s *SnapSuite) TestPathsArch(c *C) { "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ "SNAPD_LIBEXEC=/usr/lib/snapd\n") c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + // new contents, as set by filesystem-2018.12-1 + restore = release.MockReleaseInfo(&release.OS{ID: "archlinux"}) + defer restore() + + dirs.SetRootDir("/") + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "paths"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "SNAPD_MOUNT=/var/lib/snapd/snap\n"+ + "SNAPD_BIN=/var/lib/snapd/snap/bin\n"+ + "SNAPD_LIBEXEC=/usr/lib/snapd\n") + c.Assert(s.Stderr(), Equals, "") } diff --git a/dirs/dirs.go b/dirs/dirs.go index 49641f6b545..2ec6e94ae17 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -202,7 +202,7 @@ func SetRootDir(rootdir string) { GlobalRootDir = rootdir isInsideBase, _ := isInsideBaseSnap() - if !isInsideBase && release.DistroLike("fedora", "arch", "manjaro", "antergos") { + if !isInsideBase && release.DistroLike("fedora", "arch", "archlinux", "manjaro", "antergos") { SnapMountDir = filepath.Join(rootdir, "/var/lib/snapd/snap") } else { SnapMountDir = filepath.Join(rootdir, defaultSnapMountDir) diff --git a/dirs/dirs_test.go b/dirs/dirs_test.go index 04cb9008da8..347c33b0291 100644 --- a/dirs/dirs_test.go +++ b/dirs/dirs_test.go @@ -94,6 +94,8 @@ func (s *DirsTestSuite) TestClassicConfinementSupportOnSpecificDistributions(c * {"debian", nil, true}, {"suse", nil, true}, {"yocto", nil, true}, + {"arch", []string{"archlinux"}, false}, + {"archlinux", nil, false}, } { reset := release.MockReleaseInfo(&release.OS{ID: t.ID, IDLike: t.IDLike}) defer reset() diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index 716e440b742..e4dcb48e7fb 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -474,7 +474,7 @@ func downgradeConfinement() bool { // 4.16, do not downgrade the confinement template. return false } - case release.DistroLike("arch"): + case release.DistroLike("arch", "archlinux"): // The default kernel has AppArmor enabled since 4.18.8, the // hardened one since 4.17.4 return false diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index e3d60412934..40b8a67085b 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -673,10 +673,45 @@ func (s *backendSuite) TestCombineSnippetsOpenSUSETumbleweedOldKernel(c *C) { c.Check(profile, testutil.FileEquals, "\n#classic"+commonPrefix+"\nprofile \"snap.samba.smbd\" (attach_disconnected) {\n\n}\n") } +func (s *backendSuite) TestCombineSnippetsArchOldIDSufficientHardened(c *C) { + restore := release.MockAppArmorLevel(release.PartialAppArmor) + defer restore() + restore = release.MockReleaseInfo(&release.OS{ID: "arch", IDLike: []string{"archlinux"}}) + defer restore() + restore = osutil.MockKernelVersion("4.18.2.a-1-hardened") + defer restore() + restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) + defer restore() + restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil }) + defer restore() + // NOTE: replace the real template with a shorter variant + restoreTemplate := apparmor.MockTemplate("\n" + + "###VAR###\n" + + "###PROFILEATTACH### (attach_disconnected) {\n" + + "###SNIPPETS###\n" + + "}\n") + defer restoreTemplate() + restoreClassicTemplate := apparmor.MockClassicTemplate("\n" + + "#classic\n" + + "###VAR###\n" + + "###PROFILEATTACH### (attach_disconnected) {\n" + + "###SNIPPETS###\n" + + "}\n") + defer restoreClassicTemplate() + s.Iface.AppArmorPermanentSlotCallback = func(spec *apparmor.Specification, slot *snap.SlotInfo) error { + spec.AddSnippet("snippet") + return nil + } + + s.InstallSnap(c, interfaces.ConfinementOptions{}, "", ifacetest.SambaYamlV1, 1) + profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") + c.Check(profile, testutil.FileEquals, commonPrefix+"\nprofile \"snap.samba.smbd\" (attach_disconnected) {\nsnippet\n}\n") +} + func (s *backendSuite) TestCombineSnippetsArchSufficientHardened(c *C) { restore := release.MockAppArmorLevel(release.PartialAppArmor) defer restore() - restore = release.MockReleaseInfo(&release.OS{ID: "arch"}) + restore = release.MockReleaseInfo(&release.OS{ID: "archlinux"}) defer restore() restore = osutil.MockKernelVersion("4.18.2.a-1-hardened") defer restore() @@ -1506,6 +1541,7 @@ func (s *backendSuite) TestDowngradeConfinement(c *C) { {"opensuse-tumbleweed", "4.14.1-default", true}, {"arch", "4.18.2.a-1-hardened", false}, {"arch", "4.18.8-arch1-1-ARCH", false}, + {"archlinux", "4.18.2.a-1-hardened", false}, } { c.Logf("trying: %+v", tc) restore := release.MockReleaseInfo(&release.OS{ID: tc.distro}) From 6316bebd98be42fb9db7850d39b2e1a7772c7513 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Fri, 7 Dec 2018 15:55:35 +0000 Subject: [PATCH 121/580] apparmor: allow hard link to snap-specific semaphore files When adding the accessing for /dev/shm/sem.snap..., 'l' was forgotten. Hard linking to this file is important for race-free semaphore handling. References: https://forum.snapcraft.io/t/python-multiprocessing-sem-open-blocked-in-strict-mode/962/14 --- interfaces/apparmor/template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go index 997975fa5cb..7216ba4374e 100644 --- a/interfaces/apparmor/template.go +++ b/interfaces/apparmor/template.go @@ -416,7 +416,7 @@ var defaultTemplate = ` # bind mount *not* used here (see 'parallel installs', above) /{dev,run}/shm/snap.@{SNAP_INSTANCE_NAME}.** mrwlkix, # Also allow app-specific access for sem_open() - /{dev,run}/shm/sem.snap.@{SNAP_INSTANCE_NAME}.* mrwk, + /{dev,run}/shm/sem.snap.@{SNAP_INSTANCE_NAME}.* mrwlk, # Snap-specific XDG_RUNTIME_DIR that is based on the UID of the user # bind mount *not* used here (see 'parallel installs', above) From 9c5724d92a7880f954e8afa68a7f32305811e532 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 7 Dec 2018 11:48:59 +0100 Subject: [PATCH 122/580] centos: enable SELinux support on CentOS 7 CentOS 7.6 has SELinux policy is up to date and allows us to build the module for snapd. Signed-off-by: Maciej Borzecki --- packaging/fedora/snapd.spec | 3 --- 1 file changed, 3 deletions(-) diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index 70b93cd1c7d..7090ff06799 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -90,9 +90,6 @@ %if 0%{?amzn2} == 1 %global with_selinux 0 %endif -%if 0%{?centos} == 7 -%global with_selinux 0 -%endif Name: snapd Version: 2.36.2 From f6b672bbf01a5feadc95a0ef4d6bb4df21323caa Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 7 Dec 2018 13:41:31 +0100 Subject: [PATCH 123/580] tests/lib/prepare-restore: smarter copying of built RPMs Signed-off-by: Maciej Borzecki --- tests/lib/prepare-restore.sh | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh index 175a00623be..a3e1db4be09 100755 --- a/tests/lib/prepare-restore.sh +++ b/tests/lib/prepare-restore.sh @@ -152,11 +152,7 @@ build_rpm() { -ba \ "$packaging_path/snapd.spec" - cp "$rpm_dir"/RPMS/$arch/snap*.rpm "${GOPATH%%:*}" - if [[ "$SPREAD_SYSTEM" = fedora-* ]]; then - # On Fedora we have an additional package for SELinux - cp "$rpm_dir"/RPMS/noarch/snap*.rpm "${GOPATH%%:*}" - fi + find "$rpm_dir"/RPMS -name '*.rpm' -exec cp -v {} "${GOPATH%%:*}" \; } build_arch_pkg() { From 4b7d70e83027e506582e9764e9ddcd4ef573e91b Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 7 Dec 2018 13:41:49 +0100 Subject: [PATCH 124/580] packaging/fedora: RHEL7 has policycoreutils-python Sync with Fedora/EPEL spec. Signed-off-by: Maciej Borzecki --- packaging/fedora/snapd.spec | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index 7090ff06799..a4a5803929b 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -223,7 +223,11 @@ BuildArch: noarch BuildRequires: selinux-policy, selinux-policy-devel Requires(post): selinux-policy-base >= %{_selinux_policy_version} Requires(post): policycoreutils +%if 0%{?rhel} == 7 +Requires(post): policycoreutils-python +%else Requires(post): policycoreutils-python-utils +%endif Requires(pre): libselinux-utils Requires(post): libselinux-utils From d502aedba1a75ee1db5cfcd0119ee718956c9e2b Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 29 Nov 2018 10:14:22 +0100 Subject: [PATCH 125/580] release: detect SELinux Signed-off-by: Maciej Borzecki --- release/selinux.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 release/selinux.go diff --git a/release/selinux.go b/release/selinux.go new file mode 100644 index 00000000000..ad026ac64ff --- /dev/null +++ b/release/selinux.go @@ -0,0 +1,81 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 + * 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 . + * + */ + +package release + +import ( + "bytes" + "fmt" + "io/ioutil" + "path/filepath" +) + +// SELinuxLevelType encodes the state of SELinux support found on this system. +type SELinuxLevelType int + +const ( + // NoSELinux indicates that SELinux is not enabled + NoSELinux SELinuxLevelType = iota + // SELinux is supported and in permissive mode + SELinuxPermissive + // SELinux is supported and in enforcing mode + SELinuxEnforcing +) + +var ( + selinuxLevel SELinuxLevelType + selinuxSummary string +) + +func init() { + selinuxLevel, selinuxSummary = probeSELinux() +} + +// SELinuxLevel tells what level of SELinux enforcement is currently used +func SELinuxLevel() SELinuxLevelType { + return selinuxLevel +} + +// SELinuxSummary describes SELinux status +func SELinuxSummary() string { + return selinuxSummary +} + +// probe related code +var ( + selinuxSysPath = "sys/fs/selinux" +) + +func probeSELinux() (SELinuxLevelType, string) { + if !isDirectory(selinuxSysPath) { + return NoSELinux, "SELinux not enabled" + } + + rawState, err := ioutil.ReadFile(filepath.Join(selinuxSysPath, "enforce")) + if err != nil { + return NoSELinux, fmt.Sprintf("SELinux status cannot be determined: %v", err) + } + switch { + case bytes.Equal(rawState, []byte("0")): + return SELinuxPermissive, "SELinux is enabled and in permissive mode" + case bytes.Equal(rawState, []byte("1")): + return SELinuxEnforcing, "SELinux is enabled and in enforcing mode" + } + return NoSELinux, fmt.Sprintf("SELinux present but status cannot be determined: %s", rawState) +} From 41997513c6e2d4774f0b4a2ffdc898b354a8d556 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 11 Dec 2018 11:49:48 +0100 Subject: [PATCH 126/580] release: refactor selinux support to use selinux package Use the helper package. Signed-off-by: Maciej Borzecki --- release/export_test.go | 18 +++++++++ release/selinux.go | 32 ++++++++-------- release/selinux_test.go | 83 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 release/selinux_test.go diff --git a/release/export_test.go b/release/export_test.go index ce836ed5d36..67434bc3288 100644 --- a/release/export_test.go +++ b/release/export_test.go @@ -58,6 +58,22 @@ func MockIoutilReadfile(newReadfile func(string) ([]byte, error)) (restorer func } } +func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { + old := selinuxIsEnabled + selinuxIsEnabled = isEnabled + return func() { + selinuxIsEnabled = old + } +} + +func MockSELinuxIsEnforcing(isEnforcing func() (bool, error)) (restore func()) { + old := selinuxIsEnforcing + selinuxIsEnforcing = isEnforcing + return func() { + selinuxIsEnforcing = old + } +} + // CurrentAppArmorLevel returns the internal cached apparmor level. func CurrentAppArmorLevel() AppArmorLevelType { return appArmorLevel @@ -84,4 +100,6 @@ var ( PreferredAppArmorParserFeatures = preferredAppArmorParserFeatures IsWSL = isWSL + + ProbeSELinux = probeSELinux ) diff --git a/release/selinux.go b/release/selinux.go index ad026ac64ff..d51e434acba 100644 --- a/release/selinux.go +++ b/release/selinux.go @@ -20,10 +20,9 @@ package release import ( - "bytes" "fmt" - "io/ioutil" - "path/filepath" + + "github.com/snapcore/snapd/selinux" ) // SELinuxLevelType encodes the state of SELinux support found on this system. @@ -41,6 +40,9 @@ const ( var ( selinuxLevel SELinuxLevelType selinuxSummary string + + selinuxIsEnabled = selinux.IsEnabled + selinuxIsEnforcing = selinux.IsEnforcing ) func init() { @@ -57,25 +59,21 @@ func SELinuxSummary() string { return selinuxSummary } -// probe related code -var ( - selinuxSysPath = "sys/fs/selinux" -) - func probeSELinux() (SELinuxLevelType, string) { - if !isDirectory(selinuxSysPath) { - return NoSELinux, "SELinux not enabled" + enabled, err := selinuxIsEnabled() + if err != nil { + return NoSELinux, err.Error() + } + if !enabled { + return NoSELinux, "" } - rawState, err := ioutil.ReadFile(filepath.Join(selinuxSysPath, "enforce")) + enforcing, err := selinuxIsEnforcing() if err != nil { - return NoSELinux, fmt.Sprintf("SELinux status cannot be determined: %v", err) + return NoSELinux, fmt.Sprintf("SELinux is enabled, but status cannot be determined: %v", err) } - switch { - case bytes.Equal(rawState, []byte("0")): + if !enforcing { return SELinuxPermissive, "SELinux is enabled and in permissive mode" - case bytes.Equal(rawState, []byte("1")): - return SELinuxEnforcing, "SELinux is enabled and in enforcing mode" } - return NoSELinux, fmt.Sprintf("SELinux present but status cannot be determined: %s", rawState) + return SELinuxEnforcing, "SELinux is enabled and in enforcing mode" } diff --git a/release/selinux_test.go b/release/selinux_test.go new file mode 100644 index 00000000000..f93800c5aa1 --- /dev/null +++ b/release/selinux_test.go @@ -0,0 +1,83 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 + * 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 . + * + */ + +package release_test + +import ( + "errors" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/release" +) + +type selinuxSuite struct{} + +var _ = Suite(&selinuxSuite{}) + +func (s *selinuxSuite) TestProbeNone(c *C) { + restore := release.MockSELinuxIsEnabled(func() (bool, error) { return false, nil }) + defer restore() + + level, status := release.ProbeSELinux() + c.Assert(level, Equals, release.NoSELinux) + c.Assert(status, Equals, "") +} + +func (s *selinuxSuite) TestProbeEnforcingHappy(c *C) { + restore := release.MockSELinuxIsEnabled(func() (bool, error) { return true, nil }) + defer restore() + restore = release.MockSELinuxIsEnforcing(func() (bool, error) { return true, nil }) + defer restore() + + level, status := release.ProbeSELinux() + c.Assert(level, Equals, release.SELinuxEnforcing) + c.Assert(status, Equals, "SELinux is enabled and in enforcing mode") +} + +func (s *selinuxSuite) TestProbeEnabledError(c *C) { + restore := release.MockSELinuxIsEnabled(func() (bool, error) { return true, errors.New("so much fail") }) + defer restore() + + level, status := release.ProbeSELinux() + c.Assert(level, Equals, release.NoSELinux) + c.Assert(status, Equals, "so much fail") +} + +func (s *selinuxSuite) TestProbeEnforcingError(c *C) { + restore := release.MockSELinuxIsEnabled(func() (bool, error) { return true, nil }) + defer restore() + restore = release.MockSELinuxIsEnforcing(func() (bool, error) { return true, errors.New("so much fail") }) + defer restore() + + level, status := release.ProbeSELinux() + c.Assert(level, Equals, release.NoSELinux) + c.Assert(status, Equals, "SELinux is enabled, but status cannot be determined: so much fail") +} + +func (s *selinuxSuite) TestProbePermissive(c *C) { + restore := release.MockSELinuxIsEnabled(func() (bool, error) { return true, nil }) + defer restore() + restore = release.MockSELinuxIsEnforcing(func() (bool, error) { return false, nil }) + defer restore() + + level, status := release.ProbeSELinux() + c.Assert(level, Equals, release.SELinuxPermissive) + c.Assert(status, Equals, "SELinux is enabled and in permissive mode") +} From 45c6e1c32c80c67a60d03619cf60e5f5e8a9e5d5 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Tue, 11 Dec 2018 12:49:13 +0100 Subject: [PATCH 127/580] cmd/snap-confine: refactor call to snap-update-ns --user-mounts Having introduced tool helper module we have cut a significant amount of boiler-plate code related to calling snap-update-ns and snap-discard-ns. Somehow I totally neglected the call to snap-update-ns --user-mounts, so here it is. Signed-off-by: Zygmunt Krynicki --- cmd/libsnap-confine-private/tool.c | 52 ++++++++++++++++++++++------ cmd/libsnap-confine-private/tool.h | 6 ++-- cmd/snap-confine/mount-support.c | 54 ++---------------------------- 3 files changed, 48 insertions(+), 64 deletions(-) diff --git a/cmd/libsnap-confine-private/tool.c b/cmd/libsnap-confine-private/tool.c index e0f201cdb3a..e106d20a75d 100644 --- a/cmd/libsnap-confine-private/tool.c +++ b/cmd/libsnap-confine-private/tool.c @@ -72,20 +72,52 @@ int sc_open_snap_update_ns(void) } void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, - struct sc_apparmor *apparmor) + struct sc_apparmor *apparmor, bool is_user_profile) { char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; snap_name_copy = sc_strdup(snap_name); - char *argv[] = - { "snap-update-ns", "--from-snap-confine", snap_name_copy, NULL }; - /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor with - * either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function for details. */ - char *envp[] = { "SNAPD_DEBUG=x", NULL }; - char profile[PATH_MAX] = { 0 }; - sc_must_snprintf(profile, sizeof profile, "snap-update-ns.%s", + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", snap_name); - sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, "snap-update-ns", - apparmor, profile, argv, envp); + + if (is_user_profile) { + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + char xdg_runtime_dir_env[PATH_MAX]; + if (xdg_runtime_dir != NULL) { + sc_must_snprintf(xdg_runtime_dir_env, + sizeof(xdg_runtime_dir_env), + "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); + } + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + /* This tells snap-update-ns that we want to process the per-user profile */ + "--user-mounts", snap_name_copy, NULL + }; + char *envp[] = { + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor + * with either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function + * for details. */ + "SNAPD_DEBUG=x", + xdg_runtime_dir_env, NULL + }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); + } else { + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + "--from-snap-confine", + snap_name_copy, NULL + }; + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); + } } int sc_open_snap_discard_ns(void) diff --git a/cmd/libsnap-confine-private/tool.h b/cmd/libsnap-confine-private/tool.h index 7bdac4a2330..88899414014 100644 --- a/cmd/libsnap-confine-private/tool.h +++ b/cmd/libsnap-confine-private/tool.h @@ -18,6 +18,8 @@ #ifndef SNAP_CONFINE_TOOL_H #define SNAP_CONFINE_TOOL_H +#include + /* Forward declaration, for real see apparmor-support.h */ struct sc_apparmor; @@ -27,10 +29,10 @@ struct sc_apparmor; int sc_open_snap_update_ns(void); /** - * sc_call_snap_update_ns calls snap-update-ns from snap-confine. + * sc_call_snap_update_ns calls snap-update-ns from snap-confine **/ void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, - struct sc_apparmor *apparmor); + struct sc_apparmor *apparmor, bool is_user_profile); /** * sc_open_snap_update_ns returns a file descriptor for the snap-discard-ns tool. diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index bf6ae53aa1a..288c22c7f9c 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -618,7 +618,7 @@ void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd, setup_private_pts(); // setup the security backend bind mounts - sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor); + sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor, false); // Try to re-locate back to vanilla working directory. This can fail // because that directory is no longer present. @@ -699,55 +699,5 @@ void sc_setup_user_mounts(struct sc_apparmor *apparmor, int snap_update_ns_fd, } sc_make_slave_mount_ns(); - - debug("calling snap-update-ns to initialize user mounts"); - pid_t child = fork(); - if (child < 0) { - die("cannot fork to run snap-update-ns"); - } - if (child == 0) { - // We are the child, execute snap-update-ns under a dedicated profile. - char profile[PATH_MAX] = { 0 }; - sc_must_snprintf(profile, sizeof profile, "snap-update-ns.%s", - snap_name); - debug("launching snap-update-ns under per-snap profile %s", - profile); - sc_maybe_aa_change_onexec(apparmor, profile); - char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; - snap_name_copy = sc_strdup(snap_name); - char *argv[] = { - "snap-update-ns", "--user-mounts", snap_name_copy, - NULL - }; - char *envp[3] = { NULL }; - int last_env = 0; - if (sc_is_debug_enabled()) { - envp[last_env++] = "SNAPD_DEBUG=1"; - } - const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); - char xdg_runtime_dir_env[PATH_MAX]; - if (xdg_runtime_dir != NULL) { - sc_must_snprintf(xdg_runtime_dir_env, - sizeof(xdg_runtime_dir_env), - "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); - envp[last_env++] = xdg_runtime_dir_env; - } - - debug("fexecv(%d (snap-update-ns), %s %s %s,)", - snap_update_ns_fd, argv[0], argv[1], argv[2]); - fexecve(snap_update_ns_fd, argv, envp); - die("cannot execute snap-update-ns"); - } - // We are the parent, so wait for snap-update-ns to finish. - int status = 0; - debug("waiting for snap-update-ns to finish..."); - if (waitpid(child, &status, 0) < 0) { - die("waitpid() failed for snap-update-ns process"); - } - if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { - die("snap-update-ns failed with code %i", WEXITSTATUS(status)); - } else if (WIFSIGNALED(status)) { - die("snap-update-ns killed by signal %i", WTERMSIG(status)); - } - debug("snap-update-ns finished successfully"); + sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor, true); } From cf9bca910e476334ec85816b26cf81c91373bca3 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Tue, 11 Dec 2018 21:23:00 +0200 Subject: [PATCH 128/580] Update interfaces/builtin/common_files.go Co-Authored-By: mvo5 --- interfaces/builtin/common_files.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index a125d143be3..72e555c3afe 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -49,7 +49,7 @@ const ( func (a filesAAPerm) String() string { switch a { case filesRead: - return "rk" + return "rk" // [r]ead and loc[k] case filesWrite: return "rwkl" } From a600904ed4ad5c8138175a803fdd482bdf6a6859 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 12 Dec 2018 10:13:39 +0100 Subject: [PATCH 129/580] cmd/snap-confine: split sc_call_snap_update_ns into two functions One for calling snap-update-ns for per-snap profile and another with _as_user for calling snap-update-ns --user-mounts. --- cmd/libsnap-confine-private/tool.c | 85 +++++++++++++++++------------- cmd/libsnap-confine-private/tool.h | 11 ++-- cmd/snap-confine/mount-support.c | 4 +- 3 files changed, 58 insertions(+), 42 deletions(-) diff --git a/cmd/libsnap-confine-private/tool.c b/cmd/libsnap-confine-private/tool.c index e106d20a75d..04f19c01a65 100644 --- a/cmd/libsnap-confine-private/tool.c +++ b/cmd/libsnap-confine-private/tool.c @@ -72,7 +72,7 @@ int sc_open_snap_update_ns(void) } void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, - struct sc_apparmor *apparmor, bool is_user_profile) + struct sc_apparmor *apparmor) { char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; snap_name_copy = sc_strdup(snap_name); @@ -81,43 +81,54 @@ void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", snap_name); - if (is_user_profile) { - const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); - char xdg_runtime_dir_env[PATH_MAX]; - if (xdg_runtime_dir != NULL) { - sc_must_snprintf(xdg_runtime_dir_env, - sizeof(xdg_runtime_dir_env), - "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); - } - char *argv[] = { - "snap-update-ns", - /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ - /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ - /* This tells snap-update-ns that we want to process the per-user profile */ - "--user-mounts", snap_name_copy, NULL - }; - char *envp[] = { - /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor - * with either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function - * for details. */ - "SNAPD_DEBUG=x", - xdg_runtime_dir_env, NULL - }; - sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, - "snap-update-ns", apparmor, - aa_profile, argv, envp); - } else { - char *argv[] = { - "snap-update-ns", - /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ - "--from-snap-confine", - snap_name_copy, NULL - }; - char *envp[] = { "SNAPD_DEBUG=x", NULL }; - sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, - "snap-update-ns", apparmor, - aa_profile, argv, envp); + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + "--from-snap-confine", + snap_name_copy, NULL + }; + char *envp[] = { "SNAPD_DEBUG=x", NULL }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); +} + +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor) +{ + char *snap_name_copy SC_CLEANUP(sc_cleanup_string) = NULL; + snap_name_copy = sc_strdup(snap_name); + + char aa_profile[PATH_MAX] = { 0 }; + sc_must_snprintf(aa_profile, sizeof aa_profile, "snap-update-ns.%s", + snap_name); + + const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + char xdg_runtime_dir_env[PATH_MAX]; + if (xdg_runtime_dir != NULL) { + sc_must_snprintf(xdg_runtime_dir_env, + sizeof(xdg_runtime_dir_env), + "XDG_RUNTIME_DIR=%s", xdg_runtime_dir); } + + char *argv[] = { + "snap-update-ns", + /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ + /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + /* This tells snap-update-ns that we want to process the per-user profile */ + "--user-mounts", snap_name_copy, NULL + }; + char *envp[] = { + /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor + * with either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function + * for details. */ + "SNAPD_DEBUG=x", + xdg_runtime_dir_env, NULL + }; + sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, + "snap-update-ns", apparmor, + aa_profile, argv, envp); } int sc_open_snap_discard_ns(void) diff --git a/cmd/libsnap-confine-private/tool.h b/cmd/libsnap-confine-private/tool.h index 88899414014..784b0a6e4a0 100644 --- a/cmd/libsnap-confine-private/tool.h +++ b/cmd/libsnap-confine-private/tool.h @@ -18,8 +18,6 @@ #ifndef SNAP_CONFINE_TOOL_H #define SNAP_CONFINE_TOOL_H -#include - /* Forward declaration, for real see apparmor-support.h */ struct sc_apparmor; @@ -32,7 +30,14 @@ int sc_open_snap_update_ns(void); * sc_call_snap_update_ns calls snap-update-ns from snap-confine **/ void sc_call_snap_update_ns(int snap_update_ns_fd, const char *snap_name, - struct sc_apparmor *apparmor, bool is_user_profile); + struct sc_apparmor *apparmor); + +/** + * sc_call_snap_update_ns calls snap-update-ns --user-mounts from snap-confine + **/ +void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, + const char *snap_name, + struct sc_apparmor *apparmor); /** * sc_open_snap_update_ns returns a file descriptor for the snap-discard-ns tool. diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 288c22c7f9c..93538732785 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -618,7 +618,7 @@ void sc_populate_mount_ns(struct sc_apparmor *apparmor, int snap_update_ns_fd, setup_private_pts(); // setup the security backend bind mounts - sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor, false); + sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor); // Try to re-locate back to vanilla working directory. This can fail // because that directory is no longer present. @@ -699,5 +699,5 @@ void sc_setup_user_mounts(struct sc_apparmor *apparmor, int snap_update_ns_fd, } sc_make_slave_mount_ns(); - sc_call_snap_update_ns(snap_update_ns_fd, snap_name, apparmor, true); + sc_call_snap_update_ns_as_user(snap_update_ns_fd, snap_name, apparmor); } From a6019c01a711566afa72c432b565172c2bcabd73 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 12 Dec 2018 15:40:00 -0300 Subject: [PATCH 130/580] Remove comments and relocate test --- spread.yaml | 6 ------ tests/nested/{ => core}/core-snap-refresh-on-core/task.yaml | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) rename tests/nested/{ => core}/core-snap-refresh-on-core/task.yaml (96%) diff --git a/spread.yaml b/spread.yaml index 995d1cff898..8f0a898913b 100644 --- a/spread.yaml +++ b/spread.yaml @@ -728,9 +728,6 @@ suites: tests/nested/classic/: summary: Tests for nested images - # Test cases are not yet ported to Fedora/openSUSE that is why - # we keep them disabled. A later PR will enable most tests and - # drop this blacklist. backends: [google-nested] environment: NESTED_ARCH: "$(HOST: echo ${SPREAD_NESTED_ARCH:-amd64})" @@ -750,9 +747,6 @@ suites: tests/nested/core/: summary: Tests for nested images - # Test cases are not yet ported to Fedora/openSUSE that is why - # we keep them disabled. A later PR will enable most tests and - # drop this blacklist. backends: [google-nested] environment: NESTED_ARCH: "$(HOST: echo ${SPREAD_NESTED_ARCH:-amd64})" diff --git a/tests/nested/core-snap-refresh-on-core/task.yaml b/tests/nested/core/core-snap-refresh-on-core/task.yaml similarity index 96% rename from tests/nested/core-snap-refresh-on-core/task.yaml rename to tests/nested/core/core-snap-refresh-on-core/task.yaml index 2efc017737e..b5a12dd93ef 100644 --- a/tests/nested/core-snap-refresh-on-core/task.yaml +++ b/tests/nested/core/core-snap-refresh-on-core/task.yaml @@ -14,6 +14,10 @@ restore: | rm -f prevBoot nextBoot rm -f core_*.{assert,snap} + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + destroy_nested_vm + execute: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" From 99444ef30cb23569a26741c7158fad2f584b98e7 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 12 Dec 2018 20:28:34 +0100 Subject: [PATCH 131/580] data/selinux: fix syntax error in definition of snappy_admin interface The snappy_admin interface in snappy.if is incorrectly defined. It is missing a start quote in the interface name. This causes sepolgen-ifgen to raise an error when installing selinux-policy-devel package. The error is logged as follows: /usr/share/selinux/devel/include/contrib/snappy.if: Syntax error on line 260 gen_require [type=GEN_REQ] /usr/share/selinux/devel/include/contrib/snappy.if: Syntax error on line 263 ' [type=SQUOTE] /usr/share/selinux/devel/include/contrib/snappy.if: Syntax error on line 273 ' [type=SQUOTE] Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=1658152 Signed-off-by: Maciej Borzecki --- data/selinux/snappy.if | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/data/selinux/snappy.if b/data/selinux/snappy.if index d47015e945a..f73f7c615d2 100644 --- a/data/selinux/snappy.if +++ b/data/selinux/snappy.if @@ -256,18 +256,18 @@ interface(`snappy_stream_connect',` ## # -interface(`snappy_admin', - gen_require(` - type snappy_t, snappy_config_t; - type snappy_var_run_t; - ') +interface(`snappy_admin',` + gen_require(` + type snappy_t, snappy_config_t; + type snappy_var_run_t; + ') - allow $1 snappy_t:process signal_perms; + allow $1 snappy_t:process signal_perms; - ps_process_pattern($1, snappy_t); + ps_process_pattern($1, snappy_t); - admin_pattern($1, snappy_config_t); + admin_pattern($1, snappy_config_t); - files_list_pids($1, snappy_var_run_t); - admin_pattern($1, snappy_var_run_t); + files_list_pids($1, snappy_var_run_t); + admin_pattern($1, snappy_var_run_t); ') From 203ddc006d1304a722ba91633e9139164fbbb93e Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 12 Dec 2018 19:40:29 +0100 Subject: [PATCH 132/580] systemd: start snapd.autoimport.service in --no-block mode When seeding in core18 we need to make sure that the snadpd.autoimport.service is started in non-blocking mode. Otherwise it would wait forever because it has a "After=snapd.seeding.service" but the snapd.seeding.service will only finish if the code that writes the core18 snapd units can run. So this prevents a deadlock. Note that by starting it with --no-block the right thing will happen, i.e. the unit is starting but waits until the snapd.seeded.service is done and then it will run (which is what we want). --- data/systemd/snapd.autoimport.service.in | 2 ++ wrappers/core18.go | 7 +++++++ wrappers/core18_test.go | 1 + 3 files changed, 10 insertions(+) diff --git a/data/systemd/snapd.autoimport.service.in b/data/systemd/snapd.autoimport.service.in index 739196f5284..afbb428cb25 100644 --- a/data/systemd/snapd.autoimport.service.in +++ b/data/systemd/snapd.autoimport.service.in @@ -10,3 +10,5 @@ ExecStart=@bindir@/snap auto-import [Install] WantedBy=multi-user.target +# This cannot be started on firstboot +# X-Snapd-Snap: do-not-start diff --git a/wrappers/core18.go b/wrappers/core18.go index bc9f986e7e7..36884d8c8f6 100644 --- a/wrappers/core18.go +++ b/wrappers/core18.go @@ -179,6 +179,13 @@ func writeSnapdServicesOnCore(s *snap.Info, inter interacter) error { if err := sysd.StartNoBlock("snapd.seeded.service"); err != nil { return err } + // we cannot start snapd.autoimport in blocking mode because + // it has a "After=snapd.seeded.service" which means that on + // seeding a "systemctl start" that blocks would hang forever + // and we deadlock. + if err := sysd.StartNoBlock("snapd.autoimport.service"); err != nil { + return err + } return nil } diff --git a/wrappers/core18_test.go b/wrappers/core18_test.go index 54f2a4bc3f6..b724d96699c 100644 --- a/wrappers/core18_test.go +++ b/wrappers/core18_test.go @@ -122,6 +122,7 @@ WantedBy=snapd.service {"start", "snapd.autoimport.service"}, {"start", "snapd.service"}, {"start", "--no-block", "snapd.seeded.service"}, + {"start", "--no-block", "snapd.autoimport.service"}, }) } From af043ea566ffa0ea1185e0a16e534a357fcc1b7f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 13 Dec 2018 10:15:31 +0100 Subject: [PATCH 133/580] wrappers: only restart service in core18 when they are active This fixes the issue that on the very first boot the snapd.seeded.service must not be restarted. This service has a "Requires=snapd.socket" relation and that means that when we seed and we start snapd.socket for the first time we must not stop it or otherwise systemd will also stop snapd.seeded.service which will means that systemd will start the units configured with "After=snapd.seeded.service" which would be too early. --- wrappers/core18.go | 17 ++++++++++++++++- wrappers/core18_test.go | 14 ++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/wrappers/core18.go b/wrappers/core18.go index 36884d8c8f6..23fb1c7806b 100644 --- a/wrappers/core18.go +++ b/wrappers/core18.go @@ -167,9 +167,24 @@ func writeSnapdServicesOnCore(s *snap.Info, inter interacter) error { if bytes.Contains(snapdUnits[unit].Content, []byte("X-Snapd-Snap: do-not-start")) { continue } - if err := sysd.Restart(unit, 5*time.Second); err != nil { + // Ensure to only restart if the unit was previously active. + // This ensures we DTRT on firstboot and do not stop e.g. + // snapd.socket because doing that would mean that the + // snapd.seeded.service is also stopped which confuses the + // boot order (the unit exists before we are fully seeded) + statuses, err := sysd.Status(unit) + if err != nil { return err } + if statuses[0].Active { + if err := sysd.Restart(unit, 5*time.Second); err != nil { + return err + } + } else { + if err := sysd.Start(unit); err != nil { + return err + } + } } // and finally start snapd.service (it will stop by itself and gets // started by systemd then) diff --git a/wrappers/core18_test.go b/wrappers/core18_test.go index b724d96699c..e1c8e125209 100644 --- a/wrappers/core18_test.go +++ b/wrappers/core18_test.go @@ -32,6 +32,7 @@ import ( "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/systemd" "github.com/snapcore/snapd/wrappers" ) @@ -66,6 +67,16 @@ func (s *servicesTestSuite) TestAddSnapServicesForSnapdOnCore(c *C) { // reset root dir dirs.SetRootDir(s.tempdir) + systemctlRestorer := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { + s.sysdLog = append(s.sysdLog, cmd) + if cmd[0] == "show" && cmd[1] == "--property=Id,ActiveState,UnitFileState,Type" { + s := fmt.Sprintf("Type=oneshot\nId=%s\nActiveState=inactive\nUnitFileState=enabled\n", cmd[2]) + return []byte(s), nil + } + return []byte("ActiveState=inactive\n"), nil + }) + defer systemctlRestorer() + info := makeMockSnapdSnap(c) // add the snapd service err := wrappers.AddSnapServices(info, nil) @@ -117,8 +128,7 @@ WantedBy=snapd.service {"--root", dirs.GlobalRootDir, "enable", "snapd.autoimport.service"}, {"--root", dirs.GlobalRootDir, "enable", "snapd.service"}, {"--root", dirs.GlobalRootDir, "enable", "snapd.system-shutdown.service"}, - {"stop", "snapd.autoimport.service"}, - {"show", "--property=ActiveState", "snapd.autoimport.service"}, + {"show", "--property=Id,ActiveState,UnitFileState,Type", "snapd.autoimport.service"}, {"start", "snapd.autoimport.service"}, {"start", "snapd.service"}, {"start", "--no-block", "snapd.seeded.service"}, From e3db0504f5b28029e4a253614782244150ebeb50 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 13 Dec 2018 13:17:20 +0100 Subject: [PATCH 134/580] httputil: retry on temporary net errors (#6287) We currently retry network operations on various high level errors like 500. However we do not retry on network errors like DNS connection refused. To fix that this PR will also retry when we hit net.Error conditions that are marked as Temporary() (like dns connection refused errors when the DNS server is not quite ready yet). This has also a couple of hacks detecting error messages directly because of go1.6 behavior and Arch ATM using the cgo resolver. We should try to work to not need them later. This should fix LP: #1807770 --- httputil/retry.go | 27 ++++++++++++++-- httputil/retry_test.go | 50 +++++++++++++++++++++++++----- store/download_test.go | 2 +- store/export_test.go | 8 +++++ store/store.go | 13 ++++++-- store/store_test.go | 7 +++++ tests/main/network-retry/task.yaml | 33 ++++++++++++++++++++ 7 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 tests/main/network-retry/task.yaml diff --git a/httputil/retry.go b/httputil/retry.go index 995b785859f..8eef140a554 100644 --- a/httputil/retry.go +++ b/httputil/retry.go @@ -26,6 +26,7 @@ import ( "net/http" "net/url" "os" + "strings" "syscall" "time" @@ -82,16 +83,36 @@ func ShouldRetryError(attempt *retry.Attempt, err error) bool { logger.Debugf("Retrying because of: %s", opErr) return true } + // FIXME: code below is not (unit) tested and + // it is unclear if we need it with the new + // opErr.Temporary() "if" below if opErr.Op == "dial" { logger.Debugf("Retrying because of: %#v (syscall error: %#v)", opErr, syscallErr.Err) return true } logger.Debugf("Encountered syscall error: %#v", syscallErr) } - if opNetErr, ok := opErr.Err.(net.Error); ok { - // TODO: some DNS errors? just log for now - logger.Debugf("Not retrying: %#v", opNetErr) + + // If we are unable to talk to a DNS go1.9+ will set + // opErr.IsTemporary - we also support go1.6 so we need to + // add a workaround here. This block can go away once we + // use go1.9+ only. + if dnsErr, ok := opErr.Err.(*net.DNSError); ok { + // The horror, the horror + // TODO: stop Arch to use the cgo resolver + // which requires the right side of the OR + if strings.Contains(dnsErr.Err, "connection refused") || strings.Contains(dnsErr.Err, "Temporary failure in name resolution") { + logger.Debugf("Retrying because of temporary net error (DNS): %#v", dnsErr) + return true + } + } + + // Retry for temporary network errors (like dns errors in 1.9+) + if opErr.Temporary() { + logger.Debugf("Retrying because of temporary net error: %#v", opErr) + return true } + logger.Debugf("Encountered non temporary net.OpError: %#v", opErr) } if err == io.ErrUnexpectedEOF || err == io.EOF { diff --git a/httputil/retry_test.go b/httputil/retry_test.go index 9e7383b48b5..6b1e189b56e 100644 --- a/httputil/retry_test.go +++ b/httputil/retry_test.go @@ -21,8 +21,8 @@ package httputil_test import ( "encoding/json" - "fmt" "io" + "net" "net/http" "net/http/httptest" "time" @@ -403,9 +403,7 @@ func (s *retrySuite) TestRetryRequestTimeoutHandling(c *C) { c.Assert(somewhatBrokenSrvCalls, Equals, 3) } -func (s *retrySuite) TestRetryRequestFailOnDNS(c *C) { - // TODO: retry some types of DNS errors? - +func (s *retrySuite) TestRetryDoesNotFailForPermanentDNSErrors(c *C) { url := "http://nonexistingserver909123.com/" cli := httputil.NewHTTPClient(nil) @@ -416,14 +414,50 @@ func (s *retrySuite) TestRetryRequestFailOnDNS(c *C) { return cli.Get(url) } - var got interface{} readResponseBody := func(resp *http.Response) error { - if resp.StatusCode >= 500 { - return fmt.Errorf("proxy error") + return nil + } + + _, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy) + c.Assert(err, NotNil) + // we try exactly once, a non-existing server is a permanent error + c.Assert(n, Equals, 1) +} + +func (s *retrySuite) TestRetryOnTemporaryDNSfailure(c *C) { + n := 0 + doRequest := func() (*http.Response, error) { + n++ + return nil, &net.OpError{ + Op: "dial", + Net: "tcp", + Err: &net.DNSError{IsTemporary: true}, } - return json.NewDecoder(resp.Body).Decode(&got) } + readResponseBody := func(resp *http.Response) error { + return nil + } + _, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy) + c.Assert(err, NotNil) + c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n)) +} +func (s *retrySuite) TestRetryOnTemporaryDNSfailureNotGo19(c *C) { + n := 0 + doRequest := func() (*http.Response, error) { + n++ + return nil, &net.OpError{ + Op: "dial", + Net: "tcp", + Err: &net.DNSError{ + Err: "[::1]:42463->[::1]:53: read: connection refused", + }, + } + } + readResponseBody := func(resp *http.Response) error { + return nil + } _, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy) c.Assert(err, NotNil) + c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n)) } diff --git a/store/download_test.go b/store/download_test.go index 906416f0e2d..d97e633371d 100644 --- a/store/download_test.go +++ b/store/download_test.go @@ -56,7 +56,7 @@ var _ = Suite(&downloadSuite{}) func (s *downloadSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) - store.MockDefaultRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{ + store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{ Initial: time.Millisecond, Factor: 2.5, })) diff --git a/store/export_test.go b/store/export_test.go index 3488c744097..13b697c1195 100644 --- a/store/export_test.go +++ b/store/export_test.go @@ -73,6 +73,14 @@ func MockDefaultRetryStrategy(t *testutil.BaseTest, strategy retry.Strategy) { }) } +func MockDownloadRetryStrategy(t *testutil.BaseTest, strategy retry.Strategy) { + originalDownloadRetryStrategy := downloadRetryStrategy + downloadRetryStrategy = strategy + t.AddCleanup(func() { + downloadRetryStrategy = originalDownloadRetryStrategy + }) +} + func MockConnCheckStrategy(t *testutil.BaseTest, strategy retry.Strategy) { originalConnCheckStrategy := connCheckStrategy connCheckStrategy = strategy diff --git a/store/store.go b/store/store.go index 978a36e40d9..6013bb4cd50 100644 --- a/store/store.go +++ b/store/store.go @@ -81,9 +81,16 @@ type RefreshOptions struct { // the LimitTime should be slightly more than 3 times of our http.Client // Timeout value -var defaultRetryStrategy = retry.LimitCount(5, retry.LimitTime(38*time.Second, +var defaultRetryStrategy = retry.LimitCount(6, retry.LimitTime(38*time.Second, retry.Exponential{ - Initial: 300 * time.Millisecond, + Initial: 350 * time.Millisecond, + Factor: 2.5, + }, +)) + +var downloadRetryStrategy = retry.LimitCount(7, retry.LimitTime(90*time.Second, + retry.Exponential{ + Initial: 500 * time.Millisecond, Factor: 2.5, }, )) @@ -1482,7 +1489,7 @@ func downloadImpl(ctx context.Context, name, sha3_384, downloadURL string, user var finalErr error var dlSize float64 startTime := time.Now() - for attempt := retry.Start(defaultRetryStrategy, nil); attempt.Next(); { + for attempt := retry.Start(downloadRetryStrategy, nil); attempt.Next(); { reqOptions := reqOptions(storeURL, cdnHeader) httputil.MaybeLogRetryAttempt(reqOptions.URL.String(), attempt, startTime) diff --git a/store/store_test.go b/store/store_test.go index 5e23d167074..0784a1f1deb 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -388,6 +388,13 @@ func (s *storeTestSuite) SetUpTest(c *C) { Factor: 1, }, ))) + + store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second, + retry.Exponential{ + Initial: 1 * time.Millisecond, + Factor: 1, + }, + ))) } func (s *storeTestSuite) TearDownTest(c *C) { diff --git a/tests/main/network-retry/task.yaml b/tests/main/network-retry/task.yaml new file mode 100644 index 00000000000..1ad1c4affff --- /dev/null +++ b/tests/main/network-retry/task.yaml @@ -0,0 +1,33 @@ +summary: Ensure network retry works correctly + +prepare: | + echo "Break DNS" + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + resolvConf=$(realpath /etc/resolv.conf) + mv "${resolvConf}" "${resolvConf}.save" + echo "${resolvConf}" > resolvConf.txt + else + mv /etc/resolv.conf /etc/resolv.conf.save + fi +restore: | + echo "Unbreak DNS" + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + resolvConf=$(cat resolvConf.txt) + mv "${resolvConf}.save" "${resolvConf}" + rm resolvConf.txt + else + mv /etc/resolv.conf.save /etc/resolv.conf + fi + +execute: | + echo "Try to install a snap with broken DNS" + if snap install test-snapd-tools; then + echo "Installing test-snapd-tools with broken DNS should not work" + echo "Test broken" + exit 1 + fi + + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "Ensure we tried to retry this operation" + check_journalctl_log 'Retrying because of temporary net error' From 92f0f2570841ba1698b2c80f0deec8bf3033967a Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 13 Dec 2018 15:01:37 +0100 Subject: [PATCH 135/580] release: probe SELinux status on each call In contrast to AppArmor and Seccomp, SELinux state can change at runtime. Specifically, SELinux fs can be (un-)mounted, and the permissive/enforcing state can be switched. Instead of doing a once-only lazy initialization, check the status on each call. The actual cost is incurred only when called. Signed-off-by: Maciej Borzecki --- release/selinux.go | 19 ++++++++++--------- release/selinux_test.go | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/release/selinux.go b/release/selinux.go index d51e434acba..d3aeef76c38 100644 --- a/release/selinux.go +++ b/release/selinux.go @@ -38,25 +38,26 @@ const ( ) var ( - selinuxLevel SELinuxLevelType - selinuxSummary string - selinuxIsEnabled = selinux.IsEnabled selinuxIsEnforcing = selinux.IsEnforcing ) -func init() { - selinuxLevel, selinuxSummary = probeSELinux() -} - // SELinuxLevel tells what level of SELinux enforcement is currently used func SELinuxLevel() SELinuxLevelType { - return selinuxLevel + level, _ := probeSELinux() + return level } // SELinuxSummary describes SELinux status func SELinuxSummary() string { - return selinuxSummary + _, summary := probeSELinux() + return summary +} + +// SELinuxStatus returns the current level of SELinux support and a descriptive +// summary +func SELinuxStatus() (level SELinuxLevelType, summary string) { + return probeSELinux() } func probeSELinux() (SELinuxLevelType, string) { diff --git a/release/selinux_test.go b/release/selinux_test.go index f93800c5aa1..b6dde144d96 100644 --- a/release/selinux_test.go +++ b/release/selinux_test.go @@ -38,6 +38,9 @@ func (s *selinuxSuite) TestProbeNone(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.NoSELinux) c.Assert(status, Equals, "") + + c.Assert(release.SELinuxLevel(), Equals, level) + c.Assert(release.SELinuxSummary(), Equals, status) } func (s *selinuxSuite) TestProbeEnforcingHappy(c *C) { @@ -49,6 +52,9 @@ func (s *selinuxSuite) TestProbeEnforcingHappy(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.SELinuxEnforcing) c.Assert(status, Equals, "SELinux is enabled and in enforcing mode") + + c.Assert(release.SELinuxLevel(), Equals, level) + c.Assert(release.SELinuxSummary(), Equals, status) } func (s *selinuxSuite) TestProbeEnabledError(c *C) { @@ -58,6 +64,9 @@ func (s *selinuxSuite) TestProbeEnabledError(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.NoSELinux) c.Assert(status, Equals, "so much fail") + + c.Assert(release.SELinuxLevel(), Equals, level) + c.Assert(release.SELinuxSummary(), Equals, status) } func (s *selinuxSuite) TestProbeEnforcingError(c *C) { @@ -69,6 +78,9 @@ func (s *selinuxSuite) TestProbeEnforcingError(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.NoSELinux) c.Assert(status, Equals, "SELinux is enabled, but status cannot be determined: so much fail") + + c.Assert(release.SELinuxLevel(), Equals, level) + c.Assert(release.SELinuxSummary(), Equals, status) } func (s *selinuxSuite) TestProbePermissive(c *C) { @@ -80,4 +92,7 @@ func (s *selinuxSuite) TestProbePermissive(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.SELinuxPermissive) c.Assert(status, Equals, "SELinux is enabled and in permissive mode") + + c.Assert(release.SELinuxLevel(), Equals, level) + c.Assert(release.SELinuxSummary(), Equals, status) } From 09ef767475ec998abc31720113ea1ec98ad5b311 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 13 Dec 2018 12:08:55 +0100 Subject: [PATCH 136/580] wrappers: use new systemd.IsActive in core18 early boot The systemd.Status() call in early boot may not work correctly. For units that are not available it will error because there is no unit type set. Because we only need to know if the unit is active we need to use a new "systemd.IsActive()" helper. --- systemd/systemd.go | 15 +++++++++++++++ wrappers/core18.go | 4 ++-- wrappers/core18_test.go | 4 +++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/systemd/systemd.go b/systemd/systemd.go index 29262be378d..28977288940 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -131,6 +131,7 @@ type Systemd interface { Restart(service string, timeout time.Duration) error Status(services ...string) ([]*ServiceStatus, error) IsEnabled(service string) (bool, error) + IsActive(service string) (bool, error) LogReader(services []string, n int, follow bool) (io.ReadCloser, error) WriteMountUnitFile(name, revision, what, where, fstype string) (string, error) Mask(service string) error @@ -317,6 +318,20 @@ func (s *systemd) IsEnabled(serviceName string) (bool, error) { return false, err } +// IsActive checkes whether the given service is Active +func (s *systemd) IsActive(serviceName string) (bool, error) { + _, err := systemctlCmd("--root", s.rootDir, "is-active", serviceName) + if err == nil { + return true, nil + } + // "systemctl is-enabled " prints `inactive\n` to stderr and returns exit code 1 for inactive services + sysdErr, ok := err.(*Error) + if ok && sysdErr.exitCode > 0 && strings.TrimSpace(string(sysdErr.msg)) == "inactive" { + return false, nil + } + return false, err +} + // Stop the given service, and wait until it has stopped. func (s *systemd) Stop(serviceName string, timeout time.Duration) error { if _, err := systemctlCmd("stop", serviceName); err != nil { diff --git a/wrappers/core18.go b/wrappers/core18.go index 23fb1c7806b..ade6ced5df2 100644 --- a/wrappers/core18.go +++ b/wrappers/core18.go @@ -172,11 +172,11 @@ func writeSnapdServicesOnCore(s *snap.Info, inter interacter) error { // snapd.socket because doing that would mean that the // snapd.seeded.service is also stopped which confuses the // boot order (the unit exists before we are fully seeded) - statuses, err := sysd.Status(unit) + isActive, err := sysd.IsActive(unit) if err != nil { return err } - if statuses[0].Active { + if isActive { if err := sysd.Restart(unit, 5*time.Second); err != nil { return err } diff --git a/wrappers/core18_test.go b/wrappers/core18_test.go index e1c8e125209..ea02d72add9 100644 --- a/wrappers/core18_test.go +++ b/wrappers/core18_test.go @@ -128,7 +128,9 @@ WantedBy=snapd.service {"--root", dirs.GlobalRootDir, "enable", "snapd.autoimport.service"}, {"--root", dirs.GlobalRootDir, "enable", "snapd.service"}, {"--root", dirs.GlobalRootDir, "enable", "snapd.system-shutdown.service"}, - {"show", "--property=Id,ActiveState,UnitFileState,Type", "snapd.autoimport.service"}, + {"--root", dirs.GlobalRootDir, "is-active", "snapd.autoimport.service"}, + {"stop", "snapd.autoimport.service"}, + {"show", "--property=ActiveState", "snapd.autoimport.service"}, {"start", "snapd.autoimport.service"}, {"start", "snapd.service"}, {"start", "--no-block", "snapd.seeded.service"}, From c0ec65d0a089579fcc7aa9e22f8abd5f6f0e295c Mon Sep 17 00:00:00 2001 From: Samuele Pedroni Date: Mon, 3 Dec 2018 10:31:12 +0200 Subject: [PATCH 137/580] set now needed privacy key also in the InstallMany case --- overlord/snapstate/snapstate_test.go | 2 ++ overlord/snapstate/storehelpers.go | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 47666b8f39c..2e3665df463 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -10277,6 +10277,8 @@ func (s *snapmgrTestSuite) TestInstallMany(c *C) { c.Assert(tts, HasLen, 2) c.Check(installed, DeepEquals, []string{"one", "two"}) + c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true) + for i, ts := range tts { verifyInstallTasks(c, 0, 0, ts, s.state) // check that tasksets are in separate lanes diff --git a/overlord/snapstate/storehelpers.go b/overlord/snapstate/storehelpers.go index 35bfa2e2e7b..3458c77862d 100644 --- a/overlord/snapstate/storehelpers.go +++ b/overlord/snapstate/storehelpers.go @@ -457,6 +457,11 @@ func installCandidates(st *state.State, names []string, channel string, user *au return nil, err } + opts, err := refreshOptions(st, nil) + if err != nil { + return nil, err + } + actions := make([]*store.SnapAction, len(names)) for i, name := range names { actions[i] = &store.SnapAction{ @@ -470,5 +475,5 @@ func installCandidates(st *state.State, names []string, channel string, user *au theStore := Store(st) st.Unlock() // calls to the store should be done without holding the state lock defer st.Lock() - return theStore.SnapAction(context.TODO(), curSnaps, actions, user, nil) + return theStore.SnapAction(context.TODO(), curSnaps, actions, user, opts) } From 5aba5ec303c4fcd5dc2cbf0cbefa9eda8008ec13 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 14 Dec 2018 07:35:09 +0100 Subject: [PATCH 138/580] releasing package snapd version 2.36.3 --- packaging/arch/PKGBUILD | 2 +- packaging/fedora/snapd.spec | 20 +++++++++++++++++++- packaging/opensuse/snapd.changes | 5 +++++ packaging/opensuse/snapd.spec | 2 +- packaging/ubuntu-14.04/changelog | 20 ++++++++++++++++++++ packaging/ubuntu-16.04/changelog | 20 ++++++++++++++++++++ 6 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 7249e2a492c..b9e37215b0c 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -10,7 +10,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.36.2 +pkgver=2.36.3 pkgrel=1 arch=('x86_64') url="https://github.com/snapcore/snapd" diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index a4a5803929b..9cb2cedbf5e 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -92,7 +92,7 @@ %endif Name: snapd -Version: 2.36.2 +Version: 2.36.3 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -823,7 +823,25 @@ fi %endif %changelog +* Fri Dec 14 2018 Michael Vogt +- New upstream release 2.36.3 + - wrappers: use new systemd.IsActive in core18 early boot + - httputil: retry on temporary net errors + - wrappers: only restart service in core18 when they are active + - systemd: start snapd.autoimport.service in --no-block mode + - data/selinux: fix syntax error in definition of snappy_admin + interfacewhen installing selinux-policy-devel package. + - centos: enable SELinux support on CentOS 7 + - cmd, dirs, interfaces/apparmor: update distro identification to + support ID="archlinux" + - apparmor: allow hard link to snap-specific semaphore files + - overlord,apparmor: new syskey behaviour + non-ignored snap-confine + profile errors + - snap: add new `snap run --trace-exec` call + - interfaces/backends: detect too old apparmor_parser + * Thu Nov 29 2018 Michael Vogt +- New upstream release 2.36.2 - daemon, vendor: bump github.com/coreos/go-systemd/activation, handle API changes - snapstate: update fontconfig caches on install diff --git a/packaging/opensuse/snapd.changes b/packaging/opensuse/snapd.changes index f6f62be03e0..4e7c57336be 100644 --- a/packaging/opensuse/snapd.changes +++ b/packaging/opensuse/snapd.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Dec 14 07:30:58 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.36.3 + ------------------------------------------------------------------- Thu Nov 29 10:48:29 UTC 2018 - mvo@fastmail.fm diff --git a/packaging/opensuse/snapd.spec b/packaging/opensuse/snapd.spec index ad267c40a6f..92ab4afa04c 100644 --- a/packaging/opensuse/snapd.spec +++ b/packaging/opensuse/snapd.spec @@ -61,7 +61,7 @@ %global snap_mount_dir /snap Name: snapd -Version: 2.36.2 +Version: 2.36.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 910ba6a93b7..0f32074316d 100644 --- a/packaging/ubuntu-14.04/changelog +++ b/packaging/ubuntu-14.04/changelog @@ -1,3 +1,23 @@ +snapd (2.36.3~14.04) trusty; urgency=medium + + * New upstream release, LP: #1795590 + - wrappers: use new systemd.IsActive in core18 early boot + - httputil: retry on temporary net errors + - wrappers: only restart service in core18 when they are active + - systemd: start snapd.autoimport.service in --no-block mode + - data/selinux: fix syntax error in definition of snappy_admin + interfacewhen installing selinux-policy-devel package. + - centos: enable SELinux support on CentOS 7 + - cmd, dirs, interfaces/apparmor: update distro identification to + support ID="archlinux" + - apparmor: allow hard link to snap-specific semaphore files + - overlord,apparmor: new syskey behaviour + non-ignored snap-confine + profile errors + - snap: add new `snap run --trace-exec` call + - interfaces/backends: detect too old apparmor_parser + + -- Michael Vogt Fri, 14 Dec 2018 07:30:58 +0100 + snapd (2.36.2~14.04) trusty; urgency=medium * New upstream release, LP: #1795590 diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 38d67d66dcd..47c89e3f28a 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,23 @@ +snapd (2.36.3) xenial; urgency=medium + + * New upstream release, LP: #1795590 + - wrappers: use new systemd.IsActive in core18 early boot + - httputil: retry on temporary net errors + - wrappers: only restart service in core18 when they are active + - systemd: start snapd.autoimport.service in --no-block mode + - data/selinux: fix syntax error in definition of snappy_admin + interfacewhen installing selinux-policy-devel package. + - centos: enable SELinux support on CentOS 7 + - cmd, dirs, interfaces/apparmor: update distro identification to + support ID="archlinux" + - apparmor: allow hard link to snap-specific semaphore files + - overlord,apparmor: new syskey behaviour + non-ignored snap-confine + profile errors + - snap: add new `snap run --trace-exec` call + - interfaces/backends: detect too old apparmor_parser + + -- Michael Vogt Fri, 14 Dec 2018 07:30:58 +0100 + snapd (2.36.2) xenial; urgency=medium * New upstream release, LP: #1795590 From 39c1b2ccb015a84ac175ab4a8e29eaecefcf2398 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 14 Dec 2018 07:38:55 +0100 Subject: [PATCH 139/580] systemd/systemd.go: add missing tests for systemd.IsActive This adds the missing tests for the new systemd.IsActive code. --- systemd/export_test.go | 8 ++++++++ systemd/systemd.go | 2 +- systemd/systemd_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/systemd/export_test.go b/systemd/export_test.go index 2ccf45c9945..3a4cfcc0617 100644 --- a/systemd/export_test.go +++ b/systemd/export_test.go @@ -46,3 +46,11 @@ func MockJournalStdoutPath(path string) func() { journalStdoutPath = oldPath } } + +func (e *Error) SetExitCode(i int) { + e.exitCode = i +} + +func (e *Error) SetMsg(msg []byte) { + e.msg = msg +} diff --git a/systemd/systemd.go b/systemd/systemd.go index 0733a1c8172..647b1ec0e96 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -342,7 +342,7 @@ func (s *systemd) IsActive(serviceName string) (bool, error) { if err == nil { return true, nil } - // "systemctl is-enabled " prints `inactive\n` to stderr and returns exit code 1 for inactive services + // "systemctl is-active " prints `inactive\n` to stderr and returns exit code 1 for inactive services sysdErr, ok := err.(*Error) if ok && sysdErr.exitCode > 0 && strings.TrimSpace(string(sysdErr.msg)) == "inactive" { return false, nil diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 9924d3522d8..0963705158e 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -641,3 +641,35 @@ func (s *SystemdTestSuite) TestJctl(c *C) { c.Assert(err, IsNil) c.Check(args, DeepEquals, []string{"-o", "json", "--no-pager", "--no-tail", "-u", "foo", "-u", "bar"}) } + +func (s *SystemdTestSuite) TestIsActiveIsInactive(c *C) { + sysErr := &Error{} + sysErr.SetExitCode(1) + sysErr.SetMsg([]byte("inactive\n")) + s.errors = []error{sysErr} + + active, err := New("xyzzy", s.rep).IsActive("foo") + c.Assert(active, Equals, false) + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) +} + +func (s *SystemdTestSuite) TestIsActiveIsActive(c *C) { + s.errors = []error{nil} + + active, err := New("xyzzy", s.rep).IsActive("foo") + c.Assert(active, Equals, true) + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) +} + +func (s *SystemdTestSuite) TestIsActiveErr(c *C) { + sysErr := &Error{} + sysErr.SetExitCode(1) + sysErr.SetMsg([]byte("random-failure\n")) + s.errors = []error{sysErr} + + active, err := New("xyzzy", s.rep).IsActive("foo") + c.Assert(active, Equals, false) + c.Assert(err, ErrorMatches, ".* failed with exit status 1: random-failure\n") +} From a045a192f520e0f7fa0fbd8e21fabfa0894fb86a Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 14 Dec 2018 10:35:19 +0100 Subject: [PATCH 140/580] interfaces/backends: make use of UnusableAppArmor The 2.36.x release branch was using PartialAppArmor coupled with additional check for lack of parser features to know that apparmor ought to be disabled inside snapd. Meanwhile in master a new level, UnusableAppArmor, was introduced to express that. This patch reconciles the code to be in sync with master. While I was at it, I added a test for UnusableAppArmor level. Signed-off-by: Zygmunt Krynicki --- interfaces/backends/backends.go | 6 +----- interfaces/backends/backends_test.go | 13 +++---------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/interfaces/backends/backends.go b/interfaces/backends/backends.go index 4dbb870193f..0b8c7cc7d59 100644 --- a/interfaces/backends/backends.go +++ b/interfaces/backends/backends.go @@ -67,11 +67,7 @@ func backends() []interfaces.SecurityBackend { // When some features are missing the backend will generate more permissive // profiles that keep applications operational, in forced-devmode. switch release.AppArmorLevel() { - case release.PartialAppArmor: - if len(release.AppArmorParserFeatures()) != 0 { - all = append(all, &apparmor.Backend{}) - } - case release.FullAppArmor: + case release.PartialAppArmor, release.FullAppArmor: all = append(all, &apparmor.Backend{}) } return all diff --git a/interfaces/backends/backends_test.go b/interfaces/backends/backends_test.go index b8d71c351f5..9016f633579 100644 --- a/interfaces/backends/backends_test.go +++ b/interfaces/backends/backends_test.go @@ -36,7 +36,7 @@ type backendsSuite struct{} var _ = Suite(&backendsSuite{}) func (s *backendsSuite) TestIsAppArmorEnabled(c *C) { - for _, level := range []release.AppArmorLevelType{release.NoAppArmor, release.PartialAppArmor, release.FullAppArmor} { + for _, level := range []release.AppArmorLevelType{release.NoAppArmor, release.UnusableAppArmor, release.PartialAppArmor, release.FullAppArmor} { restore := release.MockAppArmorLevel(level) defer restore() @@ -46,16 +46,9 @@ func (s *backendsSuite) TestIsAppArmorEnabled(c *C) { names[i] = string(backend.Name()) } switch level { - case release.NoAppArmor: + case release.NoAppArmor, release.UnusableAppArmor: c.Assert(names, Not(testutil.Contains), "apparmor") - case release.PartialAppArmor: - // Partial support depends on modern apparmor_parser. - if len(release.AppArmorParserFeatures()) == 0 { - c.Assert(names, Not(testutil.Contains), "apparmor") - } else { - c.Assert(names, testutil.Contains, "apparmor") - } - case release.FullAppArmor: + case release.PartialAppArmor, release.FullAppArmor: c.Assert(names, testutil.Contains, "apparmor") } From ff9ca0ba96d23df40265102f472e3ae45ca49e9b Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Fri, 14 Dec 2018 12:17:11 +0100 Subject: [PATCH 141/580] addHotplugSeqWaitTask helper. --- overlord/ifacestate/export_test.go | 1 + overlord/ifacestate/helpers.go | 16 ++++++++++++++ overlord/ifacestate/helpers_test.go | 34 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/overlord/ifacestate/export_test.go b/overlord/ifacestate/export_test.go index 0cd25edb87d..f818cbc357c 100644 --- a/overlord/ifacestate/export_test.go +++ b/overlord/ifacestate/export_test.go @@ -51,6 +51,7 @@ var ( GetHotplugChangeAttrs = getHotplugChangeAttrs SetHotplugChangeAttrs = setHotplugChangeAttrs AllocHotplugSeq = allocHotplugSeq + AddHotplugSeqWaitTask = addHotplugSeqWaitTask ) func NewConnectOptsWithAutoSet() connectOpts { diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go index d1cddc45af2..a4f6dc67455 100644 --- a/overlord/ifacestate/helpers.go +++ b/overlord/ifacestate/helpers.go @@ -888,6 +888,22 @@ func setHotplugChangeAttrs(chg *state.Change, seq int, hotplugKey string) { chg.Set("hotplug-key", hotplugKey) } +// addHotplugSeqWaitTask sets mandatory hotplug attributes on the hotplug change, adds "hotplug-seq-wait" task +// and makes all existing tasks of the change wait for it. +func addHotplugSeqWaitTask(hotplugChange *state.Change, hotplugKey string) error { + st := hotplugChange.State() + seq, err := allocHotplugSeq(st) + if err != nil { + return fmt.Errorf("internal error: cannot allocate hotplug sequence number: %s", err) + } + setHotplugChangeAttrs(hotplugChange, seq, hotplugKey) + seqControl := st.NewTask("hotplug-seq-wait", fmt.Sprintf("Serialize hotplug change for hotplug key %q", hotplugKey)) + tss := state.NewTaskSet(hotplugChange.Tasks()...) + tss.WaitFor(seqControl) + hotplugChange.AddTask(seqControl) + return nil +} + type HotplugSlotInfo struct { Name string `json:"name"` Interface string `json:"interface"` diff --git a/overlord/ifacestate/helpers_test.go b/overlord/ifacestate/helpers_test.go index 48abf39dcde..17a8a2bc09a 100644 --- a/overlord/ifacestate/helpers_test.go +++ b/overlord/ifacestate/helpers_test.go @@ -489,3 +489,37 @@ func (s *helpersSuite) TestAllocHotplugSeq(c *C) { c.Assert(s.st.Get("hotplug-seq", &stateSeq), IsNil) c.Check(stateSeq, Equals, 2) } + +func (s *helpersSuite) TestAddHotplugSeqWaitTask(c *C) { + s.st.Lock() + defer s.st.Unlock() + + chg := s.st.NewChange("foo", "") + t1 := s.st.NewTask("task1", "") + t2 := s.st.NewTask("task2", "") + chg.AddTask(t1) + chg.AddTask(t2) + + c.Assert(ifacestate.AddHotplugSeqWaitTask(chg, "1234"), IsNil) + // hotplug change got an extra task + c.Assert(chg.Tasks(), HasLen, 3) + seq, key, err := ifacestate.GetHotplugChangeAttrs(chg) + c.Assert(err, IsNil) + c.Check(seq, Equals, 1) + c.Check(key, Equals, "1234") + + var seqTask *state.Task + for _, t := range chg.Tasks() { + if t.Kind() == "hotplug-seq-wait" { + seqTask = t + break + } + } + c.Assert(seqTask, NotNil) + + // existing tasks wait for the hotplug-seq-wait task + c.Assert(t1.WaitTasks(), HasLen, 1) + c.Assert(t1.WaitTasks()[0].ID(), Equals, seqTask.ID()) + c.Assert(t2.WaitTasks(), HasLen, 1) + c.Assert(t2.WaitTasks()[0].ID(), Equals, seqTask.ID()) +} From a197e63ae8e30ef4d7c6eb172bbeb12d20d5193e Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 14 Dec 2018 12:32:27 +0100 Subject: [PATCH 142/580] Update cmd/libsnap-confine-private/tool.c --- cmd/libsnap-confine-private/tool.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/libsnap-confine-private/tool.c b/cmd/libsnap-confine-private/tool.c index 04f19c01a65..fc67ebb2447 100644 --- a/cmd/libsnap-confine-private/tool.c +++ b/cmd/libsnap-confine-private/tool.c @@ -105,7 +105,7 @@ void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, snap_name); const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); - char xdg_runtime_dir_env[PATH_MAX]; + char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")]; if (xdg_runtime_dir != NULL) { sc_must_snprintf(xdg_runtime_dir_env, sizeof(xdg_runtime_dir_env), From b296497d4ac4e1948b6a4699bdadf691cdc78b77 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Tue, 27 Nov 2018 16:00:38 +0100 Subject: [PATCH 143/580] interfaces: return security setup errors Snapd used to ignore errors from backend.Setup calls and within apparmor backend it also used to ignore errors from a special code path generating snap-confine profile for when snap-confine is used from {core,snapd} snaps. We are observing a peculiar error, where snapd is trying to compile and load the profile for snap-confine, an operation involving executing apparmor_parser process, but that operation fails because apparmor parser is killed with SIGTERM. The origin of the signal is unknown but being vocal about the problem occurring might help us understand the problem better. The problem is only visible in log files as a following message: Nov 27 10:58:09 nov271039-251208 snapd[24167]: backend.go:312: cannot create host snap-confine apparmor configuration: cannot reload snap-confine apparmor profile: cannot load apparmor profiles: signal: terminated There is no output from apparmor_parser. Note: this issue is very likely not new. The existing test setup would mask it. It is only visible now because snapd in 2.36 release branch requires more permissions to execute snap-confine than it does with edge. In the past if this problem had occurred the test machine would carry on running with the apparmor profile from the edge channel. Signed-off-by: Zygmunt Krynicki --- interfaces/apparmor/backend.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index aa0f911688f..968f245f3b4 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -305,11 +305,9 @@ func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, spec.(*Specification).AddLayout(snapInfo) // core on classic is special - // - // TODO: we need to deal with the "snapd" snap here soon if snapName == "core" && release.OnClassic && release.AppArmorLevel() != release.NoAppArmor { if err := setupSnapConfineReexec(snapInfo); err != nil { - logger.Noticef("cannot create host snap-confine apparmor configuration: %s", err) + return fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) } } @@ -318,7 +316,7 @@ func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, // systems but /etc/apparmor.d is not writable on core18 systems if snapName == "snapd" && release.AppArmorLevel() != release.NoAppArmor { if err := setupSnapConfineReexec(snapInfo); err != nil { - logger.Noticef("cannot create host snap-confine apparmor configuration: %s", err) + return fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err) } } From 00e3377bfb60d6e5a0979ed008d55bd88519f7d4 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Fri, 14 Dec 2018 13:19:03 +0100 Subject: [PATCH 144/580] test checkInstallPreconditions a bit more Co-Authored-By: pedronis --- overlord/snapstate/snapstate_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 6628fff8dd3..228adc78312 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -10295,6 +10295,9 @@ func (s *snapmgrTestSuite) TestInstallManyChecksPreconditions(c *C) { _, _, err := snapstate.InstallMany(s.state, []string{"some-snap-now-classic"}, 0) c.Assert(err, NotNil) c.Check(err, DeepEquals, &snapstate.SnapNeedsClassicError{Snap: "some-snap-now-classic"}) + + _, _, err = snapstate.InstallMany(s.state, []string{"some-snap_foo"}, 0) + c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.parallel-instances' to true") } func verifyStopReason(c *C, ts *state.TaskSet, reason string) { From 29e2da55895ac39d5cdab0cc394fb75c4a592a44 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Fri, 14 Dec 2018 14:46:37 +0100 Subject: [PATCH 145/580] Use retry,Reason in hotplug disconnect conflict checks. --- overlord/ifacestate/handlers.go | 19 ++++++++----------- overlord/ifacestate/ifacestate_test.go | 7 ++----- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 34b5f23990b..d60a5467632 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -809,8 +809,11 @@ func checkHotplugDisconnectConflicts(st *state.State, plugSnap, slotSnap string) if err != nil { return err } - if plugRef.Snap == plugSnap || slotRef.Snap == slotSnap { - return &state.Retry{After: connectRetryTimeout} + if plugRef.Snap == plugSnap { + return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting plug snap %s, task %q", plugSnap, k)} + } + if slotRef.Snap == slotSnap { + return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting slot snap %s, task %q", slotSnap, k)} } continue } @@ -829,7 +832,7 @@ func checkHotplugDisconnectConflicts(st *state.State, plugSnap, slotSnap string) if k == "link-snap" || k == "setup-profiles" || k == "unlink-snap" { // other snap is getting installed/refreshed/removed - temporary conflict - return &state.Retry{After: connectRetryTimeout} + return &state.Retry{After: connectRetryTimeout, Reason: fmt.Sprintf("conflicting snap %s with task %q", otherSnapName, k)} } } return nil @@ -1262,11 +1265,6 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e st.Lock() defer st.Unlock() - syssnap, err := systemSnapInfo(st) - if err != nil { - return err - } - ifaceName, hotplugKey, err := getHotplugAttrs(task) if err != nil { return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) @@ -1283,9 +1281,8 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e // check for conflicts on all connections first before creating disconnect hooks for _, connRef := range connections { if err := checkHotplugDisconnectConflicts(st, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { - if _, retry := err.(*state.Retry); retry { - logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", syssnap.InstanceName(), connRef.PlugRef.Snap, connRef.SlotRef.Snap) - task.Logf("Waiting for conflicting change in progress...") + if retry, ok := err.(*state.Retry); ok { + task.Logf("Waiting for conflicting change in progress: %s", retry.Reason) return err // will retry } return fmt.Errorf("cannot check conflicts when disconnecting interfaces: %s", err) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 8462e939fa2..c34c6b72847 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -5113,10 +5113,7 @@ func (s *interfaceManagerSuite) testHotplugDisconnectWaitsForCoreRefresh(c *C, t chg2 := s.state.NewChange("other-chg", "...") t2 := s.state.NewTask(taskKind, "...") - t2.Set("snap-setup", &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "core"}, - }) + t2.Set("snap-setup", &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "core"}}) chg2.AddTask(t2) t3 := s.state.NewTask("other", "") t2.WaitFor(t3) @@ -5131,7 +5128,7 @@ func (s *interfaceManagerSuite) testHotplugDisconnectWaitsForCoreRefresh(c *C, t s.state.Lock() c.Assert(chg.Err(), IsNil) - c.Assert(strings.Join(t.Log(), ""), Matches, `.*Waiting for conflicting change in progress...`) + c.Assert(strings.Join(t.Log(), ""), Matches, `.*Waiting for conflicting change in progress:.*`) c.Assert(chg.Status(), Equals, state.DoingStatus) t2.SetStatus(state.DoneStatus) From 36f4ab214a2f8c16daec7f7af5e5ec31bd2c13cd Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Fri, 14 Dec 2018 15:35:40 +0100 Subject: [PATCH 146/580] Added test for conflict on disconnect caused by plug side. --- overlord/ifacestate/ifacestate_test.go | 92 +++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index c34c6b72847..3f20ca3f198 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -5107,8 +5107,7 @@ func (s *interfaceManagerSuite) testHotplugDisconnectWaitsForCoreRefresh(c *C, t chg := s.state.NewChange("hotplug change", "") t := s.state.NewTask("hotplug-disconnect", "") - t.Set("hotplug-key", "1234") - t.Set("interface", "test") + ifacestate.SetHotplugAttrs(t, "test", "1234") chg.AddTask(t) chg2 := s.state.NewChange("other-chg", "...") @@ -5157,6 +5156,95 @@ func (s *interfaceManagerSuite) TestHotplugDisconnectWaitsForCoreUnlinkSnap(c *C s.testHotplugDisconnectWaitsForCoreRefresh(c, "unlink-snap") } +func (s *interfaceManagerSuite) TestHotplugDisconnectWaitsForDisconnectPlug(c *C) { + coreInfo := s.mockSnap(c, coreSnapYaml) + + repo := s.manager(c).Repository() + err := repo.AddInterface(&ifacetest.TestInterface{ + InterfaceName: "test", + }) + c.Assert(err, IsNil) + err = repo.AddSlot(&snap.SlotInfo{ + Snap: coreInfo, + Name: "hotplugslot", + Interface: "test", + HotplugKey: "1234", + }) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + // mock the consumer + si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)} + testSnap := snaptest.MockSnapInstance(c, "", consumerYaml, si) + c.Assert(testSnap.Plugs["plug"], NotNil) + c.Assert(repo.AddPlug(testSnap.Plugs["plug"]), IsNil) + snapstate.Set(s.state, "consumer", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + }}) + conn, err := repo.Connect(&interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "core", Name: "hotplugslot"}}, + nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + hotplugChg := s.state.NewChange("hotplug change", "") + hotplugDisconnect := s.state.NewTask("hotplug-disconnect", "") + ifacestate.SetHotplugAttrs(hotplugDisconnect, "test", "1234") + hotplugChg.AddTask(hotplugDisconnect) + + disconnectChg := s.state.NewChange("disconnect change", "...") + disconnectTs, err := ifacestate.Disconnect(s.state, conn) + c.Assert(err, IsNil) + disconnectChg.AddAll(disconnectTs) + + holdingTask := s.state.NewTask("other", "") + disconnectTs.WaitFor(holdingTask) + holdingTask.SetStatus(state.HoldStatus) + disconnectChg.AddTask(holdingTask) + + s.state.Unlock() + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + c.Assert(hotplugChg.Err(), IsNil) + + c.Assert(strings.Join(hotplugDisconnect.Log(), ""), Matches, `.*Waiting for conflicting change in progress: conflicting plug snap consumer.*`) + c.Assert(hotplugChg.Status(), Equals, state.DoingStatus) + + for _, t := range disconnectTs.Tasks() { + t.SetStatus(state.DoneStatus) + } + holdingTask.SetStatus(state.DoneStatus) + + s.state.Unlock() + for i := 0; i < 3; i++ { + s.se.Ensure() + s.se.Wait() + } + s.state.Lock() + + c.Assert(hotplugChg.Err(), IsNil) + c.Assert(hotplugChg.Status(), Equals, state.DoneStatus) +} + func (s *interfaceManagerSuite) TestHotplugSeqWaitTasks(c *C) { var order []int _ = s.manager(c) From d79b561b37bc2d0db0f6d7053fc8c28eff323f78 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Fri, 14 Dec 2018 15:55:38 +0100 Subject: [PATCH 147/580] image: do not write empty etc/cloud The `snap --prepare-image` command will currently look into the gadget and create a /etc/cloud/cloud.cfg configuration if the gadget contains a "cloud.conf" file in cloud-init format. But even if there is no such file an empty /etc/cloud directory is created. This empty directory ends up on /writable/system-data/etc/cloud and that will block the initial boot from populating that dir with the data from the core18 /etc/cloud directory when this dir is in "transition" mode in the /etc/system-image/writable-paths. As a first step we should not create this /etc/cloud directory if there is nothing in it. This means we will be able to revert: https://github.com/snapcore/core18/pull/106 Once that is done we need to also support ds-identify.conf or switch to a cloud.conf/ directory instead of the current file only approach. The reason is that we do not want to merge the cloud config from core18 and from the gadget as this is messy. Instead it should either come from the gadget or from the core18. --- image/image.go | 14 ++++++-------- image/image_test.go | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/image/image.go b/image/image.go index c7b9df1608c..0e6ca2f7cf9 100644 --- a/image/image.go +++ b/image/image.go @@ -287,19 +287,17 @@ func makeFetcher(tsto *ToolingStore, dlOpts *DownloadOptions, db *asserts.Databa } func installCloudConfig(gadgetDir string) error { - var err error + cloudConfig := filepath.Join(gadgetDir, "cloud.conf") + if !osutil.FileExists(cloudConfig) { + return nil + } cloudDir := filepath.Join(dirs.GlobalRootDir, "/etc/cloud") if err := os.MkdirAll(cloudDir, 0755); err != nil { return err } - - cloudConfig := filepath.Join(gadgetDir, "cloud.conf") - if osutil.FileExists(cloudConfig) { - dst := filepath.Join(cloudDir, "cloud.cfg") - err = osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite) - } - return err + dst := filepath.Join(cloudDir, "cloud.cfg") + return osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite) } // defaultCore is used if no base is specified by the model diff --git a/image/image_test.go b/image/image_test.go index 1c2c87696bd..002aafc12a7 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -990,7 +990,7 @@ func (s *imageSuite) TestInstallCloudConfigNoConfig(c *C) { dirs.SetRootDir(targetDir) err := image.InstallCloudConfig(emptyGadgetDir) c.Assert(err, IsNil) - c.Check(osutil.FileExists(filepath.Join(targetDir, "etc/cloud")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(targetDir, "etc/cloud")), Equals, false) } func (s *imageSuite) TestInstallCloudConfigWithCloudConfig(c *C) { From 4329c717f8cf1703fdf477dfa4caffbd66dc1fbe Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Fri, 14 Dec 2018 20:21:58 +0100 Subject: [PATCH 148/580] spread: disable fedora-29-64 We're seeing dnf synchronization failures related to the modularity repository. Since this breaks all builds let's disable Fedora 29 for now. --- spread.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spread.yaml b/spread.yaml index 67439975fe5..9524ea3273d 100644 --- a/spread.yaml +++ b/spread.yaml @@ -81,6 +81,8 @@ backends: manual: true - fedora-29-64: workers: 4 + # https://twitter.com/zygoon/status/1073629342884864000 + manual: true - opensuse-42.3-64: workers: 4 # golang stack cannot compile anything, needs investigation @@ -117,6 +119,8 @@ backends: workers: 1 - fedora-29-64: workers: 1 + # https://twitter.com/zygoon/status/1073629342884864000 + manual: true - arch-linux-64: workers: 1 - amazon-linux-2-64: From ac86c7c8ba30da9c63dff6689a156c3cd5c54068 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Mon, 17 Dec 2018 09:11:08 +0100 Subject: [PATCH 149/580] cmd/snap-discard-ns: fix umount(2) typo Signed-off-by: Maciej Borzecki --- cmd/snap-discard-ns/snap-discard-ns.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/snap-discard-ns/snap-discard-ns.c b/cmd/snap-discard-ns/snap-discard-ns.c index dee3a3a202c..b81678530e5 100644 --- a/cmd/snap-discard-ns/snap-discard-ns.c +++ b/cmd/snap-discard-ns/snap-discard-ns.c @@ -88,7 +88,7 @@ int main(int argc, char** argv) { } /* Move to the namespace directory. This is used so that we don't need to - * traverse the path over and over in our upcoming unmount2(2) calls. */ + * traverse the path over and over in our upcoming umount2(2) calls. */ if (fchdir(ns_dir_fd) < 0) { die("cannot move to directory %s", ns_dir_path); } From 509449bf7a0f5f845b53187d8d9b08e6371c17dd Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Mon, 17 Dec 2018 11:22:51 +0100 Subject: [PATCH 150/580] Adjusted error messages (thanks bboozzoo). --- overlord/ifacestate/handlers.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 758eedc47ae..31ded308aac 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1273,17 +1273,17 @@ func (m *InterfaceManager) doHotplugRemoveSlot(task *state.Task, _ *tomb.Tomb) e slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) if err != nil { - return fmt.Errorf("cannot determine slots: %s", err) + return fmt.Errorf("internal error: cannot determine slots: %v", err) } if slot != nil { if err := m.repo.RemoveSlot(slot.Snap.InstanceName(), slot.Name); err != nil { - return fmt.Errorf("cannot remove slot %s of snap %q: %s", slot.Snap.InstanceName(), slot.Name, err) + return fmt.Errorf("cannot remove hotplug slot: %v", err) } } stateSlots, err := getHotplugSlots(st) if err != nil { - return fmt.Errorf("internal error obtaining hotplug slots: %v", err.Error()) + return fmt.Errorf("internal error: cannot obtain hotplug slots: %v", err) } // remove the slot from hotplug-slots in the state as long as there are no connections referencing it, From d4c308243302c6efb0e9283bcd0505fc42fe5124 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Mon, 17 Dec 2018 16:27:12 +0100 Subject: [PATCH 151/580] Fix missing err check. --- overlord/ifacestate/handlers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 94474d0ed67..25bfb7dad2e 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1450,6 +1450,9 @@ func (m *InterfaceManager) autoconnectNewDevice(task *state.Task, ifaceName, hot st := task.State() autochecker, err := newAutoConnectChecker(st) + if err != nil { + return err + } instanceName := slot.Snap.InstanceName() candidates := m.repo.AutoConnectCandidatePlugs(instanceName, slot.Name, autochecker.check) From aea157ce6c88535a648cc26fa4f731cbc8cc5022 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Mon, 17 Dec 2018 16:38:21 +0100 Subject: [PATCH 152/580] Include interface name in the hotplug-disconnect task summary. --- overlord/ifacestate/hotplug.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overlord/ifacestate/hotplug.go b/overlord/ifacestate/hotplug.go index 3ceea970167..fb0a05ff08f 100644 --- a/overlord/ifacestate/hotplug.go +++ b/overlord/ifacestate/hotplug.go @@ -180,7 +180,7 @@ func suggestedSlotName(devinfo *hotplug.HotplugDeviceInfo, fallbackName string) // updateDevice creates tasks to disconnect slots of given device, update the slot in the repository, then connect it back. func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[string]interface{}) *state.TaskSet { - hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of device %q", hotplugKey)) + hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections for interface %s, hotplug key %q", ifaceName, hotplugKey)) setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey) updateSlot := st.NewTask("hotplug-update-slot", fmt.Sprintf("Update slot of interface %s, hotplug key %q", ifaceName, hotplugKey)) @@ -188,7 +188,7 @@ func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[st updateSlot.Set("slot-attrs", newAttrs) updateSlot.WaitFor(hotplugDisconnect) - hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %s hotplug key %s", ifaceName, hotplugKey)) + hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %s hotplug key %q", ifaceName, hotplugKey)) setHotplugAttrs(hotplugConnect, ifaceName, hotplugKey) hotplugConnect.WaitFor(updateSlot) From 1a6e19b18203fd521b9cb0ed0860ea0f97fea66e Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Mon, 17 Dec 2018 16:44:53 +0100 Subject: [PATCH 153/580] for -> of. --- overlord/ifacestate/hotplug.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/overlord/ifacestate/hotplug.go b/overlord/ifacestate/hotplug.go index fb0a05ff08f..ca7957f69fe 100644 --- a/overlord/ifacestate/hotplug.go +++ b/overlord/ifacestate/hotplug.go @@ -180,7 +180,7 @@ func suggestedSlotName(devinfo *hotplug.HotplugDeviceInfo, fallbackName string) // updateDevice creates tasks to disconnect slots of given device, update the slot in the repository, then connect it back. func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[string]interface{}) *state.TaskSet { - hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections for interface %s, hotplug key %q", ifaceName, hotplugKey)) + hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of interface %s, hotplug key %q", ifaceName, hotplugKey)) setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey) updateSlot := st.NewTask("hotplug-update-slot", fmt.Sprintf("Update slot of interface %s, hotplug key %q", ifaceName, hotplugKey)) @@ -198,7 +198,7 @@ func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[st // removeDevice creates tasks to disconnect slots of given device and remove affected slots. func removeDevice(st *state.State, ifaceName, hotplugKey string) *state.TaskSet { // hotplug-disconnect task will create hooks and disconnect the slot - hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections for interface %s, hotplug key %q", ifaceName, hotplugKey)) + hotplugDisconnect := st.NewTask("hotplug-disconnect", fmt.Sprintf("Disable connections of interface %s, hotplug key %q", ifaceName, hotplugKey)) setHotplugAttrs(hotplugDisconnect, ifaceName, hotplugKey) // hotplug-remove-slot will remove this device's slot from the repository. From a83fb8dc55cb5517a24a49d0314fcf4c14f4db2a Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Mon, 17 Dec 2018 20:33:45 +0000 Subject: [PATCH 154/580] drop unneeded s.parserCmd.ForgetCalls(). Thanks mvo --- interfaces/apparmor/backend_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index c33f21c0eaa..7cd9555e70e 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -1730,8 +1730,6 @@ func (s *backendSuite) TestHomeIxRule(c *C) { } snapInfo := s.InstallSnap(c, tc.opts, "", ifacetest.SambaYamlV1, 1) - s.parserCmd.ForgetCalls() - profile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") data, err := ioutil.ReadFile(profile) c.Assert(err, IsNil) From 62fae94d310714769ac07e6e8c7d97c23860557b Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Mon, 17 Dec 2018 20:34:40 +0000 Subject: [PATCH 155/580] update for recent changes to AppArmorParserFeatures --- interfaces/builtin/docker_support.go | 3 ++- interfaces/builtin/docker_support_test.go | 4 ++-- interfaces/builtin/export_test.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index 7f04ff8cb44..3381d03909e 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.go @@ -580,7 +580,8 @@ func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specif var privileged bool _ = plug.Attr("privileged-containers", &privileged) useUnsafe := false - for _, f := range parserFeatures() { + features, _ := parserFeatures() + for _, f := range features { if f == "unsafe" { useUnsafe = true } diff --git a/interfaces/builtin/docker_support_test.go b/interfaces/builtin/docker_support_test.go index bb7b568b591..75e5528e9ba 100644 --- a/interfaces/builtin/docker_support_test.go +++ b/interfaces/builtin/docker_support_test.go @@ -106,7 +106,7 @@ func (s *DockerSupportInterfaceSuite) TestSanitizePlug(c *C) { } func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedTrue(c *C) { - restore := builtin.MockParserFeatures(func() []string { return []string{} }) + restore := builtin.MockParserFeatures(func() ([]string, error) { return []string{}, nil }) defer restore() var mockSnapYaml = []byte(`name: docker @@ -229,7 +229,7 @@ apps: c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) for _, scenario := range changeProfileScenarios { - restore := builtin.MockParserFeatures(func() []string { return scenario.features }) + restore := builtin.MockParserFeatures(func() ([]string, error) { return scenario.features, nil }) defer restore() apparmorSpec := &apparmor.Specification{} diff --git a/interfaces/builtin/export_test.go b/interfaces/builtin/export_test.go index 5d41b1c6591..807ed5d9af2 100644 --- a/interfaces/builtin/export_test.go +++ b/interfaces/builtin/export_test.go @@ -103,7 +103,7 @@ func MockOsGetenv(mock func(string) string) (restore func()) { return restore } -func MockParserFeatures(f func() []string) (resture func()) { +func MockParserFeatures(f func() ([]string, error)) (restore func()) { old := parserFeatures parserFeatures = f return func() { From e6dd66b2e5390c0e13946bd01e2660233fe3dd8d Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Mon, 17 Dec 2018 23:12:51 -0300 Subject: [PATCH 156/580] skip snapd snap on reser for core systems --- tests/lib/reset.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 91eb02ce489..2d8738c1820 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -123,7 +123,7 @@ reset_all_snap() { for snap in "$SNAP_MOUNT_DIR"/*; do snap="${snap:6}" case "$snap" in - "bin" | "$gadget_name" | "$kernel_name" | "$core_name" | "core" | README) + "bin" | "$gadget_name" | "$kernel_name" | "$core_name" | "core" | "snapd" |README) ;; *) # make sure snapd is running before we attempt to remove snaps, in case a test stopped it From d103fe04135f760f228dc9e995717c7053506d60 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Tue, 18 Dec 2018 09:07:47 +0100 Subject: [PATCH 157/580] Fix missing comma. --- overlord/ifacestate/hotplug.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/overlord/ifacestate/hotplug.go b/overlord/ifacestate/hotplug.go index ca7957f69fe..2f77078984f 100644 --- a/overlord/ifacestate/hotplug.go +++ b/overlord/ifacestate/hotplug.go @@ -188,7 +188,7 @@ func updateDevice(st *state.State, ifaceName, hotplugKey string, newAttrs map[st updateSlot.Set("slot-attrs", newAttrs) updateSlot.WaitFor(hotplugDisconnect) - hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %s hotplug key %q", ifaceName, hotplugKey)) + hotplugConnect := st.NewTask("hotplug-connect", fmt.Sprintf("Recreate connections of interface %s, hotplug key %q", ifaceName, hotplugKey)) setHotplugAttrs(hotplugConnect, ifaceName, hotplugKey) hotplugConnect.WaitFor(updateSlot) From 51a9b8a5023e49b37cc70cfdf0d94be7de89d704 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 18 Dec 2018 10:52:46 +0100 Subject: [PATCH 158/580] overlord/snapstate/backend: call fontconfig helpers from the new 'current' When switching between a pre fc-cache core version to the new one, the fc-cache helpers would be called from the old revision of the core snap. This is caused by osutil.CommandFromCore using "$SNAP_MOUNT_DIR/core/current" as the base, which points to the old version until the symlinks are updated. As a result of this bug, with new snapd, when switching from stable core to edge, snapd tries to run fc-cache-v{6,7} helpers from the stable core snap. This could be seen in the logs: link.go:75: cannot update fontconfig cache: cannot get fc-cache-v6 from core: open /var/lib/snapd/snap/core/current/bin/fc-cache-v6: no such file or directory Signed-off-by: Maciej Borzecki --- overlord/snapstate/backend/export_test.go | 12 ++++++ overlord/snapstate/backend/fontconfig.go | 3 +- overlord/snapstate/backend/link.go | 21 +++++----- overlord/snapstate/backend/link_test.go | 49 +++++++++++++++++++++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/overlord/snapstate/backend/export_test.go b/overlord/snapstate/backend/export_test.go index 2ff5ecf906d..648e3f16fe5 100644 --- a/overlord/snapstate/backend/export_test.go +++ b/overlord/snapstate/backend/export_test.go @@ -19,6 +19,10 @@ package backend +import ( + "os/exec" +) + var ( AddMountUnit = addMountUnit RemoveMountUnit = removeMountUnit @@ -31,3 +35,11 @@ func MockUpdateFontconfigCaches(f func() error) (restore func()) { updateFontconfigCaches = oldUpdateFontconfigCaches } } + +func MockCommandFromCore(f func(string, string, ...string) (*exec.Cmd, error)) (restore func()) { + old := commandFromCore + commandFromCore = f + return func() { + commandFromCore = old + } +} diff --git a/overlord/snapstate/backend/fontconfig.go b/overlord/snapstate/backend/fontconfig.go index 578b84713df..57e8cbf845d 100644 --- a/overlord/snapstate/backend/fontconfig.go +++ b/overlord/snapstate/backend/fontconfig.go @@ -27,12 +27,13 @@ import ( ) var updateFontconfigCaches = updateFontconfigCachesImpl +var commandFromCore = osutil.CommandFromCore // updateFontconfigCaches always update the fontconfig caches func updateFontconfigCachesImpl() error { for _, fc := range []string{"fc-cache-v6", "fc-cache-v7"} { // FIXME: also use the snapd snap if available - cmd, err := osutil.CommandFromCore(dirs.SnapMountDir, "/bin/"+fc) + cmd, err := commandFromCore(dirs.SnapMountDir, "/bin/"+fc) if err != nil { return fmt.Errorf("cannot get %s from core: %v", fc, err) } diff --git a/overlord/snapstate/backend/link.go b/overlord/snapstate/backend/link.go index 8543bd557f1..a9d3d196a7a 100644 --- a/overlord/snapstate/backend/link.go +++ b/overlord/snapstate/backend/link.go @@ -68,14 +68,6 @@ func (b Backend) LinkSnap(info *snap.Info, model *asserts.Model) error { return err } - // fontconfig is only relevant on classic - // TODO: consider moving this to a less hidden place - if release.OnClassic { - if err := updateFontconfigCaches(); err != nil { - logger.Noticef("cannot update fontconfig cache: %v", err) - } - } - // XXX/TODO: this needs to be a task with proper undo and tests! if model != nil && !release.OnClassic { bootBase := "core" @@ -90,7 +82,18 @@ func (b Backend) LinkSnap(info *snap.Info, model *asserts.Model) error { } } - return updateCurrentSymlinks(info) + if err := updateCurrentSymlinks(info); err != nil { + return err + } + + // fontconfig is only relevant on classic + // TODO: consider moving this to a less hidden place + if release.OnClassic { + if err := updateFontconfigCaches(); err != nil { + logger.Noticef("cannot update fontconfig cache: %v", err) + } + } + return nil } func (b Backend) StartServices(apps []*snap.AppInfo, meter progress.Meter) error { diff --git a/overlord/snapstate/backend/link_test.go b/overlord/snapstate/backend/link_test.go index 17fe766d654..4ff76264d23 100644 --- a/overlord/snapstate/backend/link_test.go +++ b/overlord/snapstate/backend/link_test.go @@ -23,6 +23,7 @@ import ( "errors" "io/ioutil" "os" + "os/exec" "path/filepath" . "gopkg.in/check.v1" @@ -34,6 +35,7 @@ import ( "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/overlord/snapstate/backend" ) @@ -336,3 +338,50 @@ func (s *linkCleanupSuite) TestLinkRunsUpdateFontconfigCachesClassic(c *C) { } } } + +func (s *linkCleanupSuite) TestLinkRunsUpdateFontconfigCachesCallsFromNewCurrent(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + const yaml = `name: core +version: 1.0 +type: os +` + // old version is 'current' + infoOld := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(11)}) + mountDirOld := infoOld.MountDir() + err := os.Symlink(filepath.Base(mountDirOld), filepath.Join(mountDirOld, "..", "current")) + c.Assert(err, IsNil) + + err = os.MkdirAll(filepath.Join(mountDirOld, "bin"), 0755) + c.Assert(err, IsNil) + + oldCmdV6 := testutil.MockCommand(c, filepath.Join(mountDirOld, "bin", "fc-cache-v6"), "") + oldCmdV7 := testutil.MockCommand(c, filepath.Join(mountDirOld, "bin", "fc-cache-v7"), "") + + infoNew := snaptest.MockSnap(c, yaml, &snap.SideInfo{Revision: snap.R(12)}) + mountDirNew := infoNew.MountDir() + + err = os.MkdirAll(filepath.Join(mountDirNew, "bin"), 0755) + c.Assert(err, IsNil) + + newCmdV6 := testutil.MockCommand(c, filepath.Join(mountDirNew, "bin", "fc-cache-v6"), "") + newCmdV7 := testutil.MockCommand(c, filepath.Join(mountDirNew, "bin", "fc-cache-v7"), "") + + // provide our own mock, osutil.CommandFromCore expects an ELF binary + restore = backend.MockCommandFromCore(func(mountDir, name string, args ...string) (*exec.Cmd, error) { + cmd := filepath.Join(mountDir, "core", "current", name) + c.Logf("command from core: %v", cmd) + return exec.Command(cmd, args...), nil + }) + defer restore() + + err = s.be.LinkSnap(infoNew, nil) + c.Assert(err, IsNil) + + c.Check(oldCmdV6.Calls(), HasLen, 0) + c.Check(oldCmdV7.Calls(), HasLen, 0) + + c.Check(newCmdV6.Calls(), HasLen, 1) + c.Check(newCmdV7.Calls(), HasLen, 1) +} From 0b6b5c9b35c7b2a1604eaa79824485b22b45d968 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Tue, 18 Dec 2018 09:14:09 +0100 Subject: [PATCH 159/580] release-tools: display self-help Doing the release once in blue moon, one would appreciate the release tools hand-holding one through the process. --- release-tools/repack-debian-tarball.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/release-tools/repack-debian-tarball.sh b/release-tools/repack-debian-tarball.sh index 9ad6d461e6d..88156ad624b 100755 --- a/release-tools/repack-debian-tarball.sh +++ b/release-tools/repack-debian-tarball.sh @@ -23,6 +23,8 @@ set -ue debian_tarball="${1:-}" if [ "$debian_tarball" = "" ]; then echo "Usage: repack-debian-tarball.sh " + echo + head -n 19 "$0" | tail -n 18 | sed -e 's/#[ ]*//g' exit 1 fi From 48b125ac3903966f97fc277d68cf53ac715e986d Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 18 Dec 2018 11:57:41 +0100 Subject: [PATCH 160/580] Revert "Merge pull request #6217 from sergiocazzolato/tests-reorg-prepare-restore" This reverts commit 19e5a1d7889de6d8cf9c86b57edeaffcae002a15, reversing changes made to 6f7203b4eb1b176524c4cba9e3b9e3c2706846a1. --- spread.yaml | 26 +++++-- tests/lib/prepare-restore.sh | 8 +- tests/lib/prepare.sh | 10 +-- tests/lib/reset.sh | 75 +++++++------------ .../main/classic-custom-device-reg/task.yaml | 4 +- tests/main/classic-firstboot/task.yaml | 5 +- 6 files changed, 58 insertions(+), 70 deletions(-) diff --git a/spread.yaml b/spread.yaml index 062ed4c3b6b..9524ea3273d 100644 --- a/spread.yaml +++ b/spread.yaml @@ -700,13 +700,29 @@ suites: TRAVIS_TAG: "$(HOST: echo $TRAVIS_TAG)" COVERMODE: "$(HOST: echo $COVERMODE)" prepare: | - "$TESTSLIB"/prepare-restore.sh --prepare-suite + #shellcheck source=tests/lib/prepare.sh + . "$TESTSLIB"/prepare.sh + prepare_classic prepare-each: | - "$TESTSLIB"/prepare-restore.sh --prepare-suite-each - restore-each: | - "$TESTSLIB"/prepare-restore.sh --restore-suite-each + "$TESTSLIB"/reset.sh --reuse-core + #shellcheck source=tests/lib/prepare.sh + . "$TESTSLIB"/prepare.sh + prepare_each_classic restore: | - "$TESTSLIB"/prepare-restore.sh --restore-suite + "$TESTSLIB"/reset.sh --store + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB"/pkgdb.sh + + distro_purge_package snapd + case "$SPREAD_SYSTEM" in + arch-*) + # there is no snap-confine and ubuntu-core-launcher + # in Arch + ;; + *) + distro_purge_package snap-confine ubuntu-core-launcher + ;; + esac tests/nightly/: summary: Suite for nightly, expensive, tests diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh index efd57c61dcb..9526ba87cef 100755 --- a/tests/lib/prepare-restore.sh +++ b/tests/lib/prepare-restore.sh @@ -37,8 +37,6 @@ set -o pipefail # shellcheck source=tests/lib/systems.sh . "$TESTSLIB/systems.sh" -# shellcheck source=tests/lib/reset.sh -. "$TESTSLIB/reset.sh" ### ### Utility functions reused below. @@ -415,6 +413,8 @@ prepare_suite() { prepare_suite_each() { # save the job which is going to be executed in the system echo -n "$SPREAD_JOB " >> "$RUNTIME_STATE_PATH/runs" + # shellcheck source=tests/lib/reset.sh + "$TESTSLIB"/reset.sh --reuse-core # Reset systemd journal cursor. start_new_journalctl_log # shellcheck source=tests/lib/prepare.sh @@ -433,12 +433,12 @@ prepare_suite_each() { } restore_suite_each() { - reset_snapd --reuse-core rm -f "$RUNTIME_STATE_PATH/audit-stamp" } restore_suite() { - reset_snapd --store + # shellcheck source=tests/lib/reset.sh + "$TESTSLIB"/reset.sh --store if is_classic_system; then # shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh diff --git a/tests/lib/prepare.sh b/tests/lib/prepare.sh index 0d74253d55c..6c47e29e10b 100755 --- a/tests/lib/prepare.sh +++ b/tests/lib/prepare.sh @@ -16,8 +16,6 @@ set -eux . "$TESTSLIB/state.sh" # shellcheck source=tests/lib/systems.sh . "$TESTSLIB/systems.sh" -# shellcheck source=tests/lib/reset.sh -. "$TESTSLIB/reset.sh" disable_kernel_rate_limiting() { @@ -253,9 +251,7 @@ prepare_classic() { fi # Snapshot the state including core. - if is_snapd_state_saved; then - reset_snapd --reuse-core - else + if ! is_snapd_state_saved; then # need to be seeded to proceed with snap install # also make sure the captured state is seeded snap wait system seed.loaded @@ -632,9 +628,7 @@ prepare_ubuntu_core() { setup_systemd_snapd_overrides # Snapshot the fresh state (including boot/bootenv) - if is_snapd_state_saved; then - reset_snapd --reuse-core - else + if ! is_snapd_state_saved; then systemctl stop snapd.service snapd.socket save_snapd_state systemctl start snapd.socket diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index 2d8738c1820..c86fd7de1dd 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -1,16 +1,18 @@ #!/bin/bash +set -e -x + # shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" - # shellcheck source=tests/lib/state.sh . "$TESTSLIB/state.sh" + # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" #shellcheck source=tests/lib/systems.sh -. "$TESTSLIB/systems.sh" +. "$TESTSLIB"/systems.sh reset_classic() { # Reload all service units as in some situations the unit might @@ -79,15 +81,8 @@ reset_classic() { # Restore snapd state and start systemd service units restore_snapd_state escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")" - all_units="$(systemctl list-unit-files --full --no-legend | cut -f1 -d ' ')" - mounts="" - if echo "$all_units" | grep -q "^${escaped_snap_mount_dir}[-.].*\\.mount"; then - mounts="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.mount")" - fi - services="" - if echo "$all_units" | grep -q "^${escaped_snap_mount_dir}[-.].*\\.service"; then - services="$(echo "$all_units" | grep "^${escaped_snap_mount_dir}[-.].*\\.service")" - fi + mounts="$(systemctl list-unit-files --full | grep "^${escaped_snap_mount_dir}[-.].*\\.mount" | cut -f1 -d ' ')" + services="$(systemctl list-unit-files --full | grep "^${escaped_snap_mount_dir}[-.].*\\.service" | cut -f1 -d ' ')" systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ for unit in $mounts $services; do systemctl start "$unit" @@ -107,9 +102,7 @@ reset_classic() { EXTRA_NC_ARGS="" ;; esac - while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do - sleep 0.5 - done + while ! printf 'GET / HTTP/1.0\r\n\r\n' | nc -U $EXTRA_NC_ARGS /run/snapd.socket; do sleep 0.5; done fi } @@ -154,35 +147,25 @@ reset_all_snap() { fi } -discard_ns() { - # Discard all mount namespaces and active mount profiles. - # This is duplicating logic in snap-discard-ns but it doesn't - # support --all switch yet so we cannot use it. - if [ -d /run/snapd/ns ]; then - for mnt in /run/snapd/ns/*.mnt; do - umount -l "$mnt" || true - rm -f "$mnt" - done - rm -f /run/snapd/ns/*.fstab - fi -} - -tear_down_store() { - if [ "$REMOTE_STORE" = staging ] && [ "$1" = "--store" ]; then - # shellcheck source=tests/lib/store.sh - . "$TESTSLIB"/store.sh - teardown_staging_store - fi -} - -reset_snapd() { - if is_core_system; then - reset_all_snap "$@" - else - reset_classic "$@" - fi - discard_ns - tear_down_store "$@" -} - - +if is_core_system; then + reset_all_snap "$@" +else + reset_classic "$@" +fi + +# Discard all mount namespaces and active mount profiles. +# This is duplicating logic in snap-discard-ns but it doesn't +# support --all switch yet so we cannot use it. +if [ -d /run/snapd/ns ]; then + for mnt in /run/snapd/ns/*.mnt; do + umount -l "$mnt" || true + rm -f "$mnt" + done + rm -f /run/snapd/ns/*.fstab +fi + +if [ "$REMOTE_STORE" = staging ] && [ "$1" = "--store" ]; then + # shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_staging_store +fi diff --git a/tests/main/classic-custom-device-reg/task.yaml b/tests/main/classic-custom-device-reg/task.yaml index 3742a9165cf..6476b3d01bb 100644 --- a/tests/main/classic-custom-device-reg/task.yaml +++ b/tests/main/classic-custom-device-reg/task.yaml @@ -19,9 +19,7 @@ prepare: | snap pack "$TESTSLIB/snaps/classic-gadget" snap download "--$CORE_CHANNEL" core - #shellcheck source=tests/lib/reset.sh - . "$TESTSLIB/reset.sh" - reset_snapd --keep-stopped + "$TESTSLIB/reset.sh" --keep-stopped mkdir -p "$SEED_DIR/snaps" mkdir -p "$SEED_DIR/assertions" cat > "$SEED_DIR/seed.yaml" < "$SEED_DIR/seed.yaml" < Date: Tue, 18 Dec 2018 15:46:31 +0100 Subject: [PATCH 161/580] spread: show free space in debug output Signed-off-by: Maciej Borzecki --- spread.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spread.yaml b/spread.yaml index 9524ea3273d..c9da6bd6c6a 100644 --- a/spread.yaml +++ b/spread.yaml @@ -451,6 +451,8 @@ debug-each: | snap interfaces || true echo '# tasks executed on system' cat "$RUNTIME_STATE_PATH/runs" || true + echo '# free space' + df -h || true fi rename: From fcabc5b728171b615bfb84a6a6be5b5bdbe810c1 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Tue, 18 Dec 2018 17:40:52 +0000 Subject: [PATCH 162/580] don't make use of 'unsafe' conditional since it is required now --- interfaces/builtin/docker_support.go | 37 ++++---------- interfaces/builtin/docker_support_test.go | 60 +---------------------- interfaces/builtin/export_test.go | 8 --- 3 files changed, 11 insertions(+), 94 deletions(-) diff --git a/interfaces/builtin/docker_support.go b/interfaces/builtin/docker_support.go index 3381d03909e..69c12ee0316 100644 --- a/interfaces/builtin/docker_support.go +++ b/interfaces/builtin/docker_support.go @@ -21,7 +21,6 @@ package builtin import ( "fmt" - "regexp" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" @@ -126,7 +125,7 @@ pivot_root, /sys/kernel/security/apparmor/{,**} r, # use 'privileged-containers: true' to support --security-opts -###CHANGEPROFILE_DOCKERDEFAULT### +change_profile unsafe /** -> docker-default, signal (send) peer=docker-default, ptrace (read, trace) peer=docker-default, @@ -531,7 +530,7 @@ const dockerSupportPrivilegedAppArmor = ` # These rules are here to allow Docker to launch unconfined containers but # allow the docker daemon itself to go unconfined. Since it runs as root, this # grants device ownership. -###CHANGEPROFILE_PRIVILEGED### +change_profile unsafe /**, signal (send) peer=unconfined, ptrace (read, trace) peer=unconfined, @@ -571,37 +570,19 @@ func (iface *dockerSupportInterface) StaticInfo() interfaces.StaticInfo { } var ( - cpDockerDefaultPattern = regexp.MustCompile("(###CHANGEPROFILE_DOCKERDEFAULT###)") - cpPrivilegedPattern = regexp.MustCompile("(###CHANGEPROFILE_PRIVILEGED###)") - parserFeatures = release.AppArmorParserFeatures + parserFeatures = release.AppArmorParserFeatures ) func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { var privileged bool _ = plug.Attr("privileged-containers", &privileged) - useUnsafe := false - features, _ := parserFeatures() - for _, f := range features { - if f == "unsafe" { - useUnsafe = true - } - } - rule := "change_profile -> docker-default," - if useUnsafe { - rule = "change_profile unsafe /** -> docker-default," - // This rule conflicts with the 'ix' rules in the home - // interface, so suppress them (LP: #1797786) - spec.SetSuppressHomeIx() - } - snippet := cpDockerDefaultPattern.ReplaceAllString(dockerSupportConnectedPlugAppArmor, rule) - spec.AddSnippet(snippet) + + // The 'change_profile unsafe' rules conflict with the 'ix' rules in + // the home interface, so suppress them (LP: #1797786) + spec.SetSuppressHomeIx() + spec.AddSnippet(dockerSupportConnectedPlugAppArmor) if privileged { - rule = "change_profile -> *," - if useUnsafe { - rule = "change_profile unsafe /**," - } - snippet = cpPrivilegedPattern.ReplaceAllString(dockerSupportPrivilegedAppArmor, rule) - spec.AddSnippet(snippet) + spec.AddSnippet(dockerSupportPrivilegedAppArmor) } spec.UsesPtraceTrace() return nil diff --git a/interfaces/builtin/docker_support_test.go b/interfaces/builtin/docker_support_test.go index 75e5528e9ba..5643b1f1505 100644 --- a/interfaces/builtin/docker_support_test.go +++ b/interfaces/builtin/docker_support_test.go @@ -106,9 +106,6 @@ func (s *DockerSupportInterfaceSuite) TestSanitizePlug(c *C) { } func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedTrue(c *C) { - restore := builtin.MockParserFeatures(func() ([]string, error) { return []string{}, nil }) - defer restore() - var mockSnapYaml = []byte(`name: docker version: 1.0 plugs: @@ -132,7 +129,7 @@ apps: err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"}) - c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), testutil.Contains, `change_profile -> *,`) + c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), testutil.Contains, `change_profile unsafe /**,`) seccompSpec := &seccomp.Specification{} err = seccompSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) @@ -165,7 +162,7 @@ apps: err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) c.Assert(err, IsNil) c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"}) - c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), Not(testutil.Contains), `change_profile -> *,`) + c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), Not(testutil.Contains), `change_profile unsafe /**,`) seccompSpec := &seccomp.Specification{} err = seccompSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) @@ -189,59 +186,6 @@ plugs: c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "docker-support plug requires bool with 'privileged-containers'") } -func (s *DockerSupportInterfaceSuite) TestAppArmorUnsafe(c *C) { - var mockSnapYaml = []byte(`name: docker -version: 1.0 -plugs: - privileged: - interface: docker-support - privileged-containers: true -apps: - app: - command: foo - plugs: - - privileged -`) - - type changeProfileScenario struct { - features []string - expected []string - } - - var changeProfileScenarios = []changeProfileScenario{{ - features: []string{}, - expected: []string{ - "change_profile -> docker-default,", - "change_profile -> *,", - }, - }, { - features: []string{"unsafe"}, - expected: []string{ - "change_profile unsafe /** -> docker-default,", - "change_profile unsafe /**,", - }, - }} - - info, err := snap.InfoFromSnapYaml(mockSnapYaml) - c.Assert(err, IsNil) - - plug := info.Plugs["privileged"] - c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil) - - for _, scenario := range changeProfileScenarios { - restore := builtin.MockParserFeatures(func() ([]string, error) { return scenario.features, nil }) - defer restore() - - apparmorSpec := &apparmor.Specification{} - err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot) - c.Assert(err, IsNil) - c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"}) - for _, rule := range scenario.expected { - c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), testutil.Contains, rule) - } - } -} - func (s *DockerSupportInterfaceSuite) TestInterfaces(c *C) { c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) } diff --git a/interfaces/builtin/export_test.go b/interfaces/builtin/export_test.go index 807ed5d9af2..fbe7410e9a4 100644 --- a/interfaces/builtin/export_test.go +++ b/interfaces/builtin/export_test.go @@ -102,11 +102,3 @@ func MockOsGetenv(mock func(string) string) (restore func()) { return restore } - -func MockParserFeatures(f func() ([]string, error)) (restore func()) { - old := parserFeatures - parserFeatures = f - return func() { - parserFeatures = old - } -} From 5274550a1345bab3b2bc1d9ca27b093e86b64bac Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 19 Dec 2018 10:42:30 +0100 Subject: [PATCH 163/580] cmd/snap: attempt to restore SELinux context of snap user directories (#6265) The SELinux policy of snapd allows certain operations on objects that are in snappy_home_t domain. The trouble is that when a user runs a snap, and ~/snap does not exist, it gets created using the parent directory context, which is user_home_t, causing the policy rules fail to match. We could update the policy to allow manipulation of user_home_t but that would open the whole of user's home directory. Instead, try to restore the proper context on a run if the wrong context is detected, which should DTRT for the newly created directory and ones that already exist. --- cmd/snap/cmd_run.go | 46 ++++- cmd/snap/cmd_run_test.go | 179 +++++++++++++++++++ cmd/snap/export_test.go | 25 +++ cmd/snap/main_test.go | 2 + tests/main/selinux-snap-restorecon/task.yaml | 55 ++++++ 5 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 tests/main/selinux-snap-restorecon/task.yaml diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go index f272fdd0c7d..1aaf8e9a82f 100644 --- a/cmd/snap/cmd_run.go +++ b/cmd/snap/cmd_run.go @@ -44,6 +44,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/strace" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" "github.com/snapcore/snapd/strutil/shlex" @@ -52,10 +53,13 @@ import ( ) var ( - syscallExec = syscall.Exec - userCurrent = user.Current - osGetenv = os.Getenv - timeNow = time.Now + syscallExec = syscall.Exec + userCurrent = user.Current + osGetenv = os.Getenv + timeNow = time.Now + selinuxIsEnabled = selinux.IsEnabled + selinuxVerifyPathContext = selinux.VerifyPathContext + selinuxRestoreContext = selinux.RestoreContext ) type cmdRun struct { @@ -333,7 +337,39 @@ func createUserDataDirs(info *snap.Info) error { } } - return createOrUpdateUserDataSymlink(info, usr) + if err := createOrUpdateUserDataSymlink(info, usr); err != nil { + return err + } + + return maybeRestoreSecurityContext(usr) +} + +// maybeRestoreSecurityContext attempts to restore security context of ~/snap on +// systems where it's applicable +func maybeRestoreSecurityContext(usr *user.User) error { + snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + enabled, err := selinuxIsEnabled() + if err != nil { + return fmt.Errorf("cannot determine SELinux status: %v", err) + } + if !enabled { + logger.Debugf("SELinux not enabled") + return nil + } + + match, err := selinuxVerifyPathContext(snapUserHome) + if err != nil { + return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err) + } + if match { + return nil + } + logger.Noticef("restoring default SELinux context of %v", snapUserHome) + + if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil { + return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err) + } + return nil } func (x *cmdRun) useStrace() bool { diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go index ce39da62eed..beb7b2e2369 100644 --- a/cmd/snap/cmd_run_test.go +++ b/cmd/snap/cmd_run_test.go @@ -20,6 +20,7 @@ package main_test import ( + "errors" "fmt" "os" "os/user" @@ -33,6 +34,7 @@ import ( "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" @@ -930,3 +932,180 @@ func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { c.Check(s.Stdout(), check.Equals, "unhappy\n") c.Check(s.Stderr(), check.Equals, "") } + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabled := false + verify := true + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return verify, nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return nil + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabled, nil + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + + // pretend SELinux is on + enabled = true + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verify = false + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) + + // and we let the user know what we're doing + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir)) +} + +func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { + logbuf, restorer := logger.MockLogger() + defer restorer() + + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + fakeHome := c.MkDir() + restorer = snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: fakeHome}, nil + }) + defer restorer() + + // redirect exec + execCalled := 0 + restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { + execCalled++ + return nil + }) + defer restorer() + + verifyCalls := 0 + restoreCalls := 0 + isEnabledCalls := 0 + enabledErr := errors.New("enabled failed") + verifyErr := errors.New("verify failed") + restoreErr := errors.New("restore failed") + + snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + + restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { + c.Check(what, check.Equals, snapUserDir) + verifyCalls++ + return false, verifyErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error { + c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true}) + c.Check(what, check.Equals, snapUserDir) + restoreCalls++ + return restoreErr + }) + defer restorer() + + restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) { + isEnabledCalls++ + return enabledErr == nil, enabledErr + }) + defer restorer() + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + // these errors are only logged, but we still run the snap + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 1) + c.Check(logbuf.String(), testutil.Contains, "cannot determine SELinux status: enabled failed") + c.Check(isEnabledCalls, check.Equals, 1) + c.Check(verifyCalls, check.Equals, 0) + c.Check(restoreCalls, check.Equals, 0) + // pretend selinux is on + enabledErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 2) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("failed to verify SELinux context of %s: verify failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 2) + c.Check(verifyCalls, check.Equals, 1) + c.Check(restoreCalls, check.Equals, 0) + + // pretend the context does not match + verifyErr = nil + + logbuf.Reset() + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"}) + c.Assert(err, check.IsNil) + c.Check(execCalled, check.Equals, 3) + c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("cannot restore SELinux context of %s: restore failed", snapUserDir)) + c.Check(isEnabledCalls, check.Equals, 3) + c.Check(verifyCalls, check.Equals, 2) + c.Check(restoreCalls, check.Equals, 1) +} diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go index 311c0b6c03a..0dc4b1cef79 100644 --- a/cmd/snap/export_test.go +++ b/cmd/snap/export_test.go @@ -27,6 +27,7 @@ import ( "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/selinux" "github.com/snapcore/snapd/store" ) @@ -216,3 +217,27 @@ func ColorMixin(cmode, umode string) colorMixin { func CmdAdviseSnap() *cmdAdviseSnap { return &cmdAdviseSnap{} } + +func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { + old := selinuxIsEnabled + selinuxIsEnabled = isEnabled + return func() { + selinuxIsEnabled = old + } +} + +func MockSELinuxVerifyPathContext(verifypathcon func(string) (bool, error)) (restore func()) { + old := selinuxVerifyPathContext + selinuxVerifyPathContext = verifypathcon + return func() { + selinuxVerifyPathContext = old + } +} + +func MockSELinuxRestoreContext(restorecon func(string, selinux.RestoreMode) error) (restore func()) { + old := selinuxRestoreContext + selinuxRestoreContext = restorecon + return func() { + selinuxRestoreContext = old + } +} diff --git a/cmd/snap/main_test.go b/cmd/snap/main_test.go index d5a77fd716b..960690dedee 100644 --- a/cmd/snap/main_test.go +++ b/cmd/snap/main_test.go @@ -93,6 +93,8 @@ func (s *BaseSnapSuite) SetUpTest(c *C) { s.AddCleanup(snap.MockIsStdoutTTY(false)) s.AddCleanup(snap.MockIsStdinTTY(false)) + + s.AddCleanup(snap.MockSELinuxIsEnabled(func() (bool, error) { return false, nil })) } func (s *BaseSnapSuite) TearDownTest(c *C) { diff --git a/tests/main/selinux-snap-restorecon/task.yaml b/tests/main/selinux-snap-restorecon/task.yaml new file mode 100644 index 00000000000..74087344c89 --- /dev/null +++ b/tests/main/selinux-snap-restorecon/task.yaml @@ -0,0 +1,55 @@ +summary: Check that snap run automatically restores SELinux context + +description: | + Verify that snap run automatically restores the SELinux context of $HOME/snap. + +systems: [fedora-*, centos-*] +prepare: | + snap install test-snapd-tools + if [ -d /home/test/snap ]; then + mv /home/test/snap /home/test/snap.old + fi + +restore: | + if [ -d /home/test/snap.old ]; then + rm -rf /home/test/snap + mv /home/test/snap.old /home/test/snap + fi + +execute: | + # TODO: extend the test to work for root when the policy is fixed for admin_home_t + # TODO: use snap debug sandbox-features once selinux backend is added + + test ! -d /home/test/snap + su -c "test-snapd-tools.cmd sh -c 'touch \$SNAP_USER_DATA/foo'" test + test -d /home/test/snap + + echo "The snap user directory and data inside has the right context" + + ls -dZ /home/test/snap /home/test/snap/test-snapd-tools /home/test/snap/test-snapd-tools/current/foo > test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap$' < test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap/test-snapd-tools$' < test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap/test-snapd-tools/current/foo$' < test-labels + + echo "When the context of \$HOME/snap is changed" + chcon -t unlabeled_t -R /home/test/snap + chcon -t unlabeled_t -R /home/test/snap/test-snapd-tools/current/foo + #shellcheck disable=SC2012 + ls -dZ /home/test/snap | MATCH ':unlabeled_t:' + + echo "It gets restored recursively" + su -c 'test-snapd-tools.cmd id -Z' test + + ls -dZ /home/test/snap /home/test/snap/test-snapd-tools /home/test/snap/test-snapd-tools/current/foo > test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap$' < test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap/test-snapd-tools$' < test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap/test-snapd-tools/current/foo$' < test-labels + + echo "Restoring happens only when the context of \$HOME/snap is incorrect" + chcon -t unlabeled_t -R /home/test/snap/test-snapd-tools/current/foo + su -c 'test-snapd-tools.cmd id -Z' test + + ls -dZ /home/test/snap /home/test/snap/test-snapd-tools /home/test/snap/test-snapd-tools/current/foo > test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap$' < test-labels + MATCH '^.*:snappy_home_t:.*/home/test/snap/test-snapd-tools$' < test-labels + MATCH '^.*:unlabeled_t:.*/home/test/snap/test-snapd-tools/current/foo$' < test-labels From 9eabbc35a5f942337a91d261140ac257b40494ba Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 19 Dec 2018 11:46:05 +0200 Subject: [PATCH 164/580] wrappers: address review feedback from #6301 (#6302) Trivial PR to address the review feedback (improve comment). --- wrappers/core18.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/wrappers/core18.go b/wrappers/core18.go index ade6ced5df2..336faa99ec9 100644 --- a/wrappers/core18.go +++ b/wrappers/core18.go @@ -167,11 +167,13 @@ func writeSnapdServicesOnCore(s *snap.Info, inter interacter) error { if bytes.Contains(snapdUnits[unit].Content, []byte("X-Snapd-Snap: do-not-start")) { continue } - // Ensure to only restart if the unit was previously active. - // This ensures we DTRT on firstboot and do not stop e.g. - // snapd.socket because doing that would mean that the - // snapd.seeded.service is also stopped which confuses the - // boot order (the unit exists before we are fully seeded) + // Ensure to only restart if the unit was previously + // active. This ensures we DTRT on firstboot and do + // not stop e.g. snapd.socket because doing that + // would mean that the snapd.seeded.service is also + // stopped (independently of snapd.socket being + // active) which confuses the boot order (the unit + // exists before we are fully seeded). isActive, err := sysd.IsActive(unit) if err != nil { return err From 552521bfda4772af5770a38b0ddc0e519c577990 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 20 Dec 2018 11:43:39 +0000 Subject: [PATCH 165/580] snap: give Epoch an Equal method This lets us check whether two epochs are the same. --- snap/epoch.go | 19 +++++++++++++++++++ snap/epoch_test.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/snap/epoch.go b/snap/epoch.go index b57637acf7d..d9220660215 100644 --- a/snap/epoch.go +++ b/snap/epoch.go @@ -159,6 +159,25 @@ func (e *Epoch) IsZero() bool { return rZero && wZero } +func ueq(a, b []uint32) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func (e *Epoch) Equal(other *Epoch) bool { + if e.IsZero() { + return other.IsZero() + } + return ueq(e.Read, other.Read) && ueq(e.Write, other.Write) +} + // Validate checks that the epoch makes sense. func (e *Epoch) Validate() error { if (e.Read != nil && len(e.Read) == 0) || (e.Write != nil && len(e.Write) == 0) { diff --git a/snap/epoch_test.go b/snap/epoch_test.go index a3c9ee68727..eee32301ae7 100644 --- a/snap/epoch_test.go +++ b/snap/epoch_test.go @@ -354,3 +354,22 @@ func (s *epochSuite) TestCanRead(c *check.C) { c.Assert(test.b.CanRead(test.a), check.Equals, test.ba, check.Commentf("ba/%d", i)) } } + +func (s *epochSuite) TestEqual(c *check.C) { + tests := []struct { + a, b *snap.Epoch + eq bool + }{ + {a: &snap.Epoch{}, b: nil, eq: true}, + {a: &snap.Epoch{Read: []uint32{}, Write: []uint32{}}, b: nil, eq: true}, + {a: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: true}, + {a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, eq: true}, + {a: &snap.Epoch{Read: []uint32{0, 1}, Write: []uint32{1}}, b: &snap.Epoch{Read: []uint32{1}, Write: []uint32{1}}, eq: false}, + {a: &snap.Epoch{Read: []uint32{1, 2, 3, 4}, Write: []uint32{7}}, b: &snap.Epoch{Read: []uint32{1, 2, 3, 7}, Write: []uint32{7}}, eq: false}, + } + + for i, test := range tests { + c.Check(test.a.Equal(test.b), check.Equals, test.eq, check.Commentf("ab/%d", i)) + c.Check(test.b.Equal(test.a), check.Equals, test.eq, check.Commentf("ab/%d", i)) + } +} From 318896ad6bced49bcf347e61086f93ffa105f1c5 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 20 Dec 2018 14:48:04 +0100 Subject: [PATCH 166/580] release: use sync.Once around lazy intialized state (#6306) Some state data in the release package, namely AppArmor level/summary & kernel/parser features as well as Seccomp feature set, is only initialized when needed. Since there is a chance, this information could be accessed in concurrently, we need to add some locking around the code that mutates the data. This also shrinks the number of globals used to keep the state and uses pointers to structs instead. This makes the Mock* methods for the state a little bit clearer. --- release/apparmor.go | 253 +++++++++++++++++++++------------------ release/apparmor_test.go | 69 ++++++----- release/export_test.go | 24 ++-- release/seccomp.go | 64 ++++++---- release/seccomp_test.go | 19 +++ 5 files changed, 245 insertions(+), 184 deletions(-) diff --git a/release/apparmor.go b/release/apparmor.go index 11f713f4546..baf58d1934d 100644 --- a/release/apparmor.go +++ b/release/apparmor.go @@ -28,6 +28,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "github.com/snapcore/snapd/strutil" ) @@ -66,61 +67,35 @@ func (level AppArmorLevelType) String() string { return fmt.Sprintf("AppArmorLevelType:%d", level) } -var ( - // appArmorLevel contains the assessment of the "level" of apparmor support. - appArmorLevel = UnknownAppArmor - // appArmorSummary contains a human readable description of the assessment. - appArmorSummary string - // appArmorKernelFeatures contains a list of kernel features that are supported. - // If the value is nil then the features were not probed yet. - appArmorKernelFeatures []string - // appArmorKernelError contains an error, if any, encountered when - // discovering available kernel features. - appArmorKernelError error - // appArmorParserFeatures contains a list of parser features that are supported. - // If the value is nil then the features were not probed yet. - appArmorParserFeatures []string - // appArmorParserError contains an error, if any, encountered when - // discovering available parser features. - appArmorParserError error -) +// appArmorAssessment represents what is supported AppArmor-wise by the system. +var appArmorAssessment = &appArmorAssess{appArmorProber: &appArmorProbe{}} // AppArmorLevel quantifies how well apparmor is supported on the current // kernel. The computation is costly to perform. The result is cached internally. func AppArmorLevel() AppArmorLevelType { - if appArmorLevel == UnknownAppArmor { - assessAppArmor() - } - return appArmorLevel + appArmorAssessment.assess() + return appArmorAssessment.level } // AppArmorSummary describes how well apparmor is supported on the current // kernel. The computation is costly to perform. The result is cached // internally. func AppArmorSummary() string { - if appArmorLevel == UnknownAppArmor { - assessAppArmor() - } - return appArmorSummary + appArmorAssessment.assess() + return appArmorAssessment.summary } // AppArmorKernelFeatures returns a sorted list of apparmor features like // []string{"dbus", "network"}. The result is cached internally. func AppArmorKernelFeatures() ([]string, error) { - if appArmorKernelFeatures == nil { - appArmorKernelFeatures, appArmorKernelError = probeAppArmorKernelFeatures() - } - return appArmorKernelFeatures, appArmorKernelError + return appArmorAssessment.KernelFeatures() } // AppArmorParserFeatures returns a sorted list of apparmor parser features // like []string{"unsafe", ...}. The computation is costly to perform. The // result is cached internally. func AppArmorParserFeatures() ([]string, error) { - if appArmorParserFeatures == nil { - appArmorParserFeatures, appArmorParserError = probeAppArmorParserFeatures() - } - return appArmorParserFeatures, appArmorParserError + return appArmorAssessment.ParserFeatures() } // AppArmorParserMtime returns the mtime of the parser, else 0. @@ -136,64 +111,6 @@ func AppArmorParserMtime() int64 { return mtime } -// MockAppArmorLevel makes the system believe it has certain level of apparmor -// support. -// -// AppArmor kernel and parser features are set to unrealistic values that do -// not match the requested level. Use this function to observe behavior that -// relies solely on the apparmor level value. -func MockAppArmorLevel(level AppArmorLevelType) (restore func()) { - oldAppArmorLevel := appArmorLevel - oldAppArmorSummary := appArmorSummary - oldAppArmorKernelFeatures := appArmorKernelFeatures - oldAppArmorKernelError := appArmorKernelError - oldAppArmorParserFeatures := appArmorParserFeatures - oldAppArmorParserError := appArmorParserError - appArmorLevel = level - appArmorSummary = fmt.Sprintf("mocked apparmor level: %s", level) - appArmorKernelFeatures = []string{"mocked-kernel-feature"} - appArmorKernelError = nil - appArmorParserFeatures = []string{"mocked-parser-feature"} - appArmorParserError = nil - return func() { - appArmorLevel = oldAppArmorLevel - appArmorSummary = oldAppArmorSummary - appArmorKernelFeatures = oldAppArmorKernelFeatures - appArmorKernelError = oldAppArmorKernelError - appArmorParserFeatures = oldAppArmorParserFeatures - appArmorParserError = oldAppArmorParserError - } -} - -// MockAppArmorFeatures makes the system believe it has certain kernel and -// parser features. -// -// AppArmor level and summary are automatically re-assessed on both the change -// and the restore process. Use this function to observe real assessment of -// arbitrary features. -func MockAppArmorFeatures(kernelFeatures []string, kernelError error, parserFeatures []string, parserError error) (restore func()) { - oldAppArmorKernelFeatures := appArmorKernelFeatures - oldAppArmorKernelError := appArmorKernelError - oldAppArmorParserFeatures := appArmorParserFeatures - oldAppArmorParserError := appArmorParserError - appArmorKernelFeatures = kernelFeatures - appArmorKernelError = kernelError - appArmorParserFeatures = parserFeatures - appArmorParserError = parserError - if appArmorKernelFeatures != nil && appArmorParserFeatures != nil { - assessAppArmor() - } - return func() { - appArmorKernelFeatures = oldAppArmorKernelFeatures - appArmorKernelError = oldAppArmorKernelError - appArmorParserFeatures = oldAppArmorParserFeatures - appArmorParserError = oldAppArmorParserError - if appArmorKernelFeatures != nil && appArmorParserFeatures != nil { - assessAppArmor() - } - } -} - // probe related code var ( @@ -234,21 +151,38 @@ var ( appArmorFeaturesSysPath = "/sys/kernel/security/apparmor/features" ) -func assessAppArmor() { +type appArmorProber interface { + KernelFeatures() ([]string, error) + ParserFeatures() ([]string, error) +} + +type appArmorAssess struct { + appArmorProber + // level contains the assessment of the "level" of apparmor support. + level AppArmorLevelType + // summary contains a human readable description of the assessment. + summary string + + once sync.Once +} + +func (aaa *appArmorAssess) assess() { + aaa.once.Do(func() { + aaa.level, aaa.summary = aaa.doAssess() + }) +} + +func (aaa *appArmorAssess) doAssess() (level AppArmorLevelType, summary string) { // First, quickly check if apparmor is available in the kernel at all. - kernelFeatures, err := AppArmorKernelFeatures() + kernelFeatures, err := aaa.KernelFeatures() if os.IsNotExist(err) { - appArmorLevel = NoAppArmor - appArmorSummary = "apparmor not enabled" - return + return NoAppArmor, "apparmor not enabled" } // Then check that the parser supports the required parser features. // If we have any missing required features then apparmor is unusable. - parserFeatures, err := AppArmorParserFeatures() + parserFeatures, err := aaa.ParserFeatures() if os.IsNotExist(err) { - appArmorLevel = NoAppArmor - appArmorSummary = "apparmor_parser not found" - return + return NoAppArmor, "apparmor_parser not found" } var missingParserFeatures []string for _, feature := range requiredAppArmorParserFeatures { @@ -257,10 +191,9 @@ func assessAppArmor() { } } if len(missingParserFeatures) > 0 { - appArmorLevel = UnusableAppArmor - appArmorSummary = fmt.Sprintf("apparmor_parser is available but required parser features are missing: %s", + summary := fmt.Sprintf("apparmor_parser is available but required parser features are missing: %s", strings.Join(missingParserFeatures, ", ")) - return + return UnusableAppArmor, summary } // Next, check that the kernel supports the required kernel features. @@ -271,10 +204,9 @@ func assessAppArmor() { } } if len(missingKernelFeatures) > 0 { - appArmorLevel = UnusableAppArmor - appArmorSummary = fmt.Sprintf("apparmor is enabled but required kernel features are missing: %s", + summary := fmt.Sprintf("apparmor is enabled but required kernel features are missing: %s", strings.Join(missingKernelFeatures, ", ")) - return + return UnusableAppArmor, summary } // Next check that the parser supports preferred parser features. @@ -285,10 +217,9 @@ func assessAppArmor() { } } if len(missingParserFeatures) > 0 { - appArmorLevel = PartialAppArmor - appArmorSummary = fmt.Sprintf("apparmor_parser is available but some features are missing: %s", + summary := fmt.Sprintf("apparmor_parser is available but some features are missing: %s", strings.Join(missingParserFeatures, ", ")) - return + return PartialAppArmor, summary } // Lastly check that the kernel supports preferred kernel features. @@ -298,15 +229,43 @@ func assessAppArmor() { } } if len(missingKernelFeatures) > 0 { - appArmorLevel = PartialAppArmor - appArmorSummary = fmt.Sprintf("apparmor is enabled but some kernel features are missing: %s", + summary := fmt.Sprintf("apparmor is enabled but some kernel features are missing: %s", strings.Join(missingKernelFeatures, ", ")) - return + return PartialAppArmor, summary } // If we got here then all features are available and supported. - appArmorLevel = FullAppArmor - appArmorSummary = "apparmor is enabled and all features are available" + return FullAppArmor, "apparmor is enabled and all features are available" +} + +type appArmorProbe struct { + // kernelFeatures contains a list of kernel features that are supported. + kernelFeatures []string + // kernelError contains an error, if any, encountered when + // discovering available kernel features. + kernelError error + // parserFeatures contains a list of parser features that are supported. + parserFeatures []string + // parserError contains an error, if any, encountered when + // discovering available parser features. + parserError error + + probeKernelOnce sync.Once + probeParserOnce sync.Once +} + +func (aap *appArmorProbe) KernelFeatures() ([]string, error) { + aap.probeKernelOnce.Do(func() { + aap.kernelFeatures, aap.kernelError = probeAppArmorKernelFeatures() + }) + return aap.kernelFeatures, aap.kernelError +} + +func (aap *appArmorProbe) ParserFeatures() ([]string, error) { + aap.probeParserOnce.Do(func() { + aap.parserFeatures, aap.parserError = probeAppArmorParserFeatures() + }) + return aap.parserFeatures, aap.parserError } func probeAppArmorKernelFeatures() ([]string, error) { @@ -357,3 +316,67 @@ func tryAppArmorParserFeature(parser, rule string) bool { } return true } + +// mocking + +type mockAppArmorProbe struct { + kernelFeatures []string + kernelError error + parserFeatures []string + parserError error +} + +func (m *mockAppArmorProbe) KernelFeatures() ([]string, error) { + return m.kernelFeatures, m.kernelError +} + +func (m *mockAppArmorProbe) ParserFeatures() ([]string, error) { + return m.parserFeatures, m.parserError +} + +// MockAppArmorLevel makes the system believe it has certain level of apparmor +// support. +// +// AppArmor kernel and parser features are set to unrealistic values that do +// not match the requested level. Use this function to observe behavior that +// relies solely on the apparmor level value. +func MockAppArmorLevel(level AppArmorLevelType) (restore func()) { + oldAppArmorAssessment := appArmorAssessment + mockProbe := &mockAppArmorProbe{ + kernelFeatures: []string{"mocked-kernel-feature"}, + parserFeatures: []string{"mocked-parser-feature"}, + } + appArmorAssessment = &appArmorAssess{ + appArmorProber: mockProbe, + level: level, + summary: fmt.Sprintf("mocked apparmor level: %s", level), + } + appArmorAssessment.once.Do(func() {}) + return func() { + appArmorAssessment = oldAppArmorAssessment + } +} + +// MockAppArmorFeatures makes the system believe it has certain kernel and +// parser features. +// +// AppArmor level and summary are automatically re-assessed as needed +// on both the change and the restore process. Use this function to +// observe real assessment of arbitrary features. +func MockAppArmorFeatures(kernelFeatures []string, kernelError error, parserFeatures []string, parserError error) (restore func()) { + oldAppArmorAssessment := appArmorAssessment + mockProbe := &mockAppArmorProbe{ + kernelFeatures: kernelFeatures, + kernelError: kernelError, + parserFeatures: parserFeatures, + parserError: parserError, + } + appArmorAssessment = &appArmorAssess{ + appArmorProber: mockProbe, + } + appArmorAssessment.assess() + return func() { + appArmorAssessment = oldAppArmorAssessment + } + +} diff --git a/release/apparmor_test.go b/release/apparmor_test.go index 2f454473e85..35ccc407554 100644 --- a/release/apparmor_test.go +++ b/release/apparmor_test.go @@ -44,35 +44,6 @@ func (*apparmorSuite) TestAppArmorLevelTypeStringer(c *C) { c.Check(release.AppArmorLevelType(42).String(), Equals, "AppArmorLevelType:42") } -func (*apparmorSuite) TestAppArmorLevelTriggersAssesment(c *C) { - // Pretend that we know the apparmor kernel and parser features. - restore := release.MockAppArmorFeatures([]string{"feature"}, nil, []string{"feature"}, nil) - defer restore() - // Pretend that we don't know what the state of apparmor is. - release.ResetAppArmorAssesment() - - // Calling AppArmorLevel assesses the kernel and parser features and sets - // the level to not-unknown value, returning it. - c.Check(release.CurrentAppArmorLevel(), Equals, release.UnknownAppArmor) - level := release.AppArmorLevel() - c.Check(level, Not(Equals), release.UnknownAppArmor) - c.Check(level, Equals, release.CurrentAppArmorLevel()) -} - -func (*apparmorSuite) TestAppArmorSummaryTriggersAssesment(c *C) { - // Pretend that we know the apparmor kernel and parser features. - restore := release.MockAppArmorFeatures([]string{"feature"}, nil, []string{"feature"}, nil) - defer restore() - // Pretend that we don't know what the state of apparmor is. - release.ResetAppArmorAssesment() - - // Calling AppArmorSummary assesses the kernel and parser features and sets - // the level to something other than unknown. - c.Check(release.CurrentAppArmorLevel(), Equals, release.UnknownAppArmor) - release.AppArmorSummary() - c.Check(release.CurrentAppArmorLevel(), Not(Equals), release.UnknownAppArmor) -} - func (*apparmorSuite) TestMockAppArmorLevel(c *C) { for _, lvl := range []release.AppArmorLevelType{release.NoAppArmor, release.UnusableAppArmor, release.PartialAppArmor, release.FullAppArmor} { restore := release.MockAppArmorLevel(lvl) @@ -224,6 +195,8 @@ func (s *apparmorSuite) TestProbeAppArmorParserFeatures(c *C) { } func (s *apparmorSuite) TestInterfaceSystemKey(c *C) { + release.FreshAppArmorAssessment() + d := c.MkDir() restore := release.MockAppArmorFeaturesSysPath(d) defer restore() @@ -235,7 +208,7 @@ func (s *apparmorSuite) TestInterfaceSystemKey(c *C) { restore = release.MockAppArmorParserSearchPath(mockParserCmd.BinDir()) defer restore() - release.AssessAppArmor() + release.AppArmorLevel() features, err := release.AppArmorKernelFeatures() c.Assert(err, IsNil) @@ -262,3 +235,39 @@ func (s *apparmorSuite) TestAppArmorParserMtime(c *C) { mtime = release.AppArmorParserMtime() c.Check(mtime, Equals, int64(0)) } + +func (s *apparmorSuite) TestFeaturesProbedOnce(c *C) { + release.FreshAppArmorAssessment() + + d := c.MkDir() + restore := release.MockAppArmorFeaturesSysPath(d) + defer restore() + c.Assert(os.MkdirAll(filepath.Join(d, "policy"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(d, "network"), 0755), IsNil) + + mockParserCmd := testutil.MockCommand(c, "apparmor_parser", "") + defer mockParserCmd.Restore() + restore = release.MockAppArmorParserSearchPath(mockParserCmd.BinDir()) + defer restore() + + features, err := release.AppArmorKernelFeatures() + c.Assert(err, IsNil) + c.Check(features, DeepEquals, []string{"network", "policy"}) + features, err = release.AppArmorParserFeatures() + c.Assert(err, IsNil) + c.Check(features, DeepEquals, []string{"unsafe"}) + + // this makes probing fails but is not done again + err = os.RemoveAll(d) + c.Assert(err, IsNil) + + _, err = release.AppArmorKernelFeatures() + c.Assert(err, IsNil) + + // this makes probing fails but is not done again + err = os.RemoveAll(mockParserCmd.BinDir()) + c.Assert(err, IsNil) + + _, err = release.AppArmorParserFeatures() + c.Assert(err, IsNil) +} diff --git a/release/export_test.go b/release/export_test.go index ce836ed5d36..45394d8e999 100644 --- a/release/export_test.go +++ b/release/export_test.go @@ -58,26 +58,10 @@ func MockIoutilReadfile(newReadfile func(string) ([]byte, error)) (restorer func } } -// CurrentAppArmorLevel returns the internal cached apparmor level. -func CurrentAppArmorLevel() AppArmorLevelType { - return appArmorLevel -} - -// ResetAppArmorAssesment resets the internal apparmor level and summary. -// -// Both appArmorLevel and appArmorSummary are assigned with zero values -// that trigger probing and assessment on the next access via the public APIs. -func ResetAppArmorAssesment() { - appArmorLevel = UnknownAppArmor - appArmorSummary = "" -} - var ( ProbeAppArmorKernelFeatures = probeAppArmorKernelFeatures ProbeAppArmorParserFeatures = probeAppArmorParserFeatures - AssessAppArmor = assessAppArmor - RequiredAppArmorKernelFeatures = requiredAppArmorKernelFeatures RequiredAppArmorParserFeatures = requiredAppArmorParserFeatures PreferredAppArmorKernelFeatures = preferredAppArmorKernelFeatures @@ -85,3 +69,11 @@ var ( IsWSL = isWSL ) + +func FreshAppArmorAssessment() { + appArmorAssessment = &appArmorAssess{appArmorProber: &appArmorProbe{}} +} + +func FreshSecCompProbe() { + secCompProber = &secCompProbe{} +} diff --git a/release/seccomp.go b/release/seccomp.go index 6222885fb3d..059e0107a84 100644 --- a/release/seccomp.go +++ b/release/seccomp.go @@ -20,37 +20,17 @@ package release import ( - "io/ioutil" "sort" "strings" + "sync" ) -var ( - secCompAvailableActionsPath = "/proc/sys/kernel/seccomp/actions_avail" -) - -var secCompActions []string - -func MockSecCompActions(actions []string) (restore func()) { - old := secCompActions - secCompActions = actions - return func() { secCompActions = old } -} +var secCompProber = &secCompProbe{} // SecCompActions returns a sorted list of seccomp actions like // []string{"allow", "errno", "kill", "log", "trace", "trap"}. func SecCompActions() []string { - if secCompActions == nil { - var actions []string - contents, err := ioutil.ReadFile(secCompAvailableActionsPath) - if err != nil { - return actions - } - actions = strings.Split(strings.TrimRight(string(contents), "\n"), " ") - sort.Strings(actions) - secCompActions = actions - } - return secCompActions + return secCompProber.actions() } func SecCompSupportsAction(action string) bool { @@ -61,3 +41,41 @@ func SecCompSupportsAction(action string) bool { } return false } + +// probing + +type secCompProbe struct { + probedActions []string + + once sync.Once +} + +func (scp *secCompProbe) actions() []string { + scp.once.Do(func() { + scp.probedActions = probeSecCompActions() + }) + return scp.probedActions +} + +func probeSecCompActions() []string { + contents, err := ioutilReadFile("/proc/sys/kernel/seccomp/actions_avail") + if err != nil { + return []string{} + } + actions := strings.Split(strings.TrimRight(string(contents), "\n"), " ") + sort.Strings(actions) + return actions +} + +// mocking + +func MockSecCompActions(actions []string) (restore func()) { + old := secCompProber + secCompProber = &secCompProbe{ + probedActions: actions, + } + secCompProber.once.Do(func() {}) + return func() { + secCompProber = old + } +} diff --git a/release/seccomp_test.go b/release/seccomp_test.go index c05145e952a..6f4a9d3fcd5 100644 --- a/release/seccomp_test.go +++ b/release/seccomp_test.go @@ -20,6 +20,8 @@ package release_test import ( + "io" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/release" @@ -48,3 +50,20 @@ func (s *seccompSuite) TestSecCompSupportsAction(c *C) { defer reset() c.Check(release.SecCompSupportsAction("log"), Equals, true) } + +func (s *seccompSuite) TestProbe(c *C) { + release.FreshSecCompProbe() + r1 := release.MockIoutilReadfile(func(string) ([]byte, error) { + return []byte("a b\n"), nil + }) + defer r1() + + c.Check(release.SecCompActions(), DeepEquals, []string{"a", "b"}) + + r2 := release.MockIoutilReadfile(func(string) ([]byte, error) { + return nil, io.ErrUnexpectedEOF + }) + defer r2() + + c.Check(release.SecCompActions(), DeepEquals, []string{"a", "b"}) +} From 9b8751a23839d5a1c540486cf4e22407c5a3ab8a Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 20 Dec 2018 17:22:13 +0100 Subject: [PATCH 167/580] Autoconnect test. --- overlord/ifacestate/ifacestate_test.go | 67 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 343865abca7..e3b590d182a 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4978,14 +4978,17 @@ func (s *interfaceManagerSuite) TestAttributesRestoredFromConns(c *C) { c.Check(restoredSlot.Attr("dynamic-number", &dynnumber), IsNil) } -func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { - s.MockModel(c, nil) +func (s *interfaceManagerSuite) setupHotplugConnectTestData(c *C) *state.Change { + s.state.Unlock() + coreInfo := s.mockSnap(c, coreSnapYaml) repo := s.manager(c).Repository() err := repo.AddInterface(&ifacetest.TestInterface{ InterfaceName: "test", }) c.Assert(err, IsNil) + + // mock hotplug slot in the repo and state err = repo.AddSlot(&snap.SlotInfo{ Snap: coreInfo, Name: "hotplugslot", @@ -4995,7 +4998,12 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { c.Assert(err, IsNil) s.state.Lock() - defer s.state.Unlock() + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1234", + }}) // mock the consumer si := &snap.SideInfo{RealName: "consumer", Revision: snap.R(1)} @@ -5009,12 +5017,20 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { SnapType: "app", }) - s.state.Set("hotplug-slots", map[string]interface{}{ - "hotplugslot": map[string]interface{}{ - "name": "hotplugslot", - "interface": "test", - "hotplug-key": "1234", - }}) + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-connect", "") + ifacestate.SetHotplugAttrs(t, "test", "1234") + chg.AddTask(t) + + return chg +} + +func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { + s.MockModel(c, nil) + + s.state.Lock() + defer s.state.Unlock() + chg := s.setupHotplugConnectTestData(c) // simulate a device that was known and connected before s.state.Set("conns", map[string]interface{}{ @@ -5024,17 +5040,31 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { "hotplug-gone": true, }}) - chg := s.state.NewChange("hotplug change", "") - t := s.state.NewTask("hotplug-connect", "") - t.Set("hotplug-key", "1234") - t.Set("interface", "test") - chg.AddTask(t) + s.state.Unlock() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + + var conns map[string]interface{} + c.Assert(s.state.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "plug-static": map[string]interface{}{"attr1": "value1"}, + }}) +} + +func (s *interfaceManagerSuite) TestHotplugAutoconnect(c *C) { + s.MockModel(c, nil) + + s.state.Lock() + defer s.state.Unlock() + chg := s.setupHotplugConnectTestData(c) s.state.Unlock() - for i := 0; i < 5; i++ { - s.se.Ensure() - s.se.Wait() - } + s.settle(c) s.state.Lock() c.Assert(chg.Err(), IsNil) @@ -5045,6 +5075,7 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { "consumer:plug core:hotplugslot": map[string]interface{}{ "interface": "test", "hotplug-key": "1234", + "auto": true, "plug-static": map[string]interface{}{"attr1": "value1"}, }}) } From 79b1fa4099f0220201afedb45de253a54a7602bd Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 20 Dec 2018 17:42:34 +0100 Subject: [PATCH 168/580] Test for retry on conflict. --- overlord/ifacestate/handlers.go | 4 +-- overlord/ifacestate/ifacestate_test.go | 35 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 25bfb7dad2e..407453f0adb 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1302,8 +1302,8 @@ func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) erro } if err := checkAutoconnectConflicts(st, task, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { - if _, retry := err.(*state.Retry); retry { - task.Logf("hotplug connect will be retried because of %q - %q conflict", connRef.PlugRef.Snap, connRef.SlotRef.Snap) + if retry, ok := err.(*state.Retry); ok { + task.Logf("hotplug connect will be retried: %s", retry.Reason) return err // will retry } return fmt.Errorf("hotplug connect conflict check failed: %s", err) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index e3b590d182a..a755079b2a7 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -5056,6 +5056,41 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { }}) } +func (s *interfaceManagerSuite) TestHotplugConnectConflictRetry(c *C) { + s.MockModel(c, nil) + + s.state.Lock() + defer s.state.Unlock() + chg := s.setupHotplugConnectTestData(c) + + // simulate a device that was known and connected before + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "hotplug-gone": true, + }}) + + otherChg := s.state.NewChange("other-chg", "...") + t := s.state.NewTask("link-snap", "...") + t.Set("snap-setup", &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "core"}}) + otherChg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + c.Check(chg.Status().Ready(), Equals, false) + tasks := chg.Tasks() + c.Assert(tasks, HasLen, 1) + + hotplugConnectTask := tasks[0] + c.Check(hotplugConnectTask.Status(), Equals, state.DoingStatus) + c.Check(hotplugConnectTask.Log()[0], Matches, `.*hotplug connect will be retried: conflicting snap core with task "link-snap"`) +} + func (s *interfaceManagerSuite) TestHotplugAutoconnect(c *C) { s.MockModel(c, nil) From 431caacd1e5b8890b5a3742e87ce2450e14e75e3 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 20 Dec 2018 17:45:19 +0100 Subject: [PATCH 169/580] Test for hotplug autoconnect conflict. --- overlord/ifacestate/handlers.go | 2 +- overlord/ifacestate/ifacestate_test.go | 27 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 407453f0adb..2fe5fcf46ba 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1474,7 +1474,7 @@ func (m *InterfaceManager) autoconnectNewDevice(task *state.Task, ifaceName, hot if err := checkAutoconnectConflicts(st, task, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { if retry, ok := err.(*state.Retry); ok { - task.Logf("Waiting for conflicting change in progress: %s", retry.Reason) + task.Logf("hotplug connect will be retried: %s", retry.Reason) return err // will retry } return fmt.Errorf("auto-connect conflict check failed: %s", err) diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index a755079b2a7..b95cb298adf 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -5115,6 +5115,33 @@ func (s *interfaceManagerSuite) TestHotplugAutoconnect(c *C) { }}) } +func (s *interfaceManagerSuite) TestHotplugAutoconnectConflictRetry(c *C) { + s.MockModel(c, nil) + + s.state.Lock() + defer s.state.Unlock() + chg := s.setupHotplugConnectTestData(c) + + otherChg := s.state.NewChange("other-chg", "...") + t := s.state.NewTask("link-snap", "...") + t.Set("snap-setup", &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "core"}}) + otherChg.AddTask(t) + + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + c.Assert(chg.Err(), IsNil) + c.Check(chg.Status().Ready(), Equals, false) + tasks := chg.Tasks() + c.Assert(tasks, HasLen, 1) + + hotplugConnectTask := tasks[0] + c.Check(hotplugConnectTask.Status(), Equals, state.DoingStatus) + c.Check(hotplugConnectTask.Log()[0], Matches, `.*hotplug connect will be retried: conflicting snap core with task "link-snap"`) +} + func (s *interfaceManagerSuite) TestHotplugDisconnect(c *C) { coreInfo := s.mockSnap(c, coreSnapYaml) repo := s.manager(c).Repository() From f6755803e723cf18b939ccf677aea3aff4542a62 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:43:36 +0100 Subject: [PATCH 170/580] run-checks: stop running HEAD of staticcheck The run-checks script runs a go static analysis tool staticcheck. The tool was always fetched from master via "go get". Recently we found that it complains about unused code (which is fine) but also about cgo and a lot of little things that need to be inspected one by one. I initially started fixing all of the issues but we should not have broken master for long periods so let's start by nuking staticcheck. We need a way to run a released version (and switch to another released version explicitly). Ideally we'd also run some sort of meta-checker that includes staticcheck internally. Signed-off-by: Zygmunt Krynicki --- run-checks | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/run-checks b/run-checks index d20fe2da96c..b8f3f9c3e90 100755 --- a/run-checks +++ b/run-checks @@ -217,19 +217,6 @@ if [ "$STATIC" = 1 ]; then echo Checking all interfaces have minimal spread test missing_interface_spread_test - - if command -v staticcheck >/dev/null || ( go version | grep -E 'go1\.(9|1[0-9])(\.[0-9]| )' ); then - # either you already have staticcheck on your path, or you're on a new enough go - echo Running staticcheck - if ! command -v staticcheck >/dev/null; then - go get -u honnef.co/go/tools/cmd/staticcheck - fi - # SA1019 https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/docs/checks/SA1019 - # complains about using os.SEEK_* instead of io.Seek*, when the latter don't exist in 1.6 yet - # SA1012 https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/docs/checks/SA1012 - # complains about tomb.Context(nil), which is wrong -- that is, tomb.Context(nil) is right. - go list ./... | grep -v '/vendor/' | xargs staticcheck -tests=false -ignore '*:SA1019,SA1012' - fi fi if [ "$UNIT" = 1 ]; then From d150e9a690cea1339dd31dbfc136b285c8f1e47f Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Mon, 7 Jan 2019 09:55:16 +0100 Subject: [PATCH 171/580] release: allow mocking SELinux state from external packages Signed-off-by: Maciej Borzecki --- release/export_test.go | 8 -------- release/selinux.go | 10 ++++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/release/export_test.go b/release/export_test.go index b629e2a4c04..1b1e0b07e75 100644 --- a/release/export_test.go +++ b/release/export_test.go @@ -58,14 +58,6 @@ func MockIoutilReadfile(newReadfile func(string) ([]byte, error)) (restorer func } } -func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { - old := selinuxIsEnabled - selinuxIsEnabled = isEnabled - return func() { - selinuxIsEnabled = old - } -} - func MockSELinuxIsEnforcing(isEnforcing func() (bool, error)) (restore func()) { old := selinuxIsEnforcing selinuxIsEnforcing = isEnforcing diff --git a/release/selinux.go b/release/selinux.go index d3aeef76c38..d6aa77a278e 100644 --- a/release/selinux.go +++ b/release/selinux.go @@ -78,3 +78,13 @@ func probeSELinux() (SELinuxLevelType, string) { } return SELinuxEnforcing, "SELinux is enabled and in enforcing mode" } + +// MockSELinuxIsEnabled makes the system believe a certain SELinux state is +// currently true +func MockSELinuxIsEnabled(isEnabled func() (bool, error)) (restore func()) { + old := selinuxIsEnabled + selinuxIsEnabled = isEnabled + return func() { + selinuxIsEnabled = old + } +} From 1b583ed4eb76d3021e0c90c81a5705b864559f4f Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:56:40 +0100 Subject: [PATCH 172/580] run-checks: add FIXME note for running staticcheck Signed-off-by: Zygmunt Krynicki --- run-checks | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run-checks b/run-checks index b8f3f9c3e90..11cc19500ce 100755 --- a/run-checks +++ b/run-checks @@ -217,6 +217,8 @@ if [ "$STATIC" = 1 ]; then echo Checking all interfaces have minimal spread test missing_interface_spread_test + + # FIXME: re-add staticcheck with a matching version for the used go-version fi if [ "$UNIT" = 1 ]; then From 780623b2874e439260b0c3dc290be28cf96a3f72 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 10:01:21 +0100 Subject: [PATCH 173/580] overlord/ifacestate: remove unused auto variable Signed-off-by: Zygmunt Krynicki --- overlord/ifacestate/handlers.go | 1 - 1 file changed, 1 deletion(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 31ded308aac..2fbf74c558c 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1070,7 +1070,6 @@ func (m *InterfaceManager) doAutoDisconnect(task *state.Task, _ *tomb.Tomb) erro // check for conflicts on all connections first before creating disconnect hooks for _, connRef := range connections { - const auto = true if err := checkDisconnectConflicts(st, snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap); err != nil { if _, retry := err.(*state.Retry); retry { logger.Debugf("disconnecting interfaces of snap %q will be retried because of %q - %q conflict", snapName, connRef.PlugRef.Snap, connRef.SlotRef.Snap) From 7138255f439f0a34a1161ceb0a28575c54fbb218 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Mon, 7 Jan 2019 09:38:05 +0000 Subject: [PATCH 174/580] snap: rename the (private) []uint32 equality helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ueq` → `epochListEq` fwiw; thanks @pedronis and @zyga for the suggestion. --- snap/epoch.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snap/epoch.go b/snap/epoch.go index d9220660215..0e0cef5c57b 100644 --- a/snap/epoch.go +++ b/snap/epoch.go @@ -159,7 +159,7 @@ func (e *Epoch) IsZero() bool { return rZero && wZero } -func ueq(a, b []uint32) bool { +func epochListEq(a, b []uint32) bool { if len(a) != len(b) { return false } @@ -175,7 +175,7 @@ func (e *Epoch) Equal(other *Epoch) bool { if e.IsZero() { return other.IsZero() } - return ueq(e.Read, other.Read) && ueq(e.Write, other.Write) + return epochListEq(e.Read, other.Read) && epochListEq(e.Write, other.Write) } // Validate checks that the epoch makes sense. From 002c7ce88ec35a89e0ca3386f22e381f3f03498b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:59:22 +0100 Subject: [PATCH 175/580] cmd/snap: remove unused AttributePair type Signed-off-by: Zygmunt Krynicki --- cmd/snap/interfaces_common.go | 21 ----------------- cmd/snap/interfaces_common_test.go | 38 ------------------------------ 2 files changed, 59 deletions(-) diff --git a/cmd/snap/interfaces_common.go b/cmd/snap/interfaces_common.go index 7b56e57b6a4..1e2e513bf1c 100644 --- a/cmd/snap/interfaces_common.go +++ b/cmd/snap/interfaces_common.go @@ -26,27 +26,6 @@ import ( "github.com/snapcore/snapd/i18n" ) -// AttributePair contains a pair of key-value strings -type AttributePair struct { - // The key - Key string - // The value - Value string -} - -// UnmarshalFlag parses a string into an AttributePair -func (ap *AttributePair) UnmarshalFlag(value string) error { - parts := strings.SplitN(value, "=", 2) - if len(parts) < 2 || parts[0] == "" { - ap.Key = "" - ap.Value = "" - return fmt.Errorf(i18n.G("invalid attribute: %q (want key=value)"), value) - } - ap.Key = parts[0] - ap.Value = parts[1] - return nil -} - // SnapAndName holds a snap name and a plug or slot name. type SnapAndName struct { Snap string diff --git a/cmd/snap/interfaces_common_test.go b/cmd/snap/interfaces_common_test.go index fd8e9a416f4..ac56b07c1ed 100644 --- a/cmd/snap/interfaces_common_test.go +++ b/cmd/snap/interfaces_common_test.go @@ -25,44 +25,6 @@ import ( . "github.com/snapcore/snapd/cmd/snap" ) -type AttributePairSuite struct{} - -var _ = Suite(&AttributePairSuite{}) - -func (s *AttributePairSuite) TestUnmarshalFlagAttributePair(c *C) { - var ap AttributePair - // Typical - err := ap.UnmarshalFlag("key=value") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "value") - // Empty key - err = ap.UnmarshalFlag("=value") - c.Assert(err, ErrorMatches, `invalid attribute: "=value" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") - // Empty value - err = ap.UnmarshalFlag("key=") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "") - // Both key and value empty - err = ap.UnmarshalFlag("=") - c.Assert(err, ErrorMatches, `invalid attribute: "=" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") - // Value containing = - err = ap.UnmarshalFlag("key=value=more") - c.Assert(err, IsNil) - c.Check(ap.Key, Equals, "key") - c.Check(ap.Value, Equals, "value=more") - // Malformed format - err = ap.UnmarshalFlag("malformed") - c.Assert(err, ErrorMatches, `invalid attribute: "malformed" \(want key=value\)`) - c.Check(ap.Key, Equals, "") - c.Check(ap.Value, Equals, "") -} - type SnapAndNameSuite struct{} var _ = Suite(&SnapAndNameSuite{}) From d350f254d01c499d4d9bfee63791989e348e59c9 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:14:53 +0100 Subject: [PATCH 176/580] interfaces: remove unused bySlotRef sorting helper Signed-off-by: Zygmunt Krynicki --- interfaces/export_test.go | 6 ------ interfaces/sorting.go | 11 ----------- interfaces/sorting_test.go | 36 ------------------------------------ 3 files changed, 53 deletions(-) diff --git a/interfaces/export_test.go b/interfaces/export_test.go index 528da2ee3b2..b062792c0c4 100644 --- a/interfaces/export_test.go +++ b/interfaces/export_test.go @@ -25,12 +25,6 @@ func (c ByConnRef) Len() int { return byConnRef(c).Len() } func (c ByConnRef) Swap(i, j int) { byConnRef(c).Swap(i, j) } func (c ByConnRef) Less(i, j int) bool { return byConnRef(c).Less(i, j) } -type BySlotRef bySlotRef - -func (c BySlotRef) Len() int { return bySlotRef(c).Len() } -func (c BySlotRef) Swap(i, j int) { bySlotRef(c).Swap(i, j) } -func (c BySlotRef) Less(i, j int) bool { return bySlotRef(c).Less(i, j) } - type ByPlugRef byPlugRef func (c ByPlugRef) Len() int { return byPlugRef(c).Len() } diff --git a/interfaces/sorting.go b/interfaces/sorting.go index e5fef485c4f..ae92e3c7be6 100644 --- a/interfaces/sorting.go +++ b/interfaces/sorting.go @@ -42,17 +42,6 @@ func (c byConnRef) Less(i, j int) bool { return c[i].SlotRef.Name < c[j].SlotRef.Name } -type bySlotRef []SlotRef - -func (c bySlotRef) Len() int { return len(c) } -func (c bySlotRef) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c bySlotRef) Less(i, j int) bool { - if c[i].Snap != c[j].Snap { - return c[i].Snap < c[j].Snap - } - return c[i].Name < c[j].Name -} - type byPlugRef []PlugRef func (c byPlugRef) Len() int { return len(c) } diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index 5a07ce96ec9..ff07446f332 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -37,42 +37,6 @@ func newConnRef(plugSnap, plug, slotSnap, slot string) *interfaces.ConnRef { return &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plug}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slot}} } -func (s *SortingSuite) TestSortBySlotRef(c *C) { - list := []interfaces.SlotRef{{ - Snap: "snap-2", - Name: "name-2", - }, { - Snap: "snap-2_instance", - Name: "name-2", - }, { - Snap: "snap-3", - Name: "name-2", - }, { - Snap: "snap-1", - Name: "name-2", - }, { - Snap: "snap-1", - Name: "name-1", - }} - sort.Sort(interfaces.BySlotRef(list)) - c.Assert(list, DeepEquals, []interfaces.SlotRef{{ - Snap: "snap-1", - Name: "name-1", - }, { - Snap: "snap-1", - Name: "name-2", - }, { - Snap: "snap-2", - Name: "name-2", - }, { - Snap: "snap-2_instance", - Name: "name-2", - }, { - Snap: "snap-3", - Name: "name-2", - }}) -} - func (s *SortingSuite) TestSortByPlugRef(c *C) { list := []interfaces.PlugRef{{ Snap: "snap-2", From 2e5f8c841459bee68ce40fa365631b332005a6c9 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:33:11 +0100 Subject: [PATCH 177/580] interfaces: remove unused byPlugRef sorting helper Signed-off-by: Zygmunt Krynicki --- interfaces/export_test.go | 6 ------ interfaces/sorting.go | 11 ----------- interfaces/sorting_test.go | 36 ------------------------------------ 3 files changed, 53 deletions(-) diff --git a/interfaces/export_test.go b/interfaces/export_test.go index b062792c0c4..06b1b625812 100644 --- a/interfaces/export_test.go +++ b/interfaces/export_test.go @@ -25,12 +25,6 @@ func (c ByConnRef) Len() int { return byConnRef(c).Len() } func (c ByConnRef) Swap(i, j int) { byConnRef(c).Swap(i, j) } func (c ByConnRef) Less(i, j int) bool { return byConnRef(c).Less(i, j) } -type ByPlugRef byPlugRef - -func (c ByPlugRef) Len() int { return byPlugRef(c).Len() } -func (c ByPlugRef) Swap(i, j int) { byPlugRef(c).Swap(i, j) } -func (c ByPlugRef) Less(i, j int) bool { return byPlugRef(c).Less(i, j) } - type ByPlugSnapAndName byPlugSnapAndName func (c ByPlugSnapAndName) Len() int { return byPlugSnapAndName(c).Len() } diff --git a/interfaces/sorting.go b/interfaces/sorting.go index ae92e3c7be6..69ab2497c9a 100644 --- a/interfaces/sorting.go +++ b/interfaces/sorting.go @@ -42,17 +42,6 @@ func (c byConnRef) Less(i, j int) bool { return c[i].SlotRef.Name < c[j].SlotRef.Name } -type byPlugRef []PlugRef - -func (c byPlugRef) Len() int { return len(c) } -func (c byPlugRef) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c byPlugRef) Less(i, j int) bool { - if c[i].Snap != c[j].Snap { - return c[i].Snap < c[j].Snap - } - return c[i].Name < c[j].Name -} - type byPlugSnapAndName []*snap.PlugInfo func (c byPlugSnapAndName) Len() int { return len(c) } diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index ff07446f332..2c214001c45 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -37,42 +37,6 @@ func newConnRef(plugSnap, plug, slotSnap, slot string) *interfaces.ConnRef { return &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plug}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slot}} } -func (s *SortingSuite) TestSortByPlugRef(c *C) { - list := []interfaces.PlugRef{{ - Snap: "snap-2", - Name: "name-2", - }, { - Snap: "snap-2_instance", - Name: "name-2", - }, { - Snap: "snap-3", - Name: "name-2", - }, { - Snap: "snap-1", - Name: "name-2", - }, { - Snap: "snap-1", - Name: "name-1", - }} - sort.Sort(interfaces.ByPlugRef(list)) - c.Assert(list, DeepEquals, []interfaces.PlugRef{{ - Snap: "snap-1", - Name: "name-1", - }, { - Snap: "snap-1", - Name: "name-2", - }, { - Snap: "snap-2", - Name: "name-2", - }, { - Snap: "snap-2_instance", - Name: "name-2", - }, { - Snap: "snap-3", - Name: "name-2", - }}) -} - func (s *SortingSuite) TestByBackendName(c *C) { list := []interfaces.SecurityBackend{ &ifacetest.TestSecurityBackend{BackendName: "backend-2"}, From c3f932b8a680774e35dc1b8be3d2a99497134ea9 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:33:20 +0100 Subject: [PATCH 178/580] interfaces: remove unused byBackendName sorting helper Signed-off-by: Zygmunt Krynicki --- interfaces/export_test.go | 6 ------ interfaces/sorting.go | 8 -------- interfaces/sorting_test.go | 12 ------------ 3 files changed, 26 deletions(-) diff --git a/interfaces/export_test.go b/interfaces/export_test.go index 06b1b625812..c3bd0c3b587 100644 --- a/interfaces/export_test.go +++ b/interfaces/export_test.go @@ -37,12 +37,6 @@ func (c BySlotSnapAndName) Len() int { return bySlotSnapAndName(c).Len func (c BySlotSnapAndName) Swap(i, j int) { bySlotSnapAndName(c).Swap(i, j) } func (c BySlotSnapAndName) Less(i, j int) bool { return bySlotSnapAndName(c).Less(i, j) } -type ByBackendName byBackendName - -func (c ByBackendName) Len() int { return byBackendName(c).Len() } -func (c ByBackendName) Swap(i, j int) { byBackendName(c).Swap(i, j) } -func (c ByBackendName) Less(i, j int) bool { return byBackendName(c).Less(i, j) } - type ByInterfaceName byInterfaceName func (c ByInterfaceName) Len() int { return byInterfaceName(c).Len() } diff --git a/interfaces/sorting.go b/interfaces/sorting.go index 69ab2497c9a..cd148645530 100644 --- a/interfaces/sorting.go +++ b/interfaces/sorting.go @@ -70,14 +70,6 @@ func (c bySlotSnapAndName) Less(i, j int) bool { return c[i].Name < c[j].Name } -type byBackendName []SecurityBackend - -func (c byBackendName) Len() int { return len(c) } -func (c byBackendName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c byBackendName) Less(i, j int) bool { - return c[i].Name() < c[j].Name() -} - func sortedSnapNamesWithPlugs(m map[string]map[string]*snap.PlugInfo) []string { keys := make([]string, 0, len(m)) for key := range m { diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index 2c214001c45..0d6242fc6c0 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -37,18 +37,6 @@ func newConnRef(plugSnap, plug, slotSnap, slot string) *interfaces.ConnRef { return &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: plugSnap, Name: plug}, SlotRef: interfaces.SlotRef{Snap: slotSnap, Name: slot}} } -func (s *SortingSuite) TestByBackendName(c *C) { - list := []interfaces.SecurityBackend{ - &ifacetest.TestSecurityBackend{BackendName: "backend-2"}, - &ifacetest.TestSecurityBackend{BackendName: "backend-1"}, - } - sort.Sort(interfaces.ByBackendName(list)) - c.Assert(list, DeepEquals, []interfaces.SecurityBackend{ - &ifacetest.TestSecurityBackend{BackendName: "backend-1"}, - &ifacetest.TestSecurityBackend{BackendName: "backend-2"}, - }) -} - func (s *SortingSuite) TestByInterfaceName(c *C) { list := []interfaces.Interface{ &ifacetest.TestInterface{InterfaceName: "iface-2"}, From 4d9656eca635e0f55a712e5c94f0223884ab9949 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:35:01 +0100 Subject: [PATCH 179/580] interfaces: remove unused byPlugInfo sorting helper Signed-off-by: Zygmunt Krynicki --- interfaces/export_test.go | 6 ------ interfaces/sorting.go | 14 -------------- interfaces/sorting_test.go | 22 ---------------------- 3 files changed, 42 deletions(-) diff --git a/interfaces/export_test.go b/interfaces/export_test.go index c3bd0c3b587..7a624f6edf7 100644 --- a/interfaces/export_test.go +++ b/interfaces/export_test.go @@ -43,12 +43,6 @@ func (c ByInterfaceName) Len() int { return byInterfaceName(c).Len() } func (c ByInterfaceName) Swap(i, j int) { byInterfaceName(c).Swap(i, j) } func (c ByInterfaceName) Less(i, j int) bool { return byInterfaceName(c).Less(i, j) } -type ByPlugInfo byPlugInfo - -func (c ByPlugInfo) Len() int { return byPlugInfo(c).Len() } -func (c ByPlugInfo) Swap(i, j int) { byPlugInfo(c).Swap(i, j) } -func (c ByPlugInfo) Less(i, j int) bool { return byPlugInfo(c).Less(i, j) } - type BySlotInfo bySlotInfo func (c BySlotInfo) Len() int { return bySlotInfo(c).Len() } diff --git a/interfaces/sorting.go b/interfaces/sorting.go index cd148645530..1c1961365b2 100644 --- a/interfaces/sorting.go +++ b/interfaces/sorting.go @@ -114,20 +114,6 @@ func (c byInterfaceName) Less(i, j int) bool { return c[i].Name() < c[j].Name() } -type byPlugInfo []*snap.PlugInfo - -func (c byPlugInfo) Len() int { return len(c) } -func (c byPlugInfo) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c byPlugInfo) Less(i, j int) bool { - if c[i].Snap.SnapName() != c[j].Snap.SnapName() { - return c[i].Snap.SnapName() < c[j].Snap.SnapName() - } - if c[i].Snap.InstanceKey != c[j].Snap.InstanceKey { - return c[i].Snap.InstanceKey < c[j].Snap.InstanceKey - } - return c[i].Name < c[j].Name -} - type bySlotInfo []*snap.SlotInfo func (c bySlotInfo) Len() int { return len(c) } diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index 0d6242fc6c0..3ecbf507417 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -49,28 +49,6 @@ func (s *SortingSuite) TestByInterfaceName(c *C) { }) } -func (s *SortingSuite) TestByPlugInfo(c *C) { - list := []*snap.PlugInfo{ - {Snap: &snap.Info{SuggestedName: "name-3"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-1"}, - } - sort.Sort(interfaces.ByPlugInfo(list)) - c.Assert(list, DeepEquals, []*snap.PlugInfo{ - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-3"}, Name: "plug-2"}, - }) -} - func (s *SortingSuite) TestBySlotInfo(c *C) { list := []*snap.SlotInfo{ {Snap: &snap.Info{SuggestedName: "name-3"}, Name: "plug-2"}, From 717c0a80178e4f613a0725b60d656e625143044f Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 09:36:02 +0100 Subject: [PATCH 180/580] interfaces: remove unused bySlotInfo sorting helper Signed-off-by: Zygmunt Krynicki --- interfaces/export_test.go | 6 ------ interfaces/sorting.go | 14 -------------- interfaces/sorting_test.go | 22 ---------------------- 3 files changed, 42 deletions(-) diff --git a/interfaces/export_test.go b/interfaces/export_test.go index 7a624f6edf7..de1e60400d9 100644 --- a/interfaces/export_test.go +++ b/interfaces/export_test.go @@ -43,12 +43,6 @@ func (c ByInterfaceName) Len() int { return byInterfaceName(c).Len() } func (c ByInterfaceName) Swap(i, j int) { byInterfaceName(c).Swap(i, j) } func (c ByInterfaceName) Less(i, j int) bool { return byInterfaceName(c).Less(i, j) } -type BySlotInfo bySlotInfo - -func (c BySlotInfo) Len() int { return bySlotInfo(c).Len() } -func (c BySlotInfo) Swap(i, j int) { bySlotInfo(c).Swap(i, j) } -func (c BySlotInfo) Less(i, j int) bool { return bySlotInfo(c).Less(i, j) } - var ( FindSnapdPath = findSnapdPath ) diff --git a/interfaces/sorting.go b/interfaces/sorting.go index 1c1961365b2..f5bb3a345d4 100644 --- a/interfaces/sorting.go +++ b/interfaces/sorting.go @@ -113,17 +113,3 @@ func (c byInterfaceName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c byInterfaceName) Less(i, j int) bool { return c[i].Name() < c[j].Name() } - -type bySlotInfo []*snap.SlotInfo - -func (c bySlotInfo) Len() int { return len(c) } -func (c bySlotInfo) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c bySlotInfo) Less(i, j int) bool { - if c[i].Snap.SnapName() != c[j].Snap.SnapName() { - return c[i].Snap.SnapName() < c[j].Snap.SnapName() - } - if c[i].Snap.InstanceKey != c[j].Snap.InstanceKey { - return c[i].Snap.InstanceKey < c[j].Snap.InstanceKey - } - return c[i].Name < c[j].Name -} diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index 3ecbf507417..adf8f8e7b16 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -49,28 +49,6 @@ func (s *SortingSuite) TestByInterfaceName(c *C) { }) } -func (s *SortingSuite) TestBySlotInfo(c *C) { - list := []*snap.SlotInfo{ - {Snap: &snap.Info{SuggestedName: "name-3"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-1"}, - } - sort.Sort(interfaces.BySlotInfo(list)) - c.Assert(list, DeepEquals, []*snap.SlotInfo{ - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-1"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-1"}, - {Snap: &snap.Info{SuggestedName: "name-2_instance"}, Name: "plug-2"}, - {Snap: &snap.Info{SuggestedName: "name-3"}, Name: "plug-2"}, - }) -} - func (s *SortingSuite) TestByConnRef(c *C) { list := []*interfaces.ConnRef{ newConnRef("name-1", "plug-3", "name-2", "slot-1"), From 9f02ca8423c571767a17738557a7cf3567b2ca1a Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 10:05:02 +0100 Subject: [PATCH 181/580] interfaces: remove unused import Signed-off-by: Zygmunt Krynicki --- interfaces/sorting_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/interfaces/sorting_test.go b/interfaces/sorting_test.go index adf8f8e7b16..f396fa3f785 100644 --- a/interfaces/sorting_test.go +++ b/interfaces/sorting_test.go @@ -26,7 +26,6 @@ import ( "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" - "github.com/snapcore/snapd/snap" ) type SortingSuite struct{} From 9b9e9b184be90b9e4f36e7c1de633e85eb28a83d Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 10:04:35 +0100 Subject: [PATCH 182/580] interfaces/apparmor: remove unused chopTree function Signed-off-by: Zygmunt Krynicki --- interfaces/apparmor/export_test.go | 1 - interfaces/apparmor/spec.go | 86 ------------------------------ interfaces/apparmor/spec_test.go | 73 ------------------------- 3 files changed, 160 deletions(-) diff --git a/interfaces/apparmor/export_test.go b/interfaces/apparmor/export_test.go index 7b3ce62ea36..ad1bf6b10cc 100644 --- a/interfaces/apparmor/export_test.go +++ b/interfaces/apparmor/export_test.go @@ -26,7 +26,6 @@ import ( ) var ( - ChopTree = chopTree NsProfile = nsProfile ProfileGlobs = profileGlobs SnapConfineFromSnapProfile = snapConfineFromSnapProfile diff --git a/interfaces/apparmor/spec.go b/interfaces/apparmor/spec.go index 78b1b50430b..9332e9f0e45 100644 --- a/interfaces/apparmor/spec.go +++ b/interfaces/apparmor/spec.go @@ -209,92 +209,6 @@ func isProbablyPresent(path string) bool { return path == "/" || path == "/snap" || path == "/var" || path == "/var/snap" || path == "/tmp" || path == "/usr" || path == "/etc" } -// chopTree takes a path and depth and returns two lists of apparmor path expressions. -// -// The returned lists of expressions are referred to as left and right. -// -// The left list describes directories at depth up to and including -// assumedPrefixDepth and can be used to grant read permission to them (by -// appending the string "r, " to each element). This corresponds to an -// assumption about those directories being present in the system and being -// just traversed. Note that depth is defined as the number of directories -// traversed, including the root directory. -// -// The right list describes the remaining directories and the leaf entry -// (either file or directory) and is somewhat subtle. At each depth level the -// expression describes all the files and directories at that level. Coupled -// with the string "rw ," it can be used to create a rule that allows write -// access to any file therein, but not deeper. -// -// For example, with path: "/foo/bar/froz/baz" and depth 3 the result is: -// []string{"/", "/foo/", "/foo/bar/"} and []string{"/foo/bar/*", -// "/foo/bar/*/", "/foo/bar/froz/*", "/foo/bar/froz/*/"}. Coupled with the -// aforementioned constants this translates to the following apparmor rules: -// -// / r, -// /foo/ r, -// /foo/bar/ r, -// -// /foo/bar/* rw, -// /foo/bar/*/ rw, -// /foo/bar/froz/* rw, -// /foo/bar/froz/*/ rw, -// -// Those rules are useful for constructing the apparmor profile for a writable -// mimic that needs to be present in a specific directory (e.g. in -// /foo/bar/froz/baz) assuming that part of that directory already exists (e.g. -// /foo/bar/) but may need to be created earlier (e.g. in /foo/bar/froz). -// -// The mimic works by mounting a tmpfs over the mimicked directory and then -// re-creating empty files and directories as mount points for the subsequent -// bind-mount operations to latch onto. This is why the right list of -// expressions use * and */, this allows the expressions to capture files and -// directories at a specific path. -func chopTree(path string, assumedPrefixDepth int) (left, right []string, err error) { - // NOTE: This implementation works around a bug in apparmor parser: - // https://bugs.launchpad.net/apparmor/+bug/1769971 - // - // Due to the nature of apparmor path expressions we need to distinguish - // directories and files. The path expression denoting a directory must end - // with a trailing slash, that denoting a file must not. - // - // The iterator requires golang-clean paths which never have a trailing - // slash. We want to allow clean paths with an optional trailing slash. - isDir := strings.HasSuffix(path, "/") - cleanPath := filepath.Clean(path) - if (isDir && cleanPath+"/" != path) || (!isDir && cleanPath != path) { - return nil, nil, fmt.Errorf("cannot chop unclean path: %q", path) - } - - // Iterate over the path and construct left and right. - iter, _ := strutil.NewPathIterator(cleanPath) - for iter.Next() { - if iter.Depth() <= assumedPrefixDepth { - // The left hand side is the part that is assumed to exist. - // We mostly enumerate those directories as-is except for the final - // entry that we re-create the trailing slash if the original path - // was a "directory" path. - if iter.CurrentPath() == iter.Path() && isDir { - left = append(left, iter.CurrentPath()+"/") - } else { - left = append(left, iter.CurrentPath()) - } - } else { - // The right hand side rules should not allow creation of the root - // directory as that itself is meaningless. - if iter.Depth() > 1 { - // The right hand side replaces the final component with a "*" - // and "*/", meaning any file and any directory, respectively. - right = append(right, iter.CurrentBase()+"*") - right = append(right, iter.CurrentBase()+"*/") - } - } - } - // Note, for completeness we could append the full path but that is - // guaranteed to be captured by one of the two expressions above. - return left, right, nil -} - // WritableMimicProfile writes apparmor rules for a writable mimic at the given path. func WritableMimicProfile(buf *bytes.Buffer, path string, assumedPrefixDepth int) { fmt.Fprintf(buf, " # Writable mimic %s\n", path) diff --git a/interfaces/apparmor/spec_test.go b/interfaces/apparmor/spec_test.go index b2db85354b9..2454127efcf 100644 --- a/interfaces/apparmor/spec_test.go +++ b/interfaces/apparmor/spec_test.go @@ -428,79 +428,6 @@ func (s *specSuite) TestApparmorSnippetsFromLayout(c *C) { c.Assert(updateNS, DeepEquals, []string{profile0, profile1, profile2, profile3}) } -func (s *specSuite) TestChopTree(c *C) { - for _, tc := range []struct { - p string // path - d int // depth - l, r []string // left and right path expressions - e string // error pattern, if non-empty - }{ - // Test case from the documentation of the function. - {p: "/foo/bar/froz/baz/", d: 3, // Assume first three directories exist - l: []string{"/", "/foo/", "/foo/bar/"}, - // Assume that /foo/bar/froz and beyond may be missing - r: []string{"/foo/bar/*", "/foo/bar/*/", "/foo/bar/froz/*", "/foo/bar/froz/*/"}}, - - // Exhaustive test cases for directory paths. - - {p: "/foo/bar/froz/", d: 0, // Assume that no directories exist (and '/' does not have 'w') - r: []string{"/*", "/*/", "/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz/", d: 1, // Assume that the root directory exists - l: []string{"/"}, - r: []string{"/*", "/*/", "/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz/", d: 2, // Assume that /foo/ exists. - l: []string{"/", "/foo/"}, - r: []string{"/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz/", d: 3, // Assume that /foo/bar/ exists. - l: []string{"/", "/foo/", "/foo/bar/"}, - r: []string{"/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz/", d: 4, // Assume that /foo/bar/froz/ exists. - l: []string{"/", "/foo/", "/foo/bar/", "/foo/bar/froz/"}}, - - // Exhaustive test cases for file paths. - - {p: "/foo/bar/froz", d: 0, // Assume that no directories exist (and '/' does not have 'w') - r: []string{"/*", "/*/", "/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz", d: 1, // Assume that the root directory exists - l: []string{"/"}, - r: []string{"/*", "/*/", "/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz", d: 2, // Assume that /foo/ exists. - l: []string{"/", "/foo/"}, - r: []string{"/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz", d: 3, // Assume that /foo/bar/ exists. - l: []string{"/", "/foo/", "/foo/bar/"}, - r: []string{"/foo/bar/*", "/foo/bar/*/"}}, - {p: "/foo/bar/froz", d: 4, // Assume that /foo/bar/froz exists. - l: []string{"/", "/foo/", "/foo/bar/", "/foo/bar/froz"}}, - - // Assumed prefix depth larger than actual path depth is harmless. - {p: "/foo/bar/froz/", d: 5, - l: []string{"/", "/foo/", "/foo/bar/", "/foo/bar/froz/"}}, - {p: "/foo/bar/froz", d: 5, - l: []string{"/", "/foo/", "/foo/bar/", "/foo/bar/froz"}}, - - // Unclean paths are not allowed. - {p: "/foo/../bar", d: 1, e: "cannot chop unclean path: .*"}, - {p: "/foo//bar", d: 1, e: "cannot chop unclean path: .*"}, - {p: "foo/../bar", d: 1, e: "cannot chop unclean path: .*"}, - {p: "foo//bar", d: 1, e: "cannot chop unclean path: .*"}, - - // https://twitter.com/thebox193/status/654457902208557056 - {p: "/foo/bar/froz/", d: -1, - r: []string{"/*", "/*/", "/foo/*", "/foo/*/", "/foo/bar/*", "/foo/bar/*/"}}, - } { - l, r, err := apparmor.ChopTree(tc.p, tc.d) - comment := Commentf("test case: %#v", tc) - if tc.e == "" { - c.Assert(err, IsNil, comment) - c.Assert(l, DeepEquals, tc.l, comment) - c.Assert(r, DeepEquals, tc.r, comment) - } else { - c.Assert(err, ErrorMatches, tc.e, comment) - } - } -} - const snapTrivial = ` name: some-snap version: 0 From c20fff8255cef16b489dec0cd4d8dc2db771b718 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 10:06:52 +0100 Subject: [PATCH 183/580] interfaces/policy: remove unused plugSnapType method Signed-off-by: Zygmunt Krynicki --- interfaces/policy/policy.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interfaces/policy/policy.go b/interfaces/policy/policy.go index 72793b21061..f7a00dd5a28 100644 --- a/interfaces/policy/policy.go +++ b/interfaces/policy/policy.go @@ -148,10 +148,6 @@ func (connc *ConnectCandidate) SlotAttr(arg string) (interface{}, error) { return nestedGet("slot", connc.Slot, arg) } -func (connc *ConnectCandidate) plugSnapType() snap.Type { - return connc.Plug.Snap().Type -} - func (connc *ConnectCandidate) slotSnapType() snap.Type { return connc.Slot.Snap().Type } From 8b2476a99beb4f5d06d0c53190e7f9ffd158a1c3 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 10:07:34 +0100 Subject: [PATCH 184/580] interfaces/policy: remove unused slotSnapType method Signed-off-by: Zygmunt Krynicki --- interfaces/policy/policy.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interfaces/policy/policy.go b/interfaces/policy/policy.go index f7a00dd5a28..d60ff8ba8ac 100644 --- a/interfaces/policy/policy.go +++ b/interfaces/policy/policy.go @@ -148,10 +148,6 @@ func (connc *ConnectCandidate) SlotAttr(arg string) (interface{}, error) { return nestedGet("slot", connc.Slot, arg) } -func (connc *ConnectCandidate) slotSnapType() snap.Type { - return connc.Slot.Snap().Type -} - func (connc *ConnectCandidate) plugSnapID() string { if connc.PlugSnapDeclaration != nil { return connc.PlugSnapDeclaration.SnapID() From c1be59e5188bc16961f2f0099db5bd534910a77e Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 11:06:56 +0100 Subject: [PATCH 185/580] release-tools: use #doc# marker for inline docs Signed-off-by: Zygmunt Krynicki --- release-tools/repack-debian-tarball.sh | 39 +++++++++++++------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/release-tools/repack-debian-tarball.sh b/release-tools/repack-debian-tarball.sh index 88156ad624b..38f1d4b4391 100755 --- a/release-tools/repack-debian-tarball.sh +++ b/release-tools/repack-debian-tarball.sh @@ -1,22 +1,23 @@ #!/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 +#doc# This script is used to re-pack the "orig" tarball from the Debian package +#doc# into a suitable upstream release. There are two changes applied: The Debian +#doc# tarball contains the directory snapd.upstream/ which needs to become +#doc# snapd-$VERSION. The Debian tarball contains the vendor/ directory which must +#doc# be removed from one of those. +#doc# +#doc# Example usage, using tarball from the archive or from the image ppa: +#doc# +#doc# $ wget https://launchpad.net/ubuntu/+archive/primary/+files/snapd_2.31.2.tar.xz +#doc# $ wget https://launchpad.net/~snappy-dev/+archive/ubuntu/image/+files/snapd_2.32.1.tar.xz +#doc# +#doc# $ repack-debian-tarball.sh snapd_2.31.2.tar.xz +#doc# +#doc# This will produce three files that need to be added to the github release page: +#doc# +#doc# - snapd_2.31.2.no-vendor.tar.xz +#doc# - snapd_2.31.2.vendor.tar.xz +#doc# - snapd_2.31.2.only-vendor.xz + set -ue # Get the filename from argv[1] @@ -24,7 +25,7 @@ debian_tarball="${1:-}" if [ "$debian_tarball" = "" ]; then echo "Usage: repack-debian-tarball.sh " echo - head -n 19 "$0" | tail -n 18 | sed -e 's/#[ ]*//g' + grep -e '^#doc#' "$0" | cut -b 7- exit 1 fi From abf625275241113c000f3fca94046b818233e96c Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 7 Jan 2019 13:08:11 +0100 Subject: [PATCH 186/580] systemd: allow only a single daemon-reload at the same time This is an RFC PR to see if the "mount protocol error" reported in systemd/systemd#10872 can be worked around by serializing the mount unit adding/removal. Proposing to get full spread runs. This is similar to #6243 but it goes further by ensuring a single daemon reload on the systemd go package level. Note that there is still a chance that the protocol error happens if something else (like dpkg or the user) runs "systemd daemon-reload" while we write a mount unit. But the risk should be hughely smaller. --- overlord/snapstate/backend/mountunit.go | 60 ++-------------- systemd/systemd.go | 92 +++++++++++++++++++++++-- systemd/systemd_test.go | 12 ++-- wrappers/core18.go | 2 +- 4 files changed, 98 insertions(+), 68 deletions(-) diff --git a/overlord/snapstate/backend/mountunit.go b/overlord/snapstate/backend/mountunit.go index af1b661b414..001095ebb80 100644 --- a/overlord/snapstate/backend/mountunit.go +++ b/overlord/snapstate/backend/mountunit.go @@ -20,13 +20,7 @@ package backend import ( - "os" - "os/exec" - "path/filepath" - "time" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/systemd" @@ -37,57 +31,11 @@ func addMountUnit(s *snap.Info, meter progress.Meter) error { whereDir := dirs.StripRootDir(s.MountDir()) sysd := systemd.New(dirs.GlobalRootDir, meter) - mountUnitName, err := sysd.WriteMountUnitFile(s.InstanceName(), s.Revision.String(), squashfsPath, whereDir, "squashfs") - if err != nil { - return err - } - - // we need to do a daemon-reload here to ensure that systemd really - // knows about this new mount unit file - if err := sysd.DaemonReload(); err != nil { - return err - } - - if err := sysd.Enable(mountUnitName); err != nil { - return err - } - - return sysd.Start(mountUnitName) + _, err := sysd.AddMountUnitFile(s.InstanceName(), s.Revision.String(), squashfsPath, whereDir, "squashfs") + return err } -func removeMountUnit(baseDir string, meter progress.Meter) error { +func removeMountUnit(mountDir string, meter progress.Meter) error { sysd := systemd.New(dirs.GlobalRootDir, meter) - unit := systemd.MountUnitPath(dirs.StripRootDir(baseDir)) - if osutil.FileExists(unit) { - // use umount -d (cleanup loopback devices) -l (lazy) to ensure that even busy mount points - // can be unmounted. - // note that the long option --lazy is not supported on trusty. - // the explicit -d is only needed on trusty. - isMounted, err := osutil.IsMounted(baseDir) - if err != nil { - return err - } - if isMounted { - if output, err := exec.Command("umount", "-d", "-l", baseDir).CombinedOutput(); err != nil { - return osutil.OutputErr(output, err) - } - - if err := sysd.Stop(filepath.Base(unit), time.Duration(1*time.Second)); err != nil { - return err - } - } - if err := sysd.Disable(filepath.Base(unit)); err != nil { - return err - } - if err := os.Remove(unit); err != nil { - return err - } - // daemon-reload to ensure that systemd actually really - // forgets about this mount unit - if err := sysd.DaemonReload(); err != nil { - return err - } - } - - return nil + return sysd.RemoveMountUnitFile(mountDir) } diff --git a/systemd/systemd.go b/systemd/systemd.go index 647b1ec0e96..09a8746d33b 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -23,11 +23,13 @@ import ( "errors" "fmt" "io" + "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" + "sync" "time" _ "github.com/snapcore/squashfuse" @@ -46,6 +48,15 @@ var ( // how much time should Stop wait between notifying the user of the waiting stopNotifyDelay = 20 * time.Second + + // daemonReloadLock is a package level lock to ensure that we + // do not run any `systemd daemon-reload` while a + // daemon-reload is in progress or a mount unit is + // generated/activated. + // + // See https://github.com/systemd/systemd/issues/10872 for the + // upstream systemd bug + daemonReloadLock sync.Mutex ) // systemctlCmd calls systemctl with the given args, returning its standard output (and wrapped error) @@ -133,7 +144,8 @@ type Systemd interface { IsEnabled(service string) (bool, error) IsActive(service string) (bool, error) LogReader(services []string, n int, follow bool) (io.ReadCloser, error) - WriteMountUnitFile(name, revision, what, where, fstype string) (string, error) + AddMountUnitFile(name, revision, what, where, fstype string) (string, error) + RemoveMountUnitFile(baseDir string) error Mask(service string) error Unmask(service string) error } @@ -170,7 +182,14 @@ type systemd struct { } // DaemonReload reloads systemd's configuration. -func (*systemd) DaemonReload() error { +func (s *systemd) DaemonReload() error { + daemonReloadLock.Lock() + defer daemonReloadLock.Unlock() + + return s.daemonReloadNoLock() +} + +func (s *systemd) daemonReloadNoLock() error { _, err := systemctlCmd("daemon-reload") return err } @@ -486,7 +505,11 @@ func MountUnitPath(baseDir string) string { return filepath.Join(dirs.SnapServicesDir, escapedPath+".mount") } -func (s *systemd) WriteMountUnitFile(name, revision, what, where, fstype string) (string, error) { +// AddMountUnitFile adds/enables/starts a mount unit. +func (s *systemd) AddMountUnitFile(snapName, revision, what, where, fstype string) (string, error) { + daemonReloadLock.Lock() + defer daemonReloadLock.Unlock() + options := []string{"nodev"} if fstype == "squashfs" { newFsType, newOptions, err := squashfs.FsType() @@ -513,8 +536,67 @@ Options=%s [Install] WantedBy=multi-user.target -`, name, revision, what, where, fstype, strings.Join(options, ",")) +`, snapName, revision, what, where, fstype, strings.Join(options, ",")) mu := MountUnitPath(where) - return filepath.Base(mu), osutil.AtomicWriteFile(mu, []byte(c), 0644, 0) + mountUnitName, err := filepath.Base(mu), osutil.AtomicWriteFile(mu, []byte(c), 0644, 0) + if err != nil { + return "", err + } + + // we need to do a daemon-reload here to ensure that systemd really + // knows about this new mount unit file + if err := s.daemonReloadNoLock(); err != nil { + return "", err + } + + if err := s.Enable(mountUnitName); err != nil { + return "", err + } + if err := s.Start(mountUnitName); err != nil { + return "", err + } + + return mountUnitName, nil +} + +func (s *systemd) RemoveMountUnitFile(mountedDir string) error { + daemonReloadLock.Lock() + defer daemonReloadLock.Unlock() + + unit := MountUnitPath(dirs.StripRootDir(mountedDir)) + if !osutil.FileExists(unit) { + return nil + } + + // use umount -d (cleanup loopback devices) -l (lazy) to ensure that even busy mount points + // can be unmounted. + // note that the long option --lazy is not supported on trusty. + // the explicit -d is only needed on trusty. + isMounted, err := osutil.IsMounted(mountedDir) + if err != nil { + return err + } + if isMounted { + if output, err := exec.Command("umount", "-d", "-l", mountedDir).CombinedOutput(); err != nil { + return osutil.OutputErr(output, err) + } + + if err := s.Stop(filepath.Base(unit), time.Duration(1*time.Second)); err != nil { + return err + } + } + if err := s.Disable(filepath.Base(unit)); err != nil { + return err + } + if err := os.Remove(unit); err != nil { + return err + } + // daemon-reload to ensure that systemd actually really + // forgets about this mount unit + if err := s.daemonReloadNoLock(); err != nil { + return err + } + + return nil } diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 0963705158e..78e38a0cde5 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -486,7 +486,7 @@ func (s *SystemdTestSuite) TestMountUnitPath(c *C) { c.Assert(MountUnitPath("/apps/hello/1.1"), Equals, filepath.Join(dirs.SnapServicesDir, "apps-hello-1.1.mount")) } -func (s *SystemdTestSuite) TestWriteMountUnit(c *C) { +func (s *SystemdTestSuite) TestAddMountUnit(c *C) { restore := squashfs.MockUseFuse(false) defer restore() @@ -496,7 +496,7 @@ func (s *SystemdTestSuite) TestWriteMountUnit(c *C) { err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", nil).WriteMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New("", nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -516,13 +516,13 @@ WantedBy=multi-user.target `[1:], mockSnapPath)) } -func (s *SystemdTestSuite) TestWriteMountUnitForDirs(c *C) { +func (s *SystemdTestSuite) TestAddMountUnitForDirs(c *C) { restore := squashfs.MockUseFuse(false) defer restore() // a directory instead of a file produces a different output snapDir := c.MkDir() - mountUnitName, err := New("", nil).WriteMountUnitFile("foodir", "x1", snapDir, "/snap/snapname/x1", "squashfs") + mountUnitName, err := New("", nil).AddMountUnitFile("foodir", "x1", snapDir, "/snap/snapname/x1", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -564,7 +564,7 @@ exit 0 err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", nil).WriteMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New("", nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -602,7 +602,7 @@ exit 0 err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", nil).WriteMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New("", nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) diff --git a/wrappers/core18.go b/wrappers/core18.go index 336faa99ec9..a6e363bd77f 100644 --- a/wrappers/core18.go +++ b/wrappers/core18.go @@ -40,7 +40,7 @@ import ( var execStartRe = regexp.MustCompile(`(?m)^ExecStart=(/usr/bin/snap\s+.*|/usr/lib/snapd/.*)$`) func writeSnapdToolingMountUnit(sysd systemd.Systemd, prefix string) error { - // Not using WriteMountUnitFile() because we need + // Not using AddMountUnitFile() because we need // "RequiredBy=snapd.service" content := []byte(fmt.Sprintf(`[Unit] Description=Make the snapd snap tooling available for the system From af1e55c283dc2be2618c0bac3fdfc564e826beed Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Mon, 7 Jan 2019 17:16:23 +0100 Subject: [PATCH 187/580] spread: make Fedora 29 auto again Probing whether the repos are stable now. Signed-off-by: Maciej Borzecki --- spread.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/spread.yaml b/spread.yaml index c9da6bd6c6a..e52da957826 100644 --- a/spread.yaml +++ b/spread.yaml @@ -81,8 +81,6 @@ backends: manual: true - fedora-29-64: workers: 4 - # https://twitter.com/zygoon/status/1073629342884864000 - manual: true - opensuse-42.3-64: workers: 4 # golang stack cannot compile anything, needs investigation From 7c0d3f0f800115874296c4013ac22417f1f6d0d8 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 7 Jan 2019 17:30:01 +0100 Subject: [PATCH 188/580] address review feedback (thanks to zyga) --- interfaces/builtin/common_files.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interfaces/builtin/common_files.go b/interfaces/builtin/common_files.go index 72e555c3afe..ef1804e8f43 100644 --- a/interfaces/builtin/common_files.go +++ b/interfaces/builtin/common_files.go @@ -51,7 +51,7 @@ func (a filesAAPerm) String() string { case filesRead: return "rk" // [r]ead and loc[k] case filesWrite: - return "rwkl" + return "rwkl" // [r]ead, [w]rite, loc[k] and [l]ink// } panic(fmt.Sprintf("invalid perm: %d", a)) } @@ -69,9 +69,10 @@ func formatPath(ip interface{}) (string, error) { p = strings.Replace(p, "$HOME", "@{HOME}", -1) prefix = "owner " } + p = filepath.Clean(p) p += "{,/,/**}" - return fmt.Sprintf("%s%q", prefix, filepath.Clean(p)), nil + return fmt.Sprintf("%s%q", prefix, p), nil } func allowPathAccess(buf *bytes.Buffer, perm filesAAPerm, paths []interface{}) error { From 8c3ad352c7e5dc668395551c1c549e113d4850f1 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 11:50:47 +0100 Subject: [PATCH 189/580] daemon: intrduce /v2/connections snapd API endpoint Introduce new endpoint for querying about interface connections. The endpoints provides means to filter based on snap, interface, slot/plug names as well as whether an interface is connected. By default, only connected interfaces are returned. Reuse the code query code for legacy interfaces endpoint. Signed-off-by: Maciej Borzecki --- daemon/api.go | 193 ++++++++++++----- daemon/api_test.go | 512 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 655 insertions(+), 50 deletions(-) diff --git a/daemon/api.go b/daemon/api.go index b871f0ee337..9f3f0378b2f 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -101,6 +101,7 @@ var api = []*Command{ warningsCmd, debugCmd, snapshotCmd, + connectionsCmd, } var ( @@ -183,6 +184,12 @@ var ( POST: changeInterfaces, } + connectionsCmd = &Command{ + Path: "/v2/connections", + UserOK: true, + GET: getConnections, + } + // TODO: allow to post assertions for UserOK? they are verified anyway assertsCmd = &Command{ Path: "/v2/assertions", @@ -1723,6 +1730,141 @@ func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { return AsyncResponse(nil, &Meta{Change: change.ID()}) } +type collectFilter struct { + snapName string + plugSlotName string + ifaceName string + connected bool +} + +func (c *collectFilter) plugMatches(plug *interfaces.PlugRef, connectedSlots []interfaces.SlotRef) bool { + for _, slot := range connectedSlots { + if c.slotMatches(&slot, nil) { + return true + } + } + if c.snapName != "" && plug.Snap != c.snapName { + return false + } + if c.plugSlotName != "" && plug.Name != c.plugSlotName { + return false + } + return true +} + +func (c *collectFilter) slotMatches(slot *interfaces.SlotRef, connectedPlugs []interfaces.PlugRef) bool { + for _, plug := range connectedPlugs { + if c.plugMatches(&plug, nil) { + return true + } + } + if c.snapName != "" && slot.Snap != c.snapName { + return false + } + if c.plugSlotName != "" && slot.Name != c.plugSlotName { + return false + } + return true +} + +func (c *collectFilter) ifaceMatches(ifaceName string) bool { + if c.ifaceName != "" && c.ifaceName != ifaceName { + return false + } + return true +} + +func collectConnections(ifaceMgr *ifacestate.InterfaceManager, filter collectFilter) interfaceJSON { + repo := ifaceMgr.Repository() + ifaces := repo.Interfaces() + + var ifjson interfaceJSON + plugConns := map[string][]interfaces.SlotRef{} + slotConns := map[string][]interfaces.PlugRef{} + + for _, cref := range ifaces.Connections { + if !filter.plugMatches(&cref.PlugRef, nil) && !filter.slotMatches(&cref.SlotRef, nil) { + continue + } + plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name} + slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name} + plugID := plugRef.String() + slotID := slotRef.String() + plugConns[plugID] = append(plugConns[plugID], slotRef) + slotConns[slotID] = append(slotConns[slotID], plugRef) + } + + for _, plug := range ifaces.Plugs { + plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name} + connectedSlots, connected := plugConns[plugRef.String()] + if !connected && filter.connected { + continue + } + if !filter.ifaceMatches(plug.Interface) || !filter.plugMatches(&plugRef, connectedSlots) { + continue + } + var apps []string + for _, app := range plug.Apps { + apps = append(apps, app.Name) + } + pj := &plugJSON{ + Snap: plugRef.Snap, + Name: plugRef.Name, + Interface: plug.Interface, + Attrs: plug.Attrs, + Apps: apps, + Label: plug.Label, + Connections: connectedSlots, + } + ifjson.Plugs = append(ifjson.Plugs, pj) + } + for _, slot := range ifaces.Slots { + slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name} + connectedPlugs, connected := slotConns[slotRef.String()] + if !connected && filter.connected { + continue + } + if !filter.ifaceMatches(slot.Interface) || !filter.slotMatches(&slotRef, connectedPlugs) { + continue + } + var apps []string + for _, app := range slot.Apps { + apps = append(apps, app.Name) + } + sj := &slotJSON{ + Snap: slotRef.Snap, + Name: slotRef.Name, + Interface: slot.Interface, + Attrs: slot.Attrs, + Apps: apps, + Label: slot.Label, + Connections: connectedPlugs, + } + ifjson.Slots = append(ifjson.Slots, sj) + } + return ifjson +} + +func getConnections(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + snapName := query.Get("snap") + plugSlotName := query.Get("name") + ifaceName := query.Get("interface") + qselect := query.Get("select") + if qselect != "all" && qselect != "" { + return BadRequest("unsupported select qualifier") + } + onlyConnected := qselect == "" + + ifjson := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{ + snapName: snapName, + plugSlotName: plugSlotName, + ifaceName: ifaceName, + connected: onlyConnected, + }) + return SyncResponse(ifjson, nil) +} + // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces). func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response { query := r.URL.Query() @@ -1789,56 +1931,7 @@ func getInterfaces(c *Command, r *http.Request, user *auth.UserState) Response { } func getLegacyConnections(c *Command, r *http.Request, user *auth.UserState) Response { - repo := c.d.overlord.InterfaceManager().Repository() - ifaces := repo.Interfaces() - - var ifjson interfaceJSON - plugConns := map[string][]interfaces.SlotRef{} - slotConns := map[string][]interfaces.PlugRef{} - - for _, cref := range ifaces.Connections { - plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name} - slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name} - plugID := plugRef.String() - slotID := slotRef.String() - plugConns[plugID] = append(plugConns[plugID], slotRef) - slotConns[slotID] = append(slotConns[slotID], plugRef) - } - - for _, plug := range ifaces.Plugs { - var apps []string - for _, app := range plug.Apps { - apps = append(apps, app.Name) - } - plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name} - pj := &plugJSON{ - Snap: plugRef.Snap, - Name: plugRef.Name, - Interface: plug.Interface, - Attrs: plug.Attrs, - Apps: apps, - Label: plug.Label, - Connections: plugConns[plugRef.String()], - } - ifjson.Plugs = append(ifjson.Plugs, pj) - } - for _, slot := range ifaces.Slots { - var apps []string - for _, app := range slot.Apps { - apps = append(apps, app.Name) - } - slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name} - sj := &slotJSON{ - Snap: slotRef.Snap, - Name: slotRef.Name, - Interface: slot.Interface, - Attrs: slot.Attrs, - Apps: apps, - Label: slot.Label, - Connections: slotConns[slotRef.String()], - } - ifjson.Slots = append(ifjson.Slots, sj) - } + ifjson := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{}) return SyncResponse(ifjson, nil) } diff --git a/daemon/api_test.go b/daemon/api_test.go index dac46075265..2bf7c73243c 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -3922,6 +3922,518 @@ func (m *inverseCaseMapper) SystemSnapName() string { return "core" } +// Tests for GET /v2/connections + +func (s *apiSuite) testConnections(c *check.C, query string, expected map[string]interface{}) { + req, err := http.NewRequest("GET", query, nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, expected) +} + +func (s *apiSuite) TestConnectionsUnhappy(c *check.C) { + s.daemon(c) + req, err := http.NewRequest("GET", "/v2/connections?select=bad", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "unsupported select qualifier", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestConnectionsEmpty(c *check.C) { + s.daemon(c) + s.testConnections(c, "/v2/connections", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsUnconnected(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsBySnapName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + s.testConnections(c, "/v2/connections?select=all&snap=producer", map[string]interface{}{ + "result": map[string]interface{}{ + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + s.testConnections(c, "/v2/connections?select=all&snap=consumer", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?snap=producer", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsByIfaceName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + var differentProducerYaml = ` +name: different-producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + var differentConsumerYaml = ` +name: different-consumer +version: 1 +apps: + app: +plugs: + plug: + interface: different + key: value + label: label +` + s.mockSnap(c, differentProducerYaml) + s.mockSnap(c, differentConsumerYaml) + + s.testConnections(c, "/v2/connections?select=all&interface=test", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all&interface=different", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "different-consumer", + "plug": "plug", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "different-producer", + "slot": "slot", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?interface=test", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?interface=different", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsByPlugSlotName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + var differentProducerYaml = ` +name: different-producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + s.mockSnap(c, differentProducerYaml) + + s.testConnections(c, "/v2/connections?select=all&name=slot", map[string]interface{}{ + "result": map[string]interface{}{ + "slots": []interface{}{ + map[string]interface{}{ + "snap": "different-producer", + "slot": "slot", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all&name=plug", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?name=plug", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?name=slot", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsDefault(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + // Tests for GET /v2/interfaces func (s *apiSuite) TestInterfacesLegacy(c *check.C) { From c04deb174bdcb56a8eaf51cbc458aff6dc4e3724 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 12:55:20 +0100 Subject: [PATCH 190/580] release: tweak SELinux summary info text Signed-off-by: Maciej Borzecki --- release/selinux.go | 2 +- release/selinux_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/release/selinux.go b/release/selinux.go index d6aa77a278e..01e6a743613 100644 --- a/release/selinux.go +++ b/release/selinux.go @@ -74,7 +74,7 @@ func probeSELinux() (SELinuxLevelType, string) { return NoSELinux, fmt.Sprintf("SELinux is enabled, but status cannot be determined: %v", err) } if !enforcing { - return SELinuxPermissive, "SELinux is enabled and in permissive mode" + return SELinuxPermissive, "SELinux is enabled but in permissive mode" } return SELinuxEnforcing, "SELinux is enabled and in enforcing mode" } diff --git a/release/selinux_test.go b/release/selinux_test.go index b6dde144d96..12e7bb088b0 100644 --- a/release/selinux_test.go +++ b/release/selinux_test.go @@ -91,7 +91,7 @@ func (s *selinuxSuite) TestProbePermissive(c *C) { level, status := release.ProbeSELinux() c.Assert(level, Equals, release.SELinuxPermissive) - c.Assert(status, Equals, "SELinux is enabled and in permissive mode") + c.Assert(status, Equals, "SELinux is enabled but in permissive mode") c.Assert(release.SELinuxLevel(), Equals, level) c.Assert(release.SELinuxSummary(), Equals, status) From 311509f980181659a29bfe873546957c23d24e30 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 13:45:56 +0100 Subject: [PATCH 191/580] dirs: handle different fontconfig cache location on Fedora Due to https://fedoraproject.org/wiki/Changes/FontconfigCacheDirChange which got changed in Fedora 26, the fontconfig cache directory is under /usr/lib/fontconfig/cache. The same change applies to CentOS and RHEL, both identify as ID_LIKE=fedora. Signed-off-by: Maciej Borzecki --- dirs/dirs.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dirs/dirs.go b/dirs/dirs.go index 2f5ccd6b56b..0c4a913238c 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -286,6 +286,13 @@ func SetRootDir(rootdir string) { SystemFontsDir = filepath.Join(rootdir, "/usr/share/fonts") SystemLocalFontsDir = filepath.Join(rootdir, "/usr/local/share/fonts") SystemFontconfigCacheDir = filepath.Join(rootdir, "/var/cache/fontconfig") + if release.DistroLike("fedora") { + // see: + // https://fedoraproject.org/wiki/Changes/FontconfigCacheDirChange + // https://bugzilla.redhat.com/show_bug.cgi?id=1416380 + // https://bugzilla.redhat.com/show_bug.cgi?id=1377367 + SystemFontconfigCacheDir = filepath.Join(rootdir, "/usr/lib/fontconfig/cache") + } FreezerCgroupDir = filepath.Join(rootdir, "/sys/fs/cgroup/freezer/") SnapshotsDir = filepath.Join(rootdir, snappyDir, "snapshots") From 5ed75e7fe4da9e49feb61bdc9a8dc6e585e39a81 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 13:48:00 +0100 Subject: [PATCH 192/580] interfaces/builtin/desktop: system fontconfig cache can vary across systems The system fontconfig cache directory can vary across systems. Specifically, on Fedora, due to https://fedoraproject.org/wiki/Changes/FontconfigCacheDirChange the cache is under /usr/lib/fontconfig/cache. Make sure that we use the right mount path regardless of the location of the cache in the host filesystem. Signed-off-by: Maciej Borzecki --- interfaces/builtin/desktop.go | 9 ++++++++- interfaces/builtin/desktop_test.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/interfaces/builtin/desktop.go b/interfaces/builtin/desktop.go index 7e831b1ed11..e9e80ec9cd4 100644 --- a/interfaces/builtin/desktop.go +++ b/interfaces/builtin/desktop.go @@ -281,9 +281,16 @@ func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plu if !osutil.IsDirectory(dir) { continue } + target := dirs.StripRootDir(dir) + if dir == dirs.SystemFontconfigCacheDir { + // fontconfig cache directory path is a little special + // and varies across systems, but we always mount it at + // the same location + target = "/var/cache/fontconfig" + } spec.AddMountEntry(osutil.MountEntry{ Name: "/var/lib/snapd/hostfs" + dir, - Dir: dirs.StripRootDir(dir), + Dir: target, Options: []string{"bind", "ro"}, }) } diff --git a/interfaces/builtin/desktop_test.go b/interfaces/builtin/desktop_test.go index 0a24cd361da..c87d5fd1aa1 100644 --- a/interfaces/builtin/desktop_test.go +++ b/interfaces/builtin/desktop_test.go @@ -185,6 +185,25 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { entries = spec.UserMountEntries() c.Assert(entries, HasLen, 1) c.Check(entries[0].Dir, Equals, "$XDG_RUNTIME_DIR/doc") + + // Fedora is a little special with their fontconfig cache location + restore = release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + + tmpdir = c.MkDir() + dirs.SetRootDir(tmpdir) + c.Assert(dirs.SystemFontconfigCacheDir, Equals, filepath.Join(tmpdir, "/usr/lib/fontconfig/cache")) + c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/share/fonts"), 0777), IsNil) + c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/local/share/fonts"), 0777), IsNil) + c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/lib/fontconfig/cache"), 0777), IsNil) + spec = &mount.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) + entries = spec.MountEntries() + c.Assert(entries, HasLen, 3) + + c.Check(entries[2].Name, Equals, hostfs+dirs.SystemFontconfigCacheDir) + c.Check(entries[2].Dir, Equals, "/var/cache/fontconfig") + c.Check(entries[2].Options, DeepEquals, []string{"bind", "ro"}) } func (s *DesktopInterfaceSuite) TestStaticInfo(c *C) { From de000929853ac234583adb905757ce9ba17fe725 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Tue, 8 Jan 2019 14:06:08 +0100 Subject: [PATCH 193/580] Moved slot attrs update logic to UpdateStaticSlotAttrs method of the repository. --- interfaces/repo.go | 27 +++++++++++++++++++++++++++ overlord/ifacestate/handlers.go | 23 ++++------------------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/interfaces/repo.go b/interfaces/repo.go index 6bea71d0f06..b5fbf057335 100644 --- a/interfaces/repo.go +++ b/interfaces/repo.go @@ -26,6 +26,7 @@ import ( "sync" "github.com/snapcore/snapd/interfaces/hotplug" + "github.com/snapcore/snapd/interfaces/utils" "github.com/snapcore/snapd/snap" ) @@ -805,6 +806,32 @@ func (r *Repository) SlotForHotplugKey(ifaceName, hotplugKey string) (*snap.Slot return nil, nil } +// UpdateStaticSlotAttrs updates static attributes of hotplug slot associated with given hotplugkey, and returns the resulting slot. +// Slots can only be updated if not connected to any plug. +func (r *Repository) UpdateStaticSlotAttrs(ifaceName, hotplugKey string, attrs map[string]interface{}) (*snap.SlotInfo, error) { + r.m.Lock() + defer r.m.Unlock() + + snapName, err := r.guessSystemSnapName() + if err != nil { + return nil, err + } + + for _, slotInfo := range r.slots[snapName] { + if slotInfo.Interface == ifaceName && slotInfo.HotplugKey == hotplugKey { + if len(r.slotPlugs[slotInfo]) > 0 { + // slots should be updated when disconnected, and reconnected back after updating. + return nil, fmt.Errorf("internal error: cannot update slot %s while connected", slotInfo.Name) + } + slotInfo.Attrs = utils.CopyAttributes(attrs) + return slotInfo, nil + } + } + + // slot not found + return nil, nil +} + func (r *Repository) Connections(snapName string) ([]*ConnRef, error) { r.m.Lock() defer r.m.Unlock() diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 0b7c5ffe94f..2b51ef736f0 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1274,38 +1274,23 @@ func (m *InterfaceManager) doHotplugUpdateSlot(task *state.Task, _ *tomb.Tomb) e return fmt.Errorf("internal error: cannot get slot-attrs attribute for device %s, interface %s: %s", hotplugKey, ifaceName, err) } - slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) - if err != nil { - return fmt.Errorf("internal error: cannot determine slot for device %s, interface %s: %s", hotplugKey, ifaceName, err) - } - if slot == nil { - return nil - } - stateSlots, err := getHotplugSlots(st) if err != nil { return fmt.Errorf("internal error: cannot obtain hotplug slots: %v", err) } - conns, err := m.repo.ConnectionsForHotplugKey(ifaceName, hotplugKey) + slot, err := m.repo.UpdateStaticSlotAttrs(ifaceName, hotplugKey, attrs) if err != nil { - return fmt.Errorf("internal error: cannot determine connections for device %s, interface %s: %s", hotplugKey, ifaceName, err) + return err } - - // hotplug-update-slot is meant to be run as part of a change that first disconnects all slots, then updates the slot and finally - // reconnects all connections, so that disconnect- and connect- hooks are run with old and new attributes, respectively. In theory - // we should never hit this condition as it should be prevented by conflict logic. - if len(conns) > 0 { - return fmt.Errorf("internal error: cannot update slot %s while connected", slot.Name) + if slot == nil { + return nil } if slotSpec, ok := stateSlots[slot.Name]; ok { slotSpec.StaticAttrs = attrs stateSlots[slot.Name] = slotSpec setHotplugSlots(st, stateSlots) - - // XXX: this is ugly and relies on the slot infos being kept as pointers in the repository - slot.Attrs = attrs } else { return fmt.Errorf("internal error: cannot find slot %s for device %s", slot.Name, hotplugKey) } From e9e69c907d5f8c236d776f67d9691146e9679def Mon Sep 17 00:00:00 2001 From: sergio-j-cazzolato Date: Sun, 22 Jul 2018 22:03:59 -0300 Subject: [PATCH 194/580] tests: add regression test to reproduce systemd mount protocol error This adds a regression test for the mount protocol error that systemd sometimes throws. --- tests/main/check-mount/task.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/main/check-mount/task.yaml diff --git a/tests/main/check-mount/task.yaml b/tests/main/check-mount/task.yaml new file mode 100644 index 00000000000..ff2a3ca3cfa --- /dev/null +++ b/tests/main/check-mount/task.yaml @@ -0,0 +1,9 @@ +summary: Check mount issue + +backends: [-autopkgtest] + +execute: | + for _ in $(seq 50); do + snap install test-snapd-tools test-snapd-public + snap remove test-snapd-tools test-snapd-public + done From afe7a89d42ed8e94473562ddd5a3bc27ae64ea8f Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Tue, 8 Jan 2019 16:23:49 +0100 Subject: [PATCH 195/580] improve mount-protocol-error spread test description --- tests/main/check-mount/task.yaml | 9 --------- tests/main/mount-protocol-error/task.yaml | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) delete mode 100644 tests/main/check-mount/task.yaml create mode 100644 tests/main/mount-protocol-error/task.yaml diff --git a/tests/main/check-mount/task.yaml b/tests/main/check-mount/task.yaml deleted file mode 100644 index ff2a3ca3cfa..00000000000 --- a/tests/main/check-mount/task.yaml +++ /dev/null @@ -1,9 +0,0 @@ -summary: Check mount issue - -backends: [-autopkgtest] - -execute: | - for _ in $(seq 50); do - snap install test-snapd-tools test-snapd-public - snap remove test-snapd-tools test-snapd-public - done diff --git a/tests/main/mount-protocol-error/task.yaml b/tests/main/mount-protocol-error/task.yaml new file mode 100644 index 00000000000..49a30973635 --- /dev/null +++ b/tests/main/mount-protocol-error/task.yaml @@ -0,0 +1,17 @@ +summary: Check mount bug https://forum.snapcraft.io/t/5682 + +description: | + Regression test for systemd mount race that produces a protocol + error. We added a workaround to snapd for the upstream bug + https://github.com/systemd/systemd/issues/10872 + + For more discussion on the issue see + https://forum.snapcraft.io/t/5682 + +backends: [-autopkgtest] + +execute: | + for _ in $(seq 50); do + snap install test-snapd-tools test-snapd-public + snap remove test-snapd-tools test-snapd-public + done From 6168c879a247bb7c0ceadbc95fe7c320bcf74b3c Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 16:26:24 +0100 Subject: [PATCH 196/580] daemon: move /v2/connections handler and tests to separate files Signed-off-by: Maciej Borzecki --- daemon/api.go | 141 --------- daemon/api_connections.go | 169 ++++++++++ daemon/api_connections_test.go | 544 +++++++++++++++++++++++++++++++++ daemon/api_test.go | 512 ------------------------------- 4 files changed, 713 insertions(+), 653 deletions(-) create mode 100644 daemon/api_connections.go create mode 100644 daemon/api_connections_test.go diff --git a/daemon/api.go b/daemon/api.go index 9f3f0378b2f..4077abe1168 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -184,12 +184,6 @@ var ( POST: changeInterfaces, } - connectionsCmd = &Command{ - Path: "/v2/connections", - UserOK: true, - GET: getConnections, - } - // TODO: allow to post assertions for UserOK? they are verified anyway assertsCmd = &Command{ Path: "/v2/assertions", @@ -1730,141 +1724,6 @@ func setSnapConf(c *Command, r *http.Request, user *auth.UserState) Response { return AsyncResponse(nil, &Meta{Change: change.ID()}) } -type collectFilter struct { - snapName string - plugSlotName string - ifaceName string - connected bool -} - -func (c *collectFilter) plugMatches(plug *interfaces.PlugRef, connectedSlots []interfaces.SlotRef) bool { - for _, slot := range connectedSlots { - if c.slotMatches(&slot, nil) { - return true - } - } - if c.snapName != "" && plug.Snap != c.snapName { - return false - } - if c.plugSlotName != "" && plug.Name != c.plugSlotName { - return false - } - return true -} - -func (c *collectFilter) slotMatches(slot *interfaces.SlotRef, connectedPlugs []interfaces.PlugRef) bool { - for _, plug := range connectedPlugs { - if c.plugMatches(&plug, nil) { - return true - } - } - if c.snapName != "" && slot.Snap != c.snapName { - return false - } - if c.plugSlotName != "" && slot.Name != c.plugSlotName { - return false - } - return true -} - -func (c *collectFilter) ifaceMatches(ifaceName string) bool { - if c.ifaceName != "" && c.ifaceName != ifaceName { - return false - } - return true -} - -func collectConnections(ifaceMgr *ifacestate.InterfaceManager, filter collectFilter) interfaceJSON { - repo := ifaceMgr.Repository() - ifaces := repo.Interfaces() - - var ifjson interfaceJSON - plugConns := map[string][]interfaces.SlotRef{} - slotConns := map[string][]interfaces.PlugRef{} - - for _, cref := range ifaces.Connections { - if !filter.plugMatches(&cref.PlugRef, nil) && !filter.slotMatches(&cref.SlotRef, nil) { - continue - } - plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name} - slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name} - plugID := plugRef.String() - slotID := slotRef.String() - plugConns[plugID] = append(plugConns[plugID], slotRef) - slotConns[slotID] = append(slotConns[slotID], plugRef) - } - - for _, plug := range ifaces.Plugs { - plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name} - connectedSlots, connected := plugConns[plugRef.String()] - if !connected && filter.connected { - continue - } - if !filter.ifaceMatches(plug.Interface) || !filter.plugMatches(&plugRef, connectedSlots) { - continue - } - var apps []string - for _, app := range plug.Apps { - apps = append(apps, app.Name) - } - pj := &plugJSON{ - Snap: plugRef.Snap, - Name: plugRef.Name, - Interface: plug.Interface, - Attrs: plug.Attrs, - Apps: apps, - Label: plug.Label, - Connections: connectedSlots, - } - ifjson.Plugs = append(ifjson.Plugs, pj) - } - for _, slot := range ifaces.Slots { - slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name} - connectedPlugs, connected := slotConns[slotRef.String()] - if !connected && filter.connected { - continue - } - if !filter.ifaceMatches(slot.Interface) || !filter.slotMatches(&slotRef, connectedPlugs) { - continue - } - var apps []string - for _, app := range slot.Apps { - apps = append(apps, app.Name) - } - sj := &slotJSON{ - Snap: slotRef.Snap, - Name: slotRef.Name, - Interface: slot.Interface, - Attrs: slot.Attrs, - Apps: apps, - Label: slot.Label, - Connections: connectedPlugs, - } - ifjson.Slots = append(ifjson.Slots, sj) - } - return ifjson -} - -func getConnections(c *Command, r *http.Request, user *auth.UserState) Response { - query := r.URL.Query() - snapName := query.Get("snap") - plugSlotName := query.Get("name") - ifaceName := query.Get("interface") - qselect := query.Get("select") - if qselect != "all" && qselect != "" { - return BadRequest("unsupported select qualifier") - } - onlyConnected := qselect == "" - - ifjson := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{ - snapName: snapName, - plugSlotName: plugSlotName, - ifaceName: ifaceName, - connected: onlyConnected, - }) - return SyncResponse(ifjson, nil) -} - // interfacesConnectionsMultiplexer multiplexes to either legacy (connection) or modern behavior (interfaces). func interfacesConnectionsMultiplexer(c *Command, r *http.Request, user *auth.UserState) Response { query := r.URL.Query() diff --git a/daemon/api_connections.go b/daemon/api_connections.go new file mode 100644 index 00000000000..4319bffb615 --- /dev/null +++ b/daemon/api_connections.go @@ -0,0 +1,169 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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 . + * + */ + +package daemon + +import ( + "net/http" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/ifacestate" +) + +var connectionsCmd = &Command{ + Path: "/v2/connections", + UserOK: true, + GET: getConnections, +} + +type collectFilter struct { + snapName string + plugSlotName string + ifaceName string + connected bool +} + +func (c *collectFilter) plugMatches(plug *interfaces.PlugRef, connectedSlots []interfaces.SlotRef) bool { + for _, slot := range connectedSlots { + if c.slotMatches(&slot, nil) { + return true + } + } + if c.snapName != "" && plug.Snap != c.snapName { + return false + } + if c.plugSlotName != "" && plug.Name != c.plugSlotName { + return false + } + return true +} + +func (c *collectFilter) slotMatches(slot *interfaces.SlotRef, connectedPlugs []interfaces.PlugRef) bool { + for _, plug := range connectedPlugs { + if c.plugMatches(&plug, nil) { + return true + } + } + if c.snapName != "" && slot.Snap != c.snapName { + return false + } + if c.plugSlotName != "" && slot.Name != c.plugSlotName { + return false + } + return true +} + +func (c *collectFilter) ifaceMatches(ifaceName string) bool { + if c.ifaceName != "" && c.ifaceName != ifaceName { + return false + } + return true +} + +func collectConnections(ifaceMgr *ifacestate.InterfaceManager, filter collectFilter) interfaceJSON { + repo := ifaceMgr.Repository() + ifaces := repo.Interfaces() + + var ifjson interfaceJSON + plugConns := map[string][]interfaces.SlotRef{} + slotConns := map[string][]interfaces.PlugRef{} + + for _, cref := range ifaces.Connections { + if !filter.plugMatches(&cref.PlugRef, nil) && !filter.slotMatches(&cref.SlotRef, nil) { + continue + } + plugRef := interfaces.PlugRef{Snap: cref.PlugRef.Snap, Name: cref.PlugRef.Name} + slotRef := interfaces.SlotRef{Snap: cref.SlotRef.Snap, Name: cref.SlotRef.Name} + plugID := plugRef.String() + slotID := slotRef.String() + plugConns[plugID] = append(plugConns[plugID], slotRef) + slotConns[slotID] = append(slotConns[slotID], plugRef) + } + + for _, plug := range ifaces.Plugs { + plugRef := interfaces.PlugRef{Snap: plug.Snap.InstanceName(), Name: plug.Name} + connectedSlots, connected := plugConns[plugRef.String()] + if !connected && filter.connected { + continue + } + if !filter.ifaceMatches(plug.Interface) || !filter.plugMatches(&plugRef, connectedSlots) { + continue + } + var apps []string + for _, app := range plug.Apps { + apps = append(apps, app.Name) + } + pj := &plugJSON{ + Snap: plugRef.Snap, + Name: plugRef.Name, + Interface: plug.Interface, + Attrs: plug.Attrs, + Apps: apps, + Label: plug.Label, + Connections: connectedSlots, + } + ifjson.Plugs = append(ifjson.Plugs, pj) + } + for _, slot := range ifaces.Slots { + slotRef := interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name} + connectedPlugs, connected := slotConns[slotRef.String()] + if !connected && filter.connected { + continue + } + if !filter.ifaceMatches(slot.Interface) || !filter.slotMatches(&slotRef, connectedPlugs) { + continue + } + var apps []string + for _, app := range slot.Apps { + apps = append(apps, app.Name) + } + sj := &slotJSON{ + Snap: slotRef.Snap, + Name: slotRef.Name, + Interface: slot.Interface, + Attrs: slot.Attrs, + Apps: apps, + Label: slot.Label, + Connections: connectedPlugs, + } + ifjson.Slots = append(ifjson.Slots, sj) + } + return ifjson +} + +func getConnections(c *Command, r *http.Request, user *auth.UserState) Response { + query := r.URL.Query() + snapName := query.Get("snap") + plugSlotName := query.Get("name") + ifaceName := query.Get("interface") + qselect := query.Get("select") + if qselect != "all" && qselect != "" { + return BadRequest("unsupported select qualifier") + } + onlyConnected := qselect == "" + + ifjson := collectConnections(c.d.overlord.InterfaceManager(), collectFilter{ + snapName: snapName, + plugSlotName: plugSlotName, + ifaceName: ifaceName, + connected: onlyConnected, + }) + return SyncResponse(ifjson, nil) +} diff --git a/daemon/api_connections_test.go b/daemon/api_connections_test.go new file mode 100644 index 00000000000..b374c93abf8 --- /dev/null +++ b/daemon/api_connections_test.go @@ -0,0 +1,544 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 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 . + * + */ + +package daemon + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/ifacetest" +) + +// Tests for GET /v2/connections + +func (s *apiSuite) testConnections(c *check.C, query string, expected map[string]interface{}) { + req, err := http.NewRequest("GET", query, nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 200) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, expected) +} + +func (s *apiSuite) TestConnectionsUnhappy(c *check.C) { + s.daemon(c) + req, err := http.NewRequest("GET", "/v2/connections?select=bad", nil) + c.Assert(err, check.IsNil) + rec := httptest.NewRecorder() + connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) + c.Check(rec.Code, check.Equals, 400) + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "result": map[string]interface{}{ + "message": "unsupported select qualifier", + }, + "status": "Bad Request", + "status-code": 400.0, + "type": "error", + }) +} + +func (s *apiSuite) TestConnectionsEmpty(c *check.C) { + s.daemon(c) + s.testConnections(c, "/v2/connections", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsUnconnected(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsBySnapName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + s.testConnections(c, "/v2/connections?select=all&snap=producer", map[string]interface{}{ + "result": map[string]interface{}{ + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + s.testConnections(c, "/v2/connections?select=all&snap=consumer", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?snap=producer", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsByIfaceName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + var differentProducerYaml = ` +name: different-producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + var differentConsumerYaml = ` +name: different-consumer +version: 1 +apps: + app: +plugs: + plug: + interface: different + key: value + label: label +` + s.mockSnap(c, differentProducerYaml) + s.mockSnap(c, differentConsumerYaml) + + s.testConnections(c, "/v2/connections?select=all&interface=test", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all&interface=different", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "different-consumer", + "plug": "plug", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "different-producer", + "slot": "slot", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?interface=test", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?interface=different", map[string]interface{}{ + "result": map[string]interface{}{}, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsByPlugSlotName(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + var differentProducerYaml = ` +name: different-producer +version: 1 +apps: + app: +slots: + slot: + interface: different + key: value + label: label +` + s.mockSnap(c, differentProducerYaml) + + s.testConnections(c, "/v2/connections?select=all&name=slot", map[string]interface{}{ + "result": map[string]interface{}{ + "slots": []interface{}{ + map[string]interface{}{ + "snap": "different-producer", + "slot": "slot", + "interface": "different", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?select=all&name=plug", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + + // connect the interface + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections?name=plug", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) + s.testConnections(c, "/v2/connections?name=slot", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} + +func (s *apiSuite) TestConnectionsDefault(c *check.C) { + restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) + defer restore() + + d := s.daemon(c) + + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + repo := d.overlord.InterfaceManager().Repository() + connRef := &interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, + SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, + } + _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) + c.Assert(err, check.IsNil) + + s.testConnections(c, "/v2/connections", map[string]interface{}{ + "result": map[string]interface{}{ + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "producer", "slot": "slot"}, + }, + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + "interface": "test", + "attrs": map[string]interface{}{"key": "value"}, + "apps": []interface{}{"app"}, + "label": "label", + "connections": []interface{}{ + map[string]interface{}{"snap": "consumer", "plug": "plug"}, + }, + }, + }, + }, + "status": "OK", + "status-code": 200.0, + "type": "sync", + }) +} diff --git a/daemon/api_test.go b/daemon/api_test.go index 2bf7c73243c..dac46075265 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -3922,518 +3922,6 @@ func (m *inverseCaseMapper) SystemSnapName() string { return "core" } -// Tests for GET /v2/connections - -func (s *apiSuite) testConnections(c *check.C, query string, expected map[string]interface{}) { - req, err := http.NewRequest("GET", query, nil) - c.Assert(err, check.IsNil) - rec := httptest.NewRecorder() - connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) - c.Check(rec.Code, check.Equals, 200) - var body map[string]interface{} - err = json.Unmarshal(rec.Body.Bytes(), &body) - c.Check(err, check.IsNil) - c.Check(body, check.DeepEquals, expected) -} - -func (s *apiSuite) TestConnectionsUnhappy(c *check.C) { - s.daemon(c) - req, err := http.NewRequest("GET", "/v2/connections?select=bad", nil) - c.Assert(err, check.IsNil) - rec := httptest.NewRecorder() - connectionsCmd.GET(connectionsCmd, req, nil).ServeHTTP(rec, req) - c.Check(rec.Code, check.Equals, 400) - var body map[string]interface{} - err = json.Unmarshal(rec.Body.Bytes(), &body) - c.Check(err, check.IsNil) - c.Check(body, check.DeepEquals, map[string]interface{}{ - "result": map[string]interface{}{ - "message": "unsupported select qualifier", - }, - "status": "Bad Request", - "status-code": 400.0, - "type": "error", - }) -} - -func (s *apiSuite) TestConnectionsEmpty(c *check.C) { - s.daemon(c) - s.testConnections(c, "/v2/connections", map[string]interface{}{ - "result": map[string]interface{}{}, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ - "result": map[string]interface{}{}, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - -func (s *apiSuite) TestConnectionsUnconnected(c *check.C) { - restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) - defer restore() - - s.daemon(c) - - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) - - s.testConnections(c, "/v2/connections?select=all", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - -func (s *apiSuite) TestConnectionsBySnapName(c *check.C) { - restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) - defer restore() - - d := s.daemon(c) - - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) - - s.testConnections(c, "/v2/connections?select=all&snap=producer", map[string]interface{}{ - "result": map[string]interface{}{ - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - - s.testConnections(c, "/v2/connections?select=all&snap=consumer", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - - // connect the interface - repo := d.overlord.InterfaceManager().Repository() - connRef := &interfaces.ConnRef{ - PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, - SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, - } - _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) - c.Assert(err, check.IsNil) - - s.testConnections(c, "/v2/connections?snap=producer", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "producer", "slot": "slot"}, - }, - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "consumer", "plug": "plug"}, - }, - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - -func (s *apiSuite) TestConnectionsByIfaceName(c *check.C) { - restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) - defer restore() - restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) - defer restore() - - d := s.daemon(c) - - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) - var differentProducerYaml = ` -name: different-producer -version: 1 -apps: - app: -slots: - slot: - interface: different - key: value - label: label -` - var differentConsumerYaml = ` -name: different-consumer -version: 1 -apps: - app: -plugs: - plug: - interface: different - key: value - label: label -` - s.mockSnap(c, differentProducerYaml) - s.mockSnap(c, differentConsumerYaml) - - s.testConnections(c, "/v2/connections?select=all&interface=test", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - s.testConnections(c, "/v2/connections?select=all&interface=different", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "different-consumer", - "plug": "plug", - "interface": "different", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "different-producer", - "slot": "slot", - "interface": "different", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - - // connect the interface - repo := d.overlord.InterfaceManager().Repository() - connRef := &interfaces.ConnRef{ - PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, - SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, - } - _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) - c.Assert(err, check.IsNil) - - s.testConnections(c, "/v2/connections?interface=test", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "producer", "slot": "slot"}, - }, - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "consumer", "plug": "plug"}, - }, - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - s.testConnections(c, "/v2/connections?interface=different", map[string]interface{}{ - "result": map[string]interface{}{}, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - -func (s *apiSuite) TestConnectionsByPlugSlotName(c *check.C) { - restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) - defer restore() - restore = builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "different"}) - defer restore() - - d := s.daemon(c) - - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) - var differentProducerYaml = ` -name: different-producer -version: 1 -apps: - app: -slots: - slot: - interface: different - key: value - label: label -` - s.mockSnap(c, differentProducerYaml) - - s.testConnections(c, "/v2/connections?select=all&name=slot", map[string]interface{}{ - "result": map[string]interface{}{ - "slots": []interface{}{ - map[string]interface{}{ - "snap": "different-producer", - "slot": "slot", - "interface": "different", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - s.testConnections(c, "/v2/connections?select=all&name=plug", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - - // connect the interface - repo := d.overlord.InterfaceManager().Repository() - connRef := &interfaces.ConnRef{ - PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, - SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, - } - _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) - c.Assert(err, check.IsNil) - - s.testConnections(c, "/v2/connections?name=plug", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "producer", "slot": "slot"}, - }, - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "consumer", "plug": "plug"}, - }, - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) - s.testConnections(c, "/v2/connections?name=slot", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "producer", "slot": "slot"}, - }, - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "consumer", "plug": "plug"}, - }, - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - -func (s *apiSuite) TestConnectionsDefault(c *check.C) { - restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) - defer restore() - - d := s.daemon(c) - - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) - - repo := d.overlord.InterfaceManager().Repository() - connRef := &interfaces.ConnRef{ - PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, - SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, - } - _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) - c.Assert(err, check.IsNil) - - s.testConnections(c, "/v2/connections", map[string]interface{}{ - "result": map[string]interface{}{ - "plugs": []interface{}{ - map[string]interface{}{ - "snap": "consumer", - "plug": "plug", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "producer", "slot": "slot"}, - }, - }, - }, - "slots": []interface{}{ - map[string]interface{}{ - "snap": "producer", - "slot": "slot", - "interface": "test", - "attrs": map[string]interface{}{"key": "value"}, - "apps": []interface{}{"app"}, - "label": "label", - "connections": []interface{}{ - map[string]interface{}{"snap": "consumer", "plug": "plug"}, - }, - }, - }, - }, - "status": "OK", - "status-code": 200.0, - "type": "sync", - }) -} - // Tests for GET /v2/interfaces func (s *apiSuite) TestInterfacesLegacy(c *check.C) { From 77eb4086cbd63a6e25c295d03575196103436c08 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Tue, 8 Jan 2019 16:26:49 +0100 Subject: [PATCH 197/580] Tests for UpdateStaticSlotAttrs. --- interfaces/repo_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index e4ef0e775b4..704c923638d 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -2451,3 +2451,43 @@ func (s *RepositorySuite) TestHotplugMethods(c *C) { c.Assert(err, IsNil) c.Check(conns, HasLen, 0) } + +func (s *RepositorySuite) TestUpdateStaticSlotAttrs(c *C) { + c.Assert(s.testRepo.AddPlug(s.plug), IsNil) + coreSlot := &snap.SlotInfo{ + Snap: s.coreSnap, + Name: "dummy-slot", + Interface: "interface", + HotplugKey: "1234", + Attrs: map[string]interface{}{"a": "b"}, + } + c.Assert(s.testRepo.AddSlot(coreSlot), IsNil) + + slot, err := s.testRepo.UpdateStaticSlotAttrs("interface", "unknownkey", nil) + c.Assert(err, IsNil) + c.Assert(slot, IsNil) + + slot, err = s.testRepo.UpdateStaticSlotAttrs("interface", "1234", map[string]interface{}{"c": "d"}) + c.Assert(err, IsNil) + c.Assert(slot, NotNil) + c.Assert(slot.Attrs, DeepEquals, map[string]interface{}{"c": "d"}) + c.Assert(coreSlot.Attrs, DeepEquals, map[string]interface{}{"c": "d"}) +} + +func (s *RepositorySuite) TestUpdateStaticSlotAttrsConnectedError(c *C) { + c.Assert(s.testRepo.AddPlug(s.plug), IsNil) + coreSlot := &snap.SlotInfo{ + Snap: s.coreSnap, + Name: "dummy-slot", + Interface: "interface", + HotplugKey: "1234", + } + c.Assert(s.testRepo.AddSlot(coreSlot), IsNil) + + _, err := s.testRepo.Connect(NewConnRef(s.plug, coreSlot), nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + slot, err := s.testRepo.UpdateStaticSlotAttrs("interface", "1234", map[string]interface{}{"c": "d"}) + c.Assert(err, ErrorMatches, `internal error: cannot update slot dummy-slot while connected`) + c.Assert(slot, IsNil) +} From 34edc6de57f6834116daf15d78d30faa99eae7a9 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 17:24:48 +0100 Subject: [PATCH 198/580] dirs: use proper fontconfig cache path for Amazon Linux 2 Signed-off-by: Maciej Borzecki --- dirs/dirs.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dirs/dirs.go b/dirs/dirs.go index 0c4a913238c..d9807a9d3a8 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -286,7 +286,9 @@ func SetRootDir(rootdir string) { SystemFontsDir = filepath.Join(rootdir, "/usr/share/fonts") SystemLocalFontsDir = filepath.Join(rootdir, "/usr/local/share/fonts") SystemFontconfigCacheDir = filepath.Join(rootdir, "/var/cache/fontconfig") - if release.DistroLike("fedora") { + if release.DistroLike("fedora") && !release.DistroLike("amzn") { + // Applies to Fedora and CentOS, Amazon Linux 2 is behind with + // updates to fontconfig and uses /var/cache/fontconfig instead, // see: // https://fedoraproject.org/wiki/Changes/FontconfigCacheDirChange // https://bugzilla.redhat.com/show_bug.cgi?id=1416380 From eef3cc66e8198626f67d58ec83153b27233b8e91 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 8 Jan 2019 17:25:25 +0100 Subject: [PATCH 199/580] tests/main/interfaces-desktop-host-fonts: update to use correct paths Fedora, CentOS and RHEL use /usr/lib/fontconfig/cache. Amazon Linux 2 uses /var/cache/fontconfig like other distros. Signed-off-by: Maciej Borzecki --- .../interfaces-desktop-host-fonts/task.yaml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/main/interfaces-desktop-host-fonts/task.yaml b/tests/main/interfaces-desktop-host-fonts/task.yaml index 1c50ebea952..fd95d8b9eac 100644 --- a/tests/main/interfaces-desktop-host-fonts/task.yaml +++ b/tests/main/interfaces-desktop-host-fonts/task.yaml @@ -7,6 +7,14 @@ details: | systems: [-ubuntu-core-*] +restore: | + rm -f /usr/share/fonts/dist-font.txt + rm -f /usr/local/share/fonts/local-font.txt + rm -f "$HOME"/.fonts/user-font1.txt + rm -f "$HOME"/.local/share/fonts/user-font2.txt + rm -f /var/cache/fontconfig/cache.txt + rm -f /usr/lib/fontconfig/cache/cache.txt + execute: | mkdir -p /usr/share/fonts echo "Distribution font" > /usr/share/fonts/dist-font.txt @@ -14,8 +22,14 @@ execute: | mkdir -p /usr/local/share/fonts echo "Local font" > /usr/local/share/fonts/local-font.txt - mkdir -p /var/cache/fontconfig - echo "Cache file" > /var/cache/fontconfig/cache.txt + cache_dir=/var/cache/fontconfig + case "$SPREAD_SYSTEM" in + fedora-*|centos-*) + cache_dir=/usr/lib/fontconfig/cache + ;; + esac + mkdir -p "$cache_dir" + echo "Cache file" > "$cache_dir"/cache.txt mkdir -p "$HOME"/.fonts echo "User font 1" > "$HOME"/.fonts/user-font1.txt From cf46c152793c980ac50984ea85486446adc4bf0d Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 8 Jan 2019 17:59:55 -0300 Subject: [PATCH 200/580] Fix "No space left on device" issue on amazon-linux --- run-checks | 2 +- tests/main/install-store-laaaarge/task.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/run-checks b/run-checks index 11cc19500ce..42fe2bf7bd2 100755 --- a/run-checks +++ b/run-checks @@ -262,7 +262,7 @@ if [ "$SPREAD" = 1 ]; then export PATH=$TMP_SPREAD:$PATH ( cd "$TMP_SPREAD" && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz ) - spread google: google-upgrade:tests/upgrade/ + spread -repeat 100 google:amazon-linux-2-64:tests/main/install-store-laaaarge # cleanup the debian-ubuntu-14.04 rm -rf debian-ubuntu-14.04 diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index b1d2cb91bc1..2313e438c53 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -13,7 +13,7 @@ restore: | . "$TESTSLIB/systemd.sh" systemd_stop_units snapd.service snapd.socket - umount /tmp || true + umount /tmp systemctl start snapd.{socket,service} execute: | From 5385d052f446f93a26337d763e1b423ee3777c5e Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 8 Jan 2019 21:09:49 -0300 Subject: [PATCH 201/580] Add new code to force error --- run-checks | 1 + spread.yaml | 2 +- tests/main/install-store-laaaarge/task.yaml | 2 ++ tests/regression/lp-1800004/task.yaml | 2 ++ 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/run-checks b/run-checks index 42fe2bf7bd2..85cfdc9ce9c 100755 --- a/run-checks +++ b/run-checks @@ -263,6 +263,7 @@ if [ "$SPREAD" = 1 ]; then ( cd "$TMP_SPREAD" && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz ) spread -repeat 100 google:amazon-linux-2-64:tests/main/install-store-laaaarge + spread -repeat 20 google:amazon-linux-2-64:tests/regression/lp-1800004 google:amazon-linux-2-64:tests/main/install-store-laaaarge google:amazon-linux-2-64:tests/main/xdg-open # cleanup the debian-ubuntu-14.04 rm -rf debian-ubuntu-14.04 diff --git a/spread.yaml b/spread.yaml index c9da6bd6c6a..312ef5c85e0 100644 --- a/spread.yaml +++ b/spread.yaml @@ -91,7 +91,7 @@ backends: workers: 4 - amazon-linux-2-64: - workers: 4 + workers: 1 storage: preserve-size - centos-7-64: diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index 2313e438c53..5674b6130e1 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -1,5 +1,7 @@ summary: snap install a large snap from the store (bigger than tmpfs) +priority: 100 + prepare: | #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" diff --git a/tests/regression/lp-1800004/task.yaml b/tests/regression/lp-1800004/task.yaml index 3c3328efd96..a851bd2b0df 100644 --- a/tests/regression/lp-1800004/task.yaml +++ b/tests/regression/lp-1800004/task.yaml @@ -1,5 +1,7 @@ summary: Check that snap try can be used for snaps in /tmp +priority: 200 + prepare: | cp -a "$TESTSLIB"/snaps/test-snapd-sh /tmp From 84fd30ad10880bf8f70caa584ef92d46bd2d697b Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 8 Jan 2019 22:09:10 -0300 Subject: [PATCH 202/580] Iterate until it is possible to umount /tmp --- run-checks | 1 - tests/main/install-store-laaaarge/task.yaml | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/run-checks b/run-checks index 85cfdc9ce9c..2a1c3f0ba5c 100755 --- a/run-checks +++ b/run-checks @@ -262,7 +262,6 @@ if [ "$SPREAD" = 1 ]; then export PATH=$TMP_SPREAD:$PATH ( cd "$TMP_SPREAD" && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz ) - spread -repeat 100 google:amazon-linux-2-64:tests/main/install-store-laaaarge spread -repeat 20 google:amazon-linux-2-64:tests/regression/lp-1800004 google:amazon-linux-2-64:tests/main/install-store-laaaarge google:amazon-linux-2-64:tests/main/xdg-open # cleanup the debian-ubuntu-14.04 diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index 5674b6130e1..4ba317bd368 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -15,7 +15,17 @@ restore: | . "$TESTSLIB/systemd.sh" systemd_stop_units snapd.service snapd.socket - umount /tmp + # Iterate trying to umount /tmp which could be busy + for i in $(seq 10); do + if [ $i -eq 10 ]; then + echo "Failed to umount /tmp" + exit 1 + fi + if umount /tmp; then + break + fi + sleep 1 + done systemctl start snapd.{socket,service} execute: | From 0515e565c8332b9e422244ba3a4668daf6f7bbc3 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Tue, 8 Jan 2019 22:56:52 -0300 Subject: [PATCH 203/580] Undo debug changes used to reproduce the error --- run-checks | 2 +- spread.yaml | 2 +- tests/main/install-store-laaaarge/task.yaml | 2 -- tests/regression/lp-1800004/task.yaml | 2 -- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/run-checks b/run-checks index 2a1c3f0ba5c..11cc19500ce 100755 --- a/run-checks +++ b/run-checks @@ -262,7 +262,7 @@ if [ "$SPREAD" = 1 ]; then export PATH=$TMP_SPREAD:$PATH ( cd "$TMP_SPREAD" && curl -s -O https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz && tar xzvf spread-amd64.tar.gz ) - spread -repeat 20 google:amazon-linux-2-64:tests/regression/lp-1800004 google:amazon-linux-2-64:tests/main/install-store-laaaarge google:amazon-linux-2-64:tests/main/xdg-open + spread google: google-upgrade:tests/upgrade/ # cleanup the debian-ubuntu-14.04 rm -rf debian-ubuntu-14.04 diff --git a/spread.yaml b/spread.yaml index 312ef5c85e0..c9da6bd6c6a 100644 --- a/spread.yaml +++ b/spread.yaml @@ -91,7 +91,7 @@ backends: workers: 4 - amazon-linux-2-64: - workers: 1 + workers: 4 storage: preserve-size - centos-7-64: diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index 4ba317bd368..c7b3b375544 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -1,7 +1,5 @@ summary: snap install a large snap from the store (bigger than tmpfs) -priority: 100 - prepare: | #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" diff --git a/tests/regression/lp-1800004/task.yaml b/tests/regression/lp-1800004/task.yaml index a851bd2b0df..3c3328efd96 100644 --- a/tests/regression/lp-1800004/task.yaml +++ b/tests/regression/lp-1800004/task.yaml @@ -1,7 +1,5 @@ summary: Check that snap try can be used for snaps in /tmp -priority: 200 - prepare: | cp -a "$TESTSLIB"/snaps/test-snapd-sh /tmp From 0d18585f1f3344a8f2861516393b3d541a562b32 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 9 Jan 2019 09:51:21 +0100 Subject: [PATCH 204/580] snap-confine: fix incorrect use "src" var in mount-support.c We got a bugreport from fedora that gcc-9 fails to build with current snapd: ``` snap-confine/mount-support.c: In function 'sc_bootstrap_mount_namespace': snap-confine/mount-support.c:403:4: error: '%s' directive argument is null [-Werror=format-overflow=] 403 | die("cannot use result from readlink: %s", src); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:2507: snap-confine/snap_confine_debug-mount-support.o] Error 1 make[1]: *** Waiting for unfinished jobs.... ``` This PR fixes this issue. --- cmd/snap-confine/mount-support.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 93538732785..f299d9c5199 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -343,7 +343,7 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) } // this cannot happen except when the kernel is buggy if (strstr(self, "/snap-confine") == NULL) { - die("cannot use result from readlink: %s", src); + die("cannot use result from readlink: %s", self); } src = dirname(self); // dirname(path) might return '.' depending on path. From 108a2728522614122fcc4a77a404b886878d6067 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 9 Jan 2019 12:18:13 +0100 Subject: [PATCH 205/580] interfaces: leave a comment on the fontconfig setup Signed-off-by: Maciej Borzecki --- interfaces/builtin/desktop.go | 13 +++++-------- interfaces/builtin/desktop_test.go | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/interfaces/builtin/desktop.go b/interfaces/builtin/desktop.go index e9e80ec9cd4..fdbace7a4a3 100644 --- a/interfaces/builtin/desktop.go +++ b/interfaces/builtin/desktop.go @@ -281,16 +281,13 @@ func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plu if !osutil.IsDirectory(dir) { continue } - target := dirs.StripRootDir(dir) - if dir == dirs.SystemFontconfigCacheDir { - // fontconfig cache directory path is a little special - // and varies across systems, but we always mount it at - // the same location - target = "/var/cache/fontconfig" - } + // Since /etc/fonts/fonts.conf in the snap mount ns is the same + // as on the host, we need to preserve the original directory + // paths for the fontconfig runtime to poke the correct + // locations spec.AddMountEntry(osutil.MountEntry{ Name: "/var/lib/snapd/hostfs" + dir, - Dir: target, + Dir: dirs.StripRootDir(dir), Options: []string{"bind", "ro"}, }) } diff --git a/interfaces/builtin/desktop_test.go b/interfaces/builtin/desktop_test.go index c87d5fd1aa1..82623d539a9 100644 --- a/interfaces/builtin/desktop_test.go +++ b/interfaces/builtin/desktop_test.go @@ -202,7 +202,7 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { c.Assert(entries, HasLen, 3) c.Check(entries[2].Name, Equals, hostfs+dirs.SystemFontconfigCacheDir) - c.Check(entries[2].Dir, Equals, "/var/cache/fontconfig") + c.Check(entries[2].Dir, Equals, "/usr/lib/fontconfig/cache") c.Check(entries[2].Options, DeepEquals, []string{"bind", "ro"}) } From 7115c062d88b4eed9760c4137c8715ef617780c8 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 9 Jan 2019 12:19:26 +0100 Subject: [PATCH 206/580] dics: comment oon which fontconfig paths differ and where Signed-off-by: Maciej Borzecki --- dirs/dirs.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dirs/dirs.go b/dirs/dirs.go index d9807a9d3a8..aec99a41b65 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -283,8 +283,10 @@ func SetRootDir(rootdir string) { CompletionHelper = filepath.Join(CoreLibExecDir, "etelpmoc.sh") CompletersDir = filepath.Join(rootdir, "/usr/share/bash-completion/completions/") + // These paths agree across all supported distros SystemFontsDir = filepath.Join(rootdir, "/usr/share/fonts") SystemLocalFontsDir = filepath.Join(rootdir, "/usr/local/share/fonts") + // The cache path is true for Ubuntu, Debian, openSUSE, Arch SystemFontconfigCacheDir = filepath.Join(rootdir, "/var/cache/fontconfig") if release.DistroLike("fedora") && !release.DistroLike("amzn") { // Applies to Fedora and CentOS, Amazon Linux 2 is behind with From 842c45891da5145d78e536c584093d92b1102418 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 11:12:47 +0000 Subject: [PATCH 207/580] store, snap, cmd/snap: channels have released-at The store is now sending the date a revision was released into a channel as part of the channel map. This change makes snapd know about it, and uses it for `snap info`. Fixes [lp:1758288]. [lp:1758288]: https://bugs.launchpad.net/snapd/+bug/1758288 --- cmd/snap/cmd_info.go | 23 +++++++++++------ cmd/snap/cmd_info_test.go | 46 +++++++++++++++++++++++++-------- snap/info.go | 1 + store/details_v2.go | 25 ++++++++++++++++++ store/store_test.go | 48 +++++++++++++++++++++++------------ tests/main/snap-info/check.py | 11 +++++--- 6 files changed, 116 insertions(+), 38 deletions(-) diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go index c950cd13ca8..795062ec10f 100644 --- a/cmd/snap/cmd_info.go +++ b/cmd/snap/cmd_info.go @@ -25,6 +25,7 @@ import ( "path/filepath" "strings" "text/tabwriter" + "time" "unicode" "unicode/utf8" @@ -321,8 +322,13 @@ func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, var channelRisks = []string{"stable", "candidate", "beta", "edge"} // displayChannels displays channels and tracks in the right order -func displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap) { - fmt.Fprintf(w, "channels:"+strings.Repeat("\t", strings.Count(chantpl, "\t"))+"\n") +func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap) { + fmt.Fprintln(w, "channels:") + + releasedfmt := "2006-01-02" + if x.AbsTime { + releasedfmt = time.RFC3339 + } // order by tracks for _, tr := range remote.Tracks { @@ -333,10 +339,11 @@ func displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.S if tr == "latest" { chName = risk } - var version, revision, size, notes string + var version, released, revision, size, notes string if ok { version = ch.Version revision = fmt.Sprintf("(%s)", ch.Revision) + released = ch.ReleasedAt.Format(releasedfmt) size = strutil.SizeToStr(ch.Size) notes = NotesFromChannelSnapInfo(ch).String() trackHasOpenChannel = true @@ -347,7 +354,7 @@ func displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.S version = esc.dash } } - fmt.Fprintf(w, " "+chantpl, chName, version, revision, size, notes) + fmt.Fprintf(w, " "+chantpl, chName, version, released, revision, size, notes) } } } @@ -457,17 +464,17 @@ func (x *infoCmd) Execute([]string) error { } } - chantpl := "%s:\t%s %s %s %s\n" + chantpl := "%s:\t%s %s%s %s %s\n" if remote != nil && remote.Channels != nil && remote.Tracks != nil { - chantpl = "%s:\t%s\t%s\t%s\t%s\n" + chantpl = "%s:\t%s\t%s\t%s\t%s\t%s\n" w.Flush() - displayChannels(w, chantpl, esc, remote) + x.displayChannels(w, chantpl, esc, remote) } if local != nil { revstr := fmt.Sprintf("(%s)", local.Revision) fmt.Fprintf(w, chantpl, - "installed", local.Version, revstr, strutil.SizeToStr(local.InstalledSize), notes) + "installed", local.Version, "", revstr, strutil.SizeToStr(local.InstalledSize), notes) } } diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go index fd38c896896..65db29f70ab 100644 --- a/cmd/snap/cmd_info_test.go +++ b/cmd/snap/cmd_info_test.go @@ -203,7 +203,8 @@ const mockInfoJSONWithChannels = ` "revision": "1", "version": "2.10", "channel": "1/stable", - "size": 65536 + "size": 65536, + "released-at": "2018-12-18T15:16:56.723501Z" } }, "tracks": ["1"] @@ -390,16 +391,16 @@ func (s *infoSuite) TestInfoWithChannelsAndLocal(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { - case 0: + case 0, 2: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") fmt.Fprintln(w, mockInfoJSONWithChannels) - case 1: + case 1, 3: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") fmt.Fprintln(w, mockInfoJSONNoLicense) default: - c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + c.Fatalf("expected to get 4 requests, now on %d (%v)", n+1, r) } n++ @@ -417,14 +418,39 @@ description: | snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02T22:04:07Z -channels: - 1/stable: 2.10 (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ -installed: 2.10 (1) 1kB disabled +channels: + 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (1) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + + // now the same but without abs-time + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap +publisher: Canonical✓ +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (1) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 4) } func (s *infoSuite) TestInfoHumanTimes(c *check.C) { diff --git a/snap/info.go b/snap/info.go index e52aa497406..f3205d547b8 100644 --- a/snap/info.go +++ b/snap/info.go @@ -319,6 +319,7 @@ type ChannelSnapInfo struct { Channel string `json:"channel"` Epoch Epoch `json:"epoch"` Size int64 `json:"size"` + ReleasedAt time.Time `json:"released-at"` } // InstanceName returns the blessed name of the snap decorated with instance diff --git a/store/details_v2.go b/store/details_v2.go index e36b030dcc1..b92c6f5cfdc 100644 --- a/store/details_v2.go +++ b/store/details_v2.go @@ -20,8 +20,10 @@ package store import ( + "encoding/json" "fmt" "strconv" + "time" "github.com/snapcore/snapd/jsonutil/safejson" "github.com/snapcore/snapd/snap" @@ -88,6 +90,28 @@ type storeInfoChannel struct { Name string `json:"name"` Risk string `json:"risk"` Track string `json:"track"` + ReleasedAt xTime `json:"released-at"` +} + +// time.Time but also try without a timezone -- this is a workaround +// for a store-side issue that already has a fix, that should get +// deployed today (2019-01-09) +type xTime time.Time + +func (tp *xTime) UnmarshalJSON(buf []byte) error { + var str string + if err := json.Unmarshal(buf, &str); err != nil { + return err + } + t, err := time.Parse(time.RFC3339, str) + if err != nil { + t, err = time.Parse("2006-01-02T15:04:05", str) + if err != nil { + return err + } + } + *tp = xTime(t) + return nil } // storeInfoChannelSnap is the snap-in-a-channel of which the channel map is made @@ -136,6 +160,7 @@ func infoFromStoreInfo(si *storeInfo) (*snap.Info, error) { Channel: ch.Name, Epoch: s.Epoch, Size: s.Download.Size, + ReleasedAt: time.Time(ch.ReleasedAt).UTC(), } if !seen[ch.Track] { seen[ch.Track] = true diff --git a/store/store_test.go b/store/store_test.go index f96a079da32..b1a79ee6490 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -1586,6 +1586,8 @@ slots: read: - / +- add "released-at" to something randomish + */ const mockInfoJSON = `{ "channel-map": [ @@ -1597,6 +1599,7 @@ const mockInfoJSON = `{ "channel": { "architecture": "amd64", "name": "stable", + "released-at": "2019-01-01T10:11:12.123456789+00:00", "risk": "stable", "track": "latest" }, @@ -1630,6 +1633,7 @@ const mockInfoJSON = `{ "channel": { "architecture": "amd64", "name": "candidate", + "released-at": "2019-01-02T10:11:12.123456789+00:00", "risk": "candidate", "track": "latest" }, @@ -1663,6 +1667,7 @@ const mockInfoJSON = `{ "channel": { "architecture": "amd64", "name": "beta", + "released-at": "2019-01-03T10:11:12.123456789+00:00", "risk": "beta", "track": "latest" }, @@ -1696,6 +1701,7 @@ const mockInfoJSON = `{ "channel": { "architecture": "amd64", "name": "edge", + "released-at": "2019-01-04T10:11:12.123456789+00:00", "risk": "edge", "track": "latest" }, @@ -2046,6 +2052,7 @@ func (s *storeTestSuite) TestInfoAndChannels(c *C) { Channel: "stable", Size: 20480, Epoch: snap.E("0"), + ReleasedAt: time.Date(2019, 1, 1, 10, 11, 12, 123456789, time.UTC), }, "latest/candidate": { Revision: snap.R(27), @@ -2054,6 +2061,7 @@ func (s *storeTestSuite) TestInfoAndChannels(c *C) { Channel: "candidate", Size: 20480, Epoch: snap.E("0"), + ReleasedAt: time.Date(2019, 1, 2, 10, 11, 12, 123456789, time.UTC), }, "latest/beta": { Revision: snap.R(27), @@ -2062,6 +2070,7 @@ func (s *storeTestSuite) TestInfoAndChannels(c *C) { Channel: "beta", Size: 20480, Epoch: snap.E("0"), + ReleasedAt: time.Date(2019, 1, 3, 10, 11, 12, 123456789, time.UTC), }, "latest/edge": { Revision: snap.R(28), @@ -2070,6 +2079,7 @@ func (s *storeTestSuite) TestInfoAndChannels(c *C) { Channel: "edge", Size: 20480, Epoch: snap.E("0"), + ReleasedAt: time.Date(2019, 1, 4, 10, 11, 12, 123456789, time.UTC), }, } for k, v := range result.Channels { @@ -2084,16 +2094,19 @@ func (s *storeTestSuite) TestInfoMoreChannels(c *C) { // NB this tests more channels, but still only one architecture mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assertRequest(c, r, "GET", infoPathPattern) - // following is just a tweaked version of: + // following is just an aligned version of: // http https://api.snapcraft.io/v2/snaps/info/go architecture==amd64 fields==channel Snap-Device-Series:16 | jq -c '.["channel-map"] | .[]' io.WriteString(w, `{"channel-map": [ -{"channel":{"name":"stable", "risk":"stable", "track":"latest"}}, -{"channel":{"name":"edge", "risk":"edge", "track":"latest"}}, -{"channel":{"name":"1.10/stable", "risk":"stable", "track":"1.10" }}, -{"channel":{"name":"1.6/stable", "risk":"stable", "track":"1.6" }}, -{"channel":{"name":"1.7/stable", "risk":"stable", "track":"1.7" }}, -{"channel":{"name":"1.8/stable", "risk":"stable", "track":"1.8" }}, -{"channel":{"name":"1.9/stable", "risk":"stable", "track":"1.9" }} +{"channel":{"architecture":"amd64","name":"stable", "released-at":"2018-12-17T09:17:16.288554+00:00","risk":"stable", "track":"latest"}}, +{"channel":{"architecture":"amd64","name":"edge", "released-at":"2018-11-06T00:46:03.348730+00:00","risk":"edge", "track":"latest"}}, +{"channel":{"architecture":"amd64","name":"1.11/stable", "released-at":"2018-12-17T09:17:48.847205+00:00","risk":"stable", "track":"1.11"}}, +{"channel":{"architecture":"amd64","name":"1.11/candidate","released-at":"2018-12-17T00:10:05.864910+00:00","risk":"candidate","track":"1.11"}}, +{"channel":{"architecture":"amd64","name":"1.10/stable", "released-at":"2018-12-17T06:53:57.915517+00:00","risk":"stable", "track":"1.10"}}, +{"channel":{"architecture":"amd64","name":"1.10/candidate","released-at":"2018-12-17T00:04:13.413244+00:00","risk":"candidate","track":"1.10"}}, +{"channel":{"architecture":"amd64","name":"1.9/stable", "released-at":"2018-06-13T02:23:06.338145+00:00","risk":"stable", "track":"1.9"}}, +{"channel":{"architecture":"amd64","name":"1.8/stable", "released-at":"2018-02-07T23:08:59.152984+00:00","risk":"stable", "track":"1.8"}}, +{"channel":{"architecture":"amd64","name":"1.7/stable", "released-at":"2017-06-02T01:16:52.640258+00:00","risk":"stable", "track":"1.7"}}, +{"channel":{"architecture":"amd64","name":"1.6/stable", "released-at":"2017-05-17T21:18:42.224979+00:00","risk":"stable", "track":"1.6"}} ]}`) })) @@ -2111,19 +2124,22 @@ func (s *storeTestSuite) TestInfoMoreChannels(c *C) { result, err := sto.SnapInfo(store.SnapSpec{Name: "eh"}, nil) c.Assert(err, IsNil) expected := map[string]*snap.ChannelSnapInfo{ - "latest/stable": {Channel: "stable"}, - "latest/edge": {Channel: "edge"}, - "1.6/stable": {Channel: "1.6/stable"}, - "1.7/stable": {Channel: "1.7/stable"}, - "1.8/stable": {Channel: "1.8/stable"}, - "1.9/stable": {Channel: "1.9/stable"}, - "1.10/stable": {Channel: "1.10/stable"}, + "latest/stable": {Channel: "stable", ReleasedAt: time.Date(2018, 12, 17, 9, 17, 16, 288554000, time.UTC)}, + "latest/edge": {Channel: "edge", ReleasedAt: time.Date(2018, 11, 6, 0, 46, 3, 348730000, time.UTC)}, + "1.6/stable": {Channel: "1.6/stable", ReleasedAt: time.Date(2017, 5, 17, 21, 18, 42, 224979000, time.UTC)}, + "1.7/stable": {Channel: "1.7/stable", ReleasedAt: time.Date(2017, 6, 2, 1, 16, 52, 640258000, time.UTC)}, + "1.8/stable": {Channel: "1.8/stable", ReleasedAt: time.Date(2018, 2, 7, 23, 8, 59, 152984000, time.UTC)}, + "1.9/stable": {Channel: "1.9/stable", ReleasedAt: time.Date(2018, 6, 13, 2, 23, 6, 338145000, time.UTC)}, + "1.10/stable": {Channel: "1.10/stable", ReleasedAt: time.Date(2018, 12, 17, 6, 53, 57, 915517000, time.UTC)}, + "1.10/candidate": {Channel: "1.10/candidate", ReleasedAt: time.Date(2018, 12, 17, 0, 4, 13, 413244000, time.UTC)}, + "1.11/stable": {Channel: "1.11/stable", ReleasedAt: time.Date(2018, 12, 17, 9, 17, 48, 847205000, time.UTC)}, + "1.11/candidate": {Channel: "1.11/candidate", ReleasedAt: time.Date(2018, 12, 17, 0, 10, 5, 864910000, time.UTC)}, } for k, v := range result.Channels { c.Check(v, DeepEquals, expected[k], Commentf("%q", k)) } c.Check(result.Channels, HasLen, len(expected)) - c.Check(result.Tracks, DeepEquals, []string{"latest", "1.10", "1.6", "1.7", "1.8", "1.9"}) + c.Check(result.Tracks, DeepEquals, []string{"latest", "1.11", "1.10", "1.9", "1.8", "1.7", "1.6"}) } func (s *storeTestSuite) TestInfoNonDefaults(c *C) { diff --git a/tests/main/snap-info/check.py b/tests/main/snap-info/check.py index 9f889b22e80..5ca95f95ff4 100644 --- a/tests/main/snap-info/check.py +++ b/tests/main/snap-info/check.py @@ -39,6 +39,9 @@ def maybe(name, d): def verRevNotesRx(s): return re.compile(r"^\w\S*\s+\(\d+\)\s+[1-9][0-9]*\w+\s+" + s + "$") +def verRelRevNotesRx(s): + return re.compile(r"^\w\S*\s+\d{4}-\d{2}-\d{2}\s+\(\d+\)\s+[1-9][0-9]*\w+\s+" + s + "$") + if os.environ['SNAPPY_USE_STAGING_STORE'] == '1': snap_ids={ "test-snapd-tools": "02AHdOomTzby7gTaiLX3M3SGMmXDfLJp", @@ -81,10 +84,10 @@ def verRevNotesRx(s): ("installed", matches, verRevNotesRx("-")), ("refresh-date", exists), ("channels", check, - ("stable", matches, verRevNotesRx("-")), + ("stable", matches, verRelRevNotesRx("-")), ("candidate", equals, "↑"), ("beta", equals, "↑"), - ("edge", matches, verRevNotesRx("-")), + ("edge", matches, verRelRevNotesRx("-")), ), ("snap-id", equals, snap_ids["test-snapd-tools"]), ("license", matches, r"(unknown|unset)"), # TODO: update once snap.yaml contains the right license @@ -102,8 +105,8 @@ def verRevNotesRx(s): ("channels", check, ("stable", equals, "–"), ("candidate", equals, "–"), - ("beta", matches, verRevNotesRx("devmode")), - ("edge", matches, verRevNotesRx("devmode")), + ("beta", matches, verRelRevNotesRx("devmode")), + ("edge", matches, verRelRevNotesRx("devmode")), ), ("snap-id", equals, snap_ids["test-snapd-devmode"]), ("license", matches, r"(unknown|unset)"), # TODO: update once snap.yaml contains the right license From 2ad2087f3b754603da35a930619966fb76c976ae Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Wed, 9 Jan 2019 12:19:57 +0100 Subject: [PATCH 208/580] tests/main/interfaces-desktop-host-fonts: use fontconfig from distro Do not create the system paths manually otherwise the test will not check whether snapd is compatible with the way the distro is set up. Signed-off-by: Maciej Borzecki --- tests/main/interfaces-desktop-host-fonts/task.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/main/interfaces-desktop-host-fonts/task.yaml b/tests/main/interfaces-desktop-host-fonts/task.yaml index fd95d8b9eac..edb0303131d 100644 --- a/tests/main/interfaces-desktop-host-fonts/task.yaml +++ b/tests/main/interfaces-desktop-host-fonts/task.yaml @@ -16,9 +16,13 @@ restore: | rm -f /usr/lib/fontconfig/cache/cache.txt execute: | - mkdir -p /usr/share/fonts + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB"/pkgdb.sh + distro_install_package fontconfig + echo "Distribution font" > /usr/share/fonts/dist-font.txt + # this may not exist across all distributions mkdir -p /usr/local/share/fonts echo "Local font" > /usr/local/share/fonts/local-font.txt @@ -28,7 +32,6 @@ execute: | cache_dir=/usr/lib/fontconfig/cache ;; esac - mkdir -p "$cache_dir" echo "Cache file" > "$cache_dir"/cache.txt mkdir -p "$HOME"/.fonts @@ -49,8 +52,8 @@ execute: | echo "Checking access to host /usr/local/share/fonts" test-snapd-desktop.check-files /usr/local/share/fonts/local-font.txt | MATCH "Local font" - echo "Checking access to host /var/cache/fontconfig" - test-snapd-desktop.check-files /var/cache/fontconfig/cache.txt | MATCH "Cache file" + echo "Checking access to host $cache_dir" + test-snapd-desktop.check-files "$cache_dir"/cache.txt | MATCH "Cache file" echo "Checking access to host ~/.fonts" test-snapd-desktop.check-files "$HOME"/.fonts/user-font1.txt | MATCH "User font 1" From 89c819e05725612fe86dd5b982e918d020d9ef5d Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 12:28:08 +0100 Subject: [PATCH 209/580] cmd/snap-update-ns: move debug functions to dedicated file Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/debug.go | 47 +++++++++++++++++++++++++++++++++++++ cmd/snap-update-ns/main.go | 22 ----------------- 2 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 cmd/snap-update-ns/debug.go diff --git a/cmd/snap-update-ns/debug.go b/cmd/snap-update-ns/debug.go new file mode 100644 index 00000000000..84e5c4020f2 --- /dev/null +++ b/cmd/snap-update-ns/debug.go @@ -0,0 +1,47 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 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 . + * + */ + +package main + +import ( + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" +) + +func debugShowProfile(profile *osutil.MountProfile, header string) { + if len(profile.Entries) > 0 { + logger.Debugf("%s:", header) + for _, entry := range profile.Entries { + logger.Debugf("\t%s", entry) + } + } else { + logger.Debugf("%s: (none)", header) + } +} + +func debugShowChanges(changes []*Change, header string) { + if len(changes) > 0 { + logger.Debugf("%s:", header) + for _, change := range changes { + logger.Debugf("\t%s", change) + } + } else { + logger.Debugf("%s: (none)", header) + } +} diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index c7baa7c84f4..38157688c96 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -241,28 +241,6 @@ func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, return ¤tAfter, nil } -func debugShowProfile(profile *osutil.MountProfile, header string) { - if len(profile.Entries) > 0 { - logger.Debugf("%s:", header) - for _, entry := range profile.Entries { - logger.Debugf("\t%s", entry) - } - } else { - logger.Debugf("%s: (none)", header) - } -} - -func debugShowChanges(changes []*Change, header string) { - if len(changes) > 0 { - logger.Debugf("%s:", header) - for _, change := range changes { - logger.Debugf("\t%s", change) - } - } else { - logger.Debugf("%s: (none)", header) - } -} - func applyUserFstab(snapName string) error { desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) desired, err := osutil.LoadMountProfile(desiredProfilePath) From e0abace08a57916b810e0b1bffeff5f810b7b02b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 12:30:24 +0100 Subject: [PATCH 210/580] cmd/snap-update-ns: rename applyFstab to applySystemFstab There's a set of helpers that are shared and a set of entry points. While helpers can remain unqualified the public entry points, for readability, be qualified with either "user" or "system". Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 38157688c96..6f87462b05a 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -86,10 +86,10 @@ func run() error { if opts.UserMounts { return applyUserFstab(opts.Positionals.SnapName) } - return applyFstab(opts.Positionals.SnapName, opts.FromSnapConfine) + return applySystemFstab(opts.Positionals.SnapName, opts.FromSnapConfine) } -func applyFstab(instanceName string, fromSnapConfine bool) error { +func applySystemFstab(instanceName string, fromSnapConfine bool) error { // Lock the mount namespace so that any concurrently attempted invocations // of snap-confine are synchronized and will see consistent state. lock, err := mount.OpenLock(instanceName) From cb589d806942fc1207108760907e43553c9d20ce Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 12:31:38 +0100 Subject: [PATCH 211/580] cmd/snap-update-ns: move applyUserFstab closer to applySystemFstab Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 6f87462b05a..d6883d4d6a6 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -160,6 +160,33 @@ func applySystemFstab(instanceName string, fromSnapConfine bool) error { return computeAndSaveChanges(instanceName, as) } +func applyUserFstab(snapName string) error { + desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := osutil.LoadMountProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) + } + + // Replace XDG_RUNTIME_DIR in mount profile + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + for i := range desired.Entries { + if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + } + + debugShowProfile(desired, "desired mount profile") + + // TODO: configure the secure helper and inform it about directories that + // can be created without trespassing. + as := &Assumptions{} + _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) + return err +} + func computeAndSaveChanges(snapName string, as *Assumptions) error { // Read the desired and current mount profiles. Note that missing files // count as empty profiles so that we can gracefully handle a mount @@ -240,30 +267,3 @@ func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, debugShowProfile(¤tAfter, "current mount profile (after applying changes)") return ¤tAfter, nil } - -func applyUserFstab(snapName string) error { - desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) - desired, err := osutil.LoadMountProfile(desiredProfilePath) - if err != nil { - return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) - } - - // Replace XDG_RUNTIME_DIR in mount profile - xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) - for i := range desired.Entries { - if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - } - - debugShowProfile(desired, "desired mount profile") - - // TODO: configure the secure helper and inform it about directories that - // can be created without trespassing. - as := &Assumptions{} - _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) - return err -} From 6c52eace376d2a6585fd32e83056edf0ba87623a Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 12:32:56 +0100 Subject: [PATCH 212/580] cmd/snap-update-ns: move TODO where it belongs Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index d6883d4d6a6..5602d61f28b 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -148,10 +148,6 @@ func applySystemFstab(instanceName string, fromSnapConfine bool) error { // // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow // remapping for parallel installs only when the snap has an instance key - // - // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and - // allow defining layout items that refer to SNAP_USER_DATA and - // SNAP_USER_COMMON. as := &Assumptions{} as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName) if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { @@ -183,6 +179,9 @@ func applyUserFstab(snapName string) error { // TODO: configure the secure helper and inform it about directories that // can be created without trespassing. as := &Assumptions{} + // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and + // allow defining layout items that refer to SNAP_USER_DATA and + // SNAP_USER_COMMON. _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) return err } From 75f0dcb34159376d6ccd537db4e1c54b211cde8b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 14:39:00 +0100 Subject: [PATCH 213/580] cmd/snap-update-ns: rename computeAndSaveChanges to computeAndSaveSystemChanges Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/export_test.go | 4 ++-- cmd/snap-update-ns/main.go | 4 ++-- cmd/snap-update-ns/main_test.go | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go index 74865fc68eb..f7a5deb974e 100644 --- a/cmd/snap-update-ns/export_test.go +++ b/cmd/snap-update-ns/export_test.go @@ -40,8 +40,8 @@ var ( ExecWritableMimic = execWritableMimic // main - ComputeAndSaveChanges = computeAndSaveChanges - ApplyUserFstab = applyUserFstab + ComputeAndSaveSystemChanges = computeAndSaveSystemChanges + ApplyUserFstab = applyUserFstab // bootstrap ClearBootstrapError = clearBootstrapError diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 5602d61f28b..e784c9da602 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -153,7 +153,7 @@ func applySystemFstab(instanceName string, fromSnapConfine bool) error { if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { as.AddUnrestrictedPaths("/snap/" + snapName) } - return computeAndSaveChanges(instanceName, as) + return computeAndSaveSystemChanges(instanceName, as) } func applyUserFstab(snapName string) error { @@ -186,7 +186,7 @@ func applyUserFstab(snapName string) error { return err } -func computeAndSaveChanges(snapName string, as *Assumptions) error { +func computeAndSaveSystemChanges(snapName string, as *Assumptions) error { // Read the desired and current mount profiles. Note that missing files // count as empty profiles so that we can gracefully handle a mount // interface connection/disconnection. diff --git a/cmd/snap-update-ns/main_test.go b/cmd/snap-update-ns/main_test.go index 82628a3cd4e..daf976ded0f 100644 --- a/cmd/snap-update-ns/main_test.go +++ b/cmd/snap-update-ns/main_test.go @@ -54,7 +54,7 @@ func (s *mainSuite) SetUpTest(c *C) { s.log = buf } -func (s *mainSuite) TestComputeAndSaveChanges(c *C) { +func (s *mainSuite) TestComputeAndSaveSystemChanges(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") @@ -79,7 +79,7 @@ func (s *mainSuite) TestComputeAndSaveChanges(c *C) { err = ioutil.WriteFile(currentProfilePath, nil, 0644) c.Assert(err, IsNil) - err = update.ComputeAndSaveChanges(snapName, s.as) + err = update.ComputeAndSaveSystemChanges(snapName, s.as) c.Assert(err, IsNil) c.Check(currentProfilePath, testutil.FileEquals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0 @@ -146,7 +146,7 @@ func (s *mainSuite) TestAddingSyntheticChanges(c *C) { }) defer restore() - c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + c.Assert(update.ComputeAndSaveSystemChanges(snapName, s.as), IsNil) c.Check(currentProfilePath, testutil.FileEquals, `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 @@ -223,7 +223,7 @@ func (s *mainSuite) TestRemovingSyntheticChanges(c *C) { }) defer restore() - c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + c.Assert(update.ComputeAndSaveSystemChanges(snapName, s.as), IsNil) c.Check(currentProfilePath, testutil.FileEquals, "") } @@ -265,7 +265,7 @@ func (s *mainSuite) TestApplyingLayoutChanges(c *C) { defer restore() // The error was not ignored, we bailed out. - c.Assert(update.ComputeAndSaveChanges(snapName, s.as), ErrorMatches, "testing") + c.Assert(update.ComputeAndSaveSystemChanges(snapName, s.as), ErrorMatches, "testing") c.Check(currentProfilePath, testutil.FileEquals, "") } @@ -307,7 +307,7 @@ func (s *mainSuite) TestApplyingParallelInstanceChanges(c *C) { defer restore() // The error was not ignored, we bailed out. - c.Assert(update.ComputeAndSaveChanges(snapName, nil), ErrorMatches, "testing") + c.Assert(update.ComputeAndSaveSystemChanges(snapName, nil), ErrorMatches, "testing") c.Check(currentProfilePath, testutil.FileEquals, "") } @@ -350,7 +350,7 @@ func (s *mainSuite) TestApplyIgnoredMissingMount(c *C) { defer restore() // The error was ignored, and no mount was recorded in the profile - c.Assert(update.ComputeAndSaveChanges(snapName, s.as), IsNil) + c.Assert(update.ComputeAndSaveSystemChanges(snapName, s.as), IsNil) c.Check(s.log.String(), Equals, "") c.Check(currentProfilePath, testutil.FileEquals, "") } From eb435e8cef361eb201ead5545d5cc8af2134c685 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 14:42:55 +0100 Subject: [PATCH 214/580] cmd/snap-update-ns: move system profile code to system.go This will allow easier comparison with upcoming user.go Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 103 --------------------------- cmd/snap-update-ns/system.go | 131 +++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 103 deletions(-) create mode 100644 cmd/snap-update-ns/system.go diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index e784c9da602..d5d33be76b5 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -27,10 +27,8 @@ import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/snap" ) var opts struct { @@ -89,73 +87,6 @@ func run() error { return applySystemFstab(opts.Positionals.SnapName, opts.FromSnapConfine) } -func applySystemFstab(instanceName string, fromSnapConfine bool) error { - // Lock the mount namespace so that any concurrently attempted invocations - // of snap-confine are synchronized and will see consistent state. - lock, err := mount.OpenLock(instanceName) - if err != nil { - return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err) - } - defer func() { - logger.Debugf("unlocking mount namespace of snap %q", instanceName) - lock.Close() - }() - - logger.Debugf("locking mount namespace of snap %q", instanceName) - if fromSnapConfine { - // When --from-snap-confine is passed then we just ensure that the - // namespace is locked. This is used by snap-confine to use - // snap-update-ns to apply mount profiles. - if err := lock.TryLock(); err != osutil.ErrAlreadyLocked { - return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName) - } - } else { - if err := lock.Lock(); err != nil { - return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err) - } - } - - // Freeze the mount namespace and unfreeze it later. This lets us perform - // modifications without snap processes attempting to construct - // symlinks or perform other malicious activity (such as attempting to - // introduce a symlink that would cause us to mount something other - // than what we expected). - logger.Debugf("freezing processes of snap %q", instanceName) - if err := freezeSnapProcesses(instanceName); err != nil { - return err - } - defer func() { - logger.Debugf("thawing processes of snap %q", instanceName) - thawSnapProcesses(instanceName) - }() - - // Allow creating directories related to this snap name. - // - // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because - // content interface connections can readily create missing mount points on - // both sides of the interface connection. - // - // We scope /snap/$SNAP_NAME because only one side of the connection can be - // created, as snaps are read-only, the mimic construction will kick-in and - // create the missing directory but this directory is only visible from the - // snap that we are operating on (either plug or slot side, the point is, - // the mount point is not universally visible). - // - // /snap/$SNAP_NAME needs to be there as the code that creates such mount - // points must traverse writable host filesystem that contains /snap/*/ and - // normally such access is off-limits. This approach allows /snap/foo - // without allowing /snap/bin, for example. - // - // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow - // remapping for parallel installs only when the snap has an instance key - as := &Assumptions{} - as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName) - if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { - as.AddUnrestrictedPaths("/snap/" + snapName) - } - return computeAndSaveSystemChanges(instanceName, as) -} - func applyUserFstab(snapName string) error { desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) desired, err := osutil.LoadMountProfile(desiredProfilePath) @@ -186,40 +117,6 @@ func applyUserFstab(snapName string) error { return err } -func computeAndSaveSystemChanges(snapName string, as *Assumptions) error { - // Read the desired and current mount profiles. Note that missing files - // count as empty profiles so that we can gracefully handle a mount - // interface connection/disconnection. - desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) - desired, err := osutil.LoadMountProfile(desiredProfilePath) - if err != nil { - return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err) - } - debugShowProfile(desired, "desired mount profile") - - currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) - currentBefore, err := osutil.LoadMountProfile(currentProfilePath) - if err != nil { - return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err) - } - debugShowProfile(currentBefore, "current mount profile (before applying changes)") - // Synthesize mount changes that were applied before for the purpose of the tmpfs detector. - for _, entry := range currentBefore.Entries { - as.AddChange(&Change{Action: Mount, Entry: entry}) - } - - currentAfter, err := applyProfile(snapName, currentBefore, desired, as) - if err != nil { - return err - } - - logger.Debugf("saving current mount profile of snap %q", snapName) - if err := currentAfter.Save(currentProfilePath); err != nil { - return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) - } - return nil -} - func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { // Compute the needed changes and perform each change if // needed, collecting those that we managed to perform or that diff --git a/cmd/snap-update-ns/system.go b/cmd/snap-update-ns/system.go new file mode 100644 index 00000000000..8881c2bfda8 --- /dev/null +++ b/cmd/snap-update-ns/system.go @@ -0,0 +1,131 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 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 . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +func applySystemFstab(instanceName string, fromSnapConfine bool) error { + // Lock the mount namespace so that any concurrently attempted invocations + // of snap-confine are synchronized and will see consistent state. + lock, err := mount.OpenLock(instanceName) + if err != nil { + return fmt.Errorf("cannot open lock file for mount namespace of snap %q: %s", instanceName, err) + } + defer func() { + logger.Debugf("unlocking mount namespace of snap %q", instanceName) + lock.Close() + }() + + logger.Debugf("locking mount namespace of snap %q", instanceName) + if fromSnapConfine { + // When --from-snap-confine is passed then we just ensure that the + // namespace is locked. This is used by snap-confine to use + // snap-update-ns to apply mount profiles. + if err := lock.TryLock(); err != osutil.ErrAlreadyLocked { + return fmt.Errorf("mount namespace of snap %q is not locked but --from-snap-confine was used", instanceName) + } + } else { + if err := lock.Lock(); err != nil { + return fmt.Errorf("cannot lock mount namespace of snap %q: %s", instanceName, err) + } + } + + // Freeze the mount namespace and unfreeze it later. This lets us perform + // modifications without snap processes attempting to construct + // symlinks or perform other malicious activity (such as attempting to + // introduce a symlink that would cause us to mount something other + // than what we expected). + logger.Debugf("freezing processes of snap %q", instanceName) + if err := freezeSnapProcesses(instanceName); err != nil { + return err + } + defer func() { + logger.Debugf("thawing processes of snap %q", instanceName) + thawSnapProcesses(instanceName) + }() + + // Allow creating directories related to this snap name. + // + // Note that we allow /var/snap instead of /var/snap/$SNAP_NAME because + // content interface connections can readily create missing mount points on + // both sides of the interface connection. + // + // We scope /snap/$SNAP_NAME because only one side of the connection can be + // created, as snaps are read-only, the mimic construction will kick-in and + // create the missing directory but this directory is only visible from the + // snap that we are operating on (either plug or slot side, the point is, + // the mount point is not universally visible). + // + // /snap/$SNAP_NAME needs to be there as the code that creates such mount + // points must traverse writable host filesystem that contains /snap/*/ and + // normally such access is off-limits. This approach allows /snap/foo + // without allowing /snap/bin, for example. + // + // /snap/$SNAP_INSTANCE_NAME and /snap/$SNAP_NAME are added to allow + // remapping for parallel installs only when the snap has an instance key + as := &Assumptions{} + as.AddUnrestrictedPaths("/tmp", "/var/snap", "/snap/"+instanceName) + if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { + as.AddUnrestrictedPaths("/snap/" + snapName) + } + return computeAndSaveSystemChanges(instanceName, as) +} + +func computeAndSaveSystemChanges(snapName string, as *Assumptions) error { + // Read the desired and current mount profiles. Note that missing files + // count as empty profiles so that we can gracefully handle a mount + // interface connection/disconnection. + desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := osutil.LoadMountProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(desired, "desired mount profile") + + currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) + currentBefore, err := osutil.LoadMountProfile(currentProfilePath) + if err != nil { + return fmt.Errorf("cannot load current mount profile of snap %q: %s", snapName, err) + } + debugShowProfile(currentBefore, "current mount profile (before applying changes)") + // Synthesize mount changes that were applied before for the purpose of the tmpfs detector. + for _, entry := range currentBefore.Entries { + as.AddChange(&Change{Action: Mount, Entry: entry}) + } + + currentAfter, err := applyProfile(snapName, currentBefore, desired, as) + if err != nil { + return err + } + + logger.Debugf("saving current mount profile of snap %q", snapName) + if err := currentAfter.Save(currentProfilePath); err != nil { + return fmt.Errorf("cannot save current mount profile of snap %q: %s", snapName, err) + } + return nil +} From 1c406225ad20e177dff8568d01db5955f21455b2 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 14:45:13 +0100 Subject: [PATCH 215/580] cmd/snap-update-ns: move user profile code to user.go Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 32 --------------------- cmd/snap-update-ns/user.go | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 32 deletions(-) create mode 100644 cmd/snap-update-ns/user.go diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index d5d33be76b5..89d178bb70d 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -22,11 +22,9 @@ package main import ( "fmt" "os" - "strings" "github.com/jessevdk/go-flags" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" ) @@ -87,36 +85,6 @@ func run() error { return applySystemFstab(opts.Positionals.SnapName, opts.FromSnapConfine) } -func applyUserFstab(snapName string) error { - desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) - desired, err := osutil.LoadMountProfile(desiredProfilePath) - if err != nil { - return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) - } - - // Replace XDG_RUNTIME_DIR in mount profile - xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) - for i := range desired.Entries { - if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - } - - debugShowProfile(desired, "desired mount profile") - - // TODO: configure the secure helper and inform it about directories that - // can be created without trespassing. - as := &Assumptions{} - // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and - // allow defining layout items that refer to SNAP_USER_DATA and - // SNAP_USER_COMMON. - _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) - return err -} - func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { // Compute the needed changes and perform each change if // needed, collecting those that we managed to perform or that diff --git a/cmd/snap-update-ns/user.go b/cmd/snap-update-ns/user.go new file mode 100644 index 00000000000..03f1b8e0282 --- /dev/null +++ b/cmd/snap-update-ns/user.go @@ -0,0 +1,59 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 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 . + * + */ + +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func applyUserFstab(snapName string) error { + desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) + desired, err := osutil.LoadMountProfile(desiredProfilePath) + if err != nil { + return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) + } + + // Replace XDG_RUNTIME_DIR in mount profile + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + for i := range desired.Entries { + if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { + desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) + } + } + + debugShowProfile(desired, "desired mount profile") + + // TODO: configure the secure helper and inform it about directories that + // can be created without trespassing. + as := &Assumptions{} + // TODO: Handle /home/*/snap/* when we do per-user mount namespaces and + // allow defining layout items that refer to SNAP_USER_DATA and + // SNAP_USER_COMMON. + _, err = applyProfile(snapName, &osutil.MountProfile{}, desired, as) + return err +} From f7a44f256b3313067f6c50b170ffa09fdbfc1238 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Mon, 7 Jan 2019 14:47:13 +0100 Subject: [PATCH 216/580] cmd/snap-update-ns: move applyProfile to utils.go Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/main.go | 48 ------------------------------------- cmd/snap-update-ns/utils.go | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 89d178bb70d..76fc0923751 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -26,7 +26,6 @@ import ( "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" ) var opts struct { @@ -84,50 +83,3 @@ func run() error { } return applySystemFstab(opts.Positionals.SnapName, opts.FromSnapConfine) } - -func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { - // Compute the needed changes and perform each change if - // needed, collecting those that we managed to perform or that - // were performed already. - changesNeeded := NeededChanges(currentBefore, desired) - debugShowChanges(changesNeeded, "mount changes needed") - - logger.Debugf("performing mount changes:") - var changesMade []*Change - for _, change := range changesNeeded { - logger.Debugf("\t * %s", change) - synthesised, err := changePerform(change, as) - changesMade = append(changesMade, synthesised...) - if len(synthesised) > 0 { - logger.Debugf("\tsynthesised additional mount changes:") - for _, synth := range synthesised { - logger.Debugf(" * \t\t%s", synth) - } - } - if err != nil { - // We may have done something even if Perform itself has - // failed. We need to collect synthesized changes and - // store them. - origin := change.Entry.XSnapdOrigin() - if origin == "layout" || origin == "overname" { - return nil, err - } else if err != ErrIgnoredMissingMount { - logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err) - } - continue - } - - changesMade = append(changesMade, change) - } - - // Compute the new current profile so that it contains only changes that were made - // and save it back for next runs. - var currentAfter osutil.MountProfile - for _, change := range changesMade { - if change.Action == Mount || change.Action == Keep { - currentAfter.Entries = append(currentAfter.Entries, change.Entry) - } - } - debugShowProfile(¤tAfter, "current mount profile (after applying changes)") - return ¤tAfter, nil -} diff --git a/cmd/snap-update-ns/utils.go b/cmd/snap-update-ns/utils.go index 5d47aa9809c..125ad28f388 100644 --- a/cmd/snap-update-ns/utils.go +++ b/cmd/snap-update-ns/utils.go @@ -653,3 +653,50 @@ func createWritableMimic(dir, neededBy string, as *Assumptions) ([]*Change, erro } return changes, nil } + +func applyProfile(snapName string, currentBefore, desired *osutil.MountProfile, as *Assumptions) (*osutil.MountProfile, error) { + // Compute the needed changes and perform each change if + // needed, collecting those that we managed to perform or that + // were performed already. + changesNeeded := NeededChanges(currentBefore, desired) + debugShowChanges(changesNeeded, "mount changes needed") + + logger.Debugf("performing mount changes:") + var changesMade []*Change + for _, change := range changesNeeded { + logger.Debugf("\t * %s", change) + synthesised, err := changePerform(change, as) + changesMade = append(changesMade, synthesised...) + if len(synthesised) > 0 { + logger.Debugf("\tsynthesised additional mount changes:") + for _, synth := range synthesised { + logger.Debugf(" * \t\t%s", synth) + } + } + if err != nil { + // We may have done something even if Perform itself has + // failed. We need to collect synthesized changes and + // store them. + origin := change.Entry.XSnapdOrigin() + if origin == "layout" || origin == "overname" { + return nil, err + } else if err != ErrIgnoredMissingMount { + logger.Noticef("cannot change mount namespace of snap %q according to change %s: %s", snapName, change, err) + } + continue + } + + changesMade = append(changesMade, change) + } + + // Compute the new current profile so that it contains only changes that were made + // and save it back for next runs. + var currentAfter osutil.MountProfile + for _, change := range changesMade { + if change.Action == Mount || change.Action == Keep { + currentAfter.Entries = append(currentAfter.Entries, change.Entry) + } + } + debugShowProfile(¤tAfter, "current mount profile (after applying changes)") + return ¤tAfter, nil +} From 2971560cdc1c79606237161e68a280e2e5393252 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 11:55:43 +0000 Subject: [PATCH 217/580] cmd/snap: right-align revision and size in info's channel map --- cmd/snap/cmd_info.go | 53 ++++++++++++++++++++++++++++----------- cmd/snap/cmd_info_test.go | 26 +++++++++---------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go index 795062ec10f..74d7eca9851 100644 --- a/cmd/snap/cmd_info.go +++ b/cmd/snap/cmd_info.go @@ -322,7 +322,7 @@ func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, var channelRisks = []string{"stable", "candidate", "beta", "edge"} // displayChannels displays channels and tracks in the right order -func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap) { +func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { fmt.Fprintln(w, "channels:") releasedfmt := "2006-01-02" @@ -330,6 +330,12 @@ func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, rem releasedfmt = time.RFC3339 } + type chInfoT struct { + name, version, released, revision, size, notes string + } + var chInfos []*chInfoT + maxRevLen, maxSizeLen = revLen, sizeLen + // order by tracks for _, tr := range remote.Tracks { trackHasOpenChannel := false @@ -339,24 +345,36 @@ func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, rem if tr == "latest" { chName = risk } - var version, released, revision, size, notes string + chInfo := chInfoT{name: chName} if ok { - version = ch.Version - revision = fmt.Sprintf("(%s)", ch.Revision) - released = ch.ReleasedAt.Format(releasedfmt) - size = strutil.SizeToStr(ch.Size) - notes = NotesFromChannelSnapInfo(ch).String() + chInfo.version = ch.Version + chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) + if len(chInfo.revision) > maxRevLen { + maxRevLen = len(chInfo.revision) + } + chInfo.released = ch.ReleasedAt.Format(releasedfmt) + chInfo.size = strutil.SizeToStr(ch.Size) + if len(chInfo.size) > maxSizeLen { + maxSizeLen = len(chInfo.size) + } + chInfo.notes = NotesFromChannelSnapInfo(ch).String() trackHasOpenChannel = true } else { if trackHasOpenChannel { - version = esc.uparrow + chInfo.version = esc.uparrow } else { - version = esc.dash + chInfo.version = esc.dash } } - fmt.Fprintf(w, " "+chantpl, chName, version, released, revision, size, notes) + chInfos = append(chInfos, &chInfo) } } + + for _, chInfo := range chInfos { + fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) + } + + return maxRevLen, maxSizeLen } func formatSummary(raw string) string { @@ -455,6 +473,8 @@ func (x *infoCmd) Execute([]string) error { maybePrintType(w, both.Type) maybePrintBase(w, both.Base, x.Verbose) maybePrintID(w, both) + var localRev, localSize string + var revLen, sizeLen int if local != nil { if local.TrackingChannel != "" { fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) @@ -462,19 +482,22 @@ func (x *infoCmd) Execute([]string) error { if !local.InstallDate.IsZero() { fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) } + localRev = fmt.Sprintf("(%s)", local.Revision) + revLen = len(localRev) + localSize = strutil.SizeToStr(local.InstalledSize) + sizeLen = len(localSize) } - chantpl := "%s:\t%s %s%s %s %s\n" + chantpl := "%s:\t%s %s%*s %*s %s\n" if remote != nil && remote.Channels != nil && remote.Tracks != nil { - chantpl = "%s:\t%s\t%s\t%s\t%s\t%s\n" + chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" w.Flush() - x.displayChannels(w, chantpl, esc, remote) + revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) } if local != nil { - revstr := fmt.Sprintf("(%s)", local.Revision) fmt.Fprintf(w, chantpl, - "installed", local.Version, "", revstr, strutil.SizeToStr(local.InstalledSize), notes) + "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) } } diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go index 65db29f70ab..68e1152a22d 100644 --- a/cmd/snap/cmd_info_test.go +++ b/cmd/snap/cmd_info_test.go @@ -304,7 +304,7 @@ const mockInfoJSONNoLicense = ` "name": "hello", "private": false, "resource": "/v2/snaps/hello", - "revision": "1", + "revision": "100", "status": "available", "summary": "The GNU Hello snap", "type": "app", @@ -382,7 +382,7 @@ description: | snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02T22:04:07Z -installed: 2.10 (1) 1kB disabled +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } @@ -419,11 +419,11 @@ snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02T22:04:07Z channels: - 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ -installed: 2.10 (1) 1kB disabled + 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") @@ -443,11 +443,11 @@ snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02 channels: - 1/stable: 2.10 2018-12-18 (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ -installed: 2.10 (1) 1kB disabled + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") c.Check(n, check.Equals, 4) @@ -488,7 +488,7 @@ description: | snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: TOTALLY NOT A ROBOT -installed: 2.10 (1) 1kB disabled +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } From a0b4696ea0480b5c17eb39d9655ca3e1407713e3 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 9 Jan 2019 08:56:21 -0300 Subject: [PATCH 218/580] Fix shellchek --- tests/main/install-store-laaaarge/task.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index c7b3b375544..532dd082bb5 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -15,7 +15,7 @@ restore: | systemd_stop_units snapd.service snapd.socket # Iterate trying to umount /tmp which could be busy for i in $(seq 10); do - if [ $i -eq 10 ]; then + if [ "$i" -eq 10 ]; then echo "Failed to umount /tmp" exit 1 fi From 80c7dfde0fc073c0b628e9ca5991350fda144b10 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 11:55:43 +0000 Subject: [PATCH 219/580] cmd/snap: right-align revision and size in info's channel map --- cmd/snap/cmd_info.go | 53 ++++++++++++++++++++++++++++----------- cmd/snap/cmd_info_test.go | 26 +++++++++---------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/cmd/snap/cmd_info.go b/cmd/snap/cmd_info.go index 795062ec10f..74d7eca9851 100644 --- a/cmd/snap/cmd_info.go +++ b/cmd/snap/cmd_info.go @@ -322,7 +322,7 @@ func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, var channelRisks = []string{"stable", "candidate", "beta", "edge"} // displayChannels displays channels and tracks in the right order -func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap) { +func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { fmt.Fprintln(w, "channels:") releasedfmt := "2006-01-02" @@ -330,6 +330,12 @@ func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, rem releasedfmt = time.RFC3339 } + type chInfoT struct { + name, version, released, revision, size, notes string + } + var chInfos []*chInfoT + maxRevLen, maxSizeLen = revLen, sizeLen + // order by tracks for _, tr := range remote.Tracks { trackHasOpenChannel := false @@ -339,24 +345,36 @@ func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, rem if tr == "latest" { chName = risk } - var version, released, revision, size, notes string + chInfo := chInfoT{name: chName} if ok { - version = ch.Version - revision = fmt.Sprintf("(%s)", ch.Revision) - released = ch.ReleasedAt.Format(releasedfmt) - size = strutil.SizeToStr(ch.Size) - notes = NotesFromChannelSnapInfo(ch).String() + chInfo.version = ch.Version + chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) + if len(chInfo.revision) > maxRevLen { + maxRevLen = len(chInfo.revision) + } + chInfo.released = ch.ReleasedAt.Format(releasedfmt) + chInfo.size = strutil.SizeToStr(ch.Size) + if len(chInfo.size) > maxSizeLen { + maxSizeLen = len(chInfo.size) + } + chInfo.notes = NotesFromChannelSnapInfo(ch).String() trackHasOpenChannel = true } else { if trackHasOpenChannel { - version = esc.uparrow + chInfo.version = esc.uparrow } else { - version = esc.dash + chInfo.version = esc.dash } } - fmt.Fprintf(w, " "+chantpl, chName, version, released, revision, size, notes) + chInfos = append(chInfos, &chInfo) } } + + for _, chInfo := range chInfos { + fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) + } + + return maxRevLen, maxSizeLen } func formatSummary(raw string) string { @@ -455,6 +473,8 @@ func (x *infoCmd) Execute([]string) error { maybePrintType(w, both.Type) maybePrintBase(w, both.Base, x.Verbose) maybePrintID(w, both) + var localRev, localSize string + var revLen, sizeLen int if local != nil { if local.TrackingChannel != "" { fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) @@ -462,19 +482,22 @@ func (x *infoCmd) Execute([]string) error { if !local.InstallDate.IsZero() { fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) } + localRev = fmt.Sprintf("(%s)", local.Revision) + revLen = len(localRev) + localSize = strutil.SizeToStr(local.InstalledSize) + sizeLen = len(localSize) } - chantpl := "%s:\t%s %s%s %s %s\n" + chantpl := "%s:\t%s %s%*s %*s %s\n" if remote != nil && remote.Channels != nil && remote.Tracks != nil { - chantpl = "%s:\t%s\t%s\t%s\t%s\t%s\n" + chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" w.Flush() - x.displayChannels(w, chantpl, esc, remote) + revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) } if local != nil { - revstr := fmt.Sprintf("(%s)", local.Revision) fmt.Fprintf(w, chantpl, - "installed", local.Version, "", revstr, strutil.SizeToStr(local.InstalledSize), notes) + "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) } } diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go index 65db29f70ab..68e1152a22d 100644 --- a/cmd/snap/cmd_info_test.go +++ b/cmd/snap/cmd_info_test.go @@ -304,7 +304,7 @@ const mockInfoJSONNoLicense = ` "name": "hello", "private": false, "resource": "/v2/snaps/hello", - "revision": "1", + "revision": "100", "status": "available", "summary": "The GNU Hello snap", "type": "app", @@ -382,7 +382,7 @@ description: | snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02T22:04:07Z -installed: 2.10 (1) 1kB disabled +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } @@ -419,11 +419,11 @@ snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02T22:04:07Z channels: - 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ -installed: 2.10 (1) 1kB disabled + 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") @@ -443,11 +443,11 @@ snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: 2006-01-02 channels: - 1/stable: 2.10 2018-12-18 (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ -installed: 2.10 (1) 1kB disabled + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ↑ + 1/beta: ↑ + 1/edge: ↑ +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") c.Check(n, check.Equals, 4) @@ -488,7 +488,7 @@ description: | snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 tracking: beta refresh-date: TOTALLY NOT A ROBOT -installed: 2.10 (1) 1kB disabled +installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") } From 92f74b7081b52b62d8c25c3c8b9dbe0c5174a125 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 12:42:47 +0000 Subject: [PATCH 220/580] cmd/snap: only auto-enable unicode to a tty The current behaviour of `--unicode=auto` (the default) is to output unicode depending on the contents of a few locale-related environment variables, independenlty of whether stdout was going to a terminal or not. This was considered a bug. With this change, the behaviour changes to not output unicode to a pipe unless `--unicode=always` is given. --- cmd/snap/cmd_find_test.go | 12 ++++---- cmd/snap/cmd_info_test.go | 52 +++++++++++++++++++++++++--------- cmd/snap/color.go | 3 ++ cmd/snap/color_test.go | 4 +++ tests/main/snap-info/task.yaml | 10 ++++++- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/cmd/snap/cmd_find_test.go b/cmd/snap/cmd_find_test.go index 53cdd6630cd..8e6de081070 100644 --- a/cmd/snap/cmd_find_test.go +++ b/cmd/snap/cmd_find_test.go @@ -143,8 +143,8 @@ func (s *SnapSuite) TestFindSnapName(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical✓ +- +GNU Hello, the "hello world" snap -hello-world +6.1 +canonical✓ +- +Hello world example +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap +hello-world +6.1 +canonical\* +- +Hello world example hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -234,7 +234,7 @@ func (s *SnapSuite) TestFindHello(c *check.C) { c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical✓ +- +GNU Hello, the "hello world" snap +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -261,7 +261,7 @@ func (s *SnapSuite) TestFindHelloNarrow(c *check.C) { c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical✓ +- +GNU Hello, the "hello world" snap +hello +2.10 +canonical\* +- +GNU Hello, the "hello world" snap hello-huge +1.0 +noise +- +a really big snap `) c.Check(s.Stderr(), check.Equals, "") @@ -324,7 +324,7 @@ func (s *SnapSuite) TestFindPriced(c *check.C) { c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical✓ +1.99GBP +GNU Hello, the "hello world" snap +hello +2.10 +canonical\* +1.99GBP +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } @@ -385,7 +385,7 @@ func (s *SnapSuite) TestFindPricedAndBought(c *check.C) { c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `Name +Version +Publisher +Notes +Summary -hello +2.10 +canonical✓ +bought +GNU Hello, the "hello world" snap +hello +2.10 +canonical\* +bought +GNU Hello, the "hello world" snap `) c.Check(s.Stderr(), check.Equals, "") } diff --git a/cmd/snap/cmd_info_test.go b/cmd/snap/cmd_info_test.go index 68e1152a22d..185fd7df922 100644 --- a/cmd/snap/cmd_info_test.go +++ b/cmd/snap/cmd_info_test.go @@ -120,7 +120,7 @@ func (s *infoSuite) TestInfoPriced(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: GNU Hello, the "hello world" snap -publisher: Canonical✓ +publisher: Canonical* license: Proprietary price: 1.99GBP description: | @@ -240,7 +240,7 @@ func (s *infoSuite) TestInfoUnquoted(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical✓ +publisher: Canonical* license: MIT description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -338,7 +338,7 @@ func (s *infoSuite) TestInfoWithLocalDifferentLicense(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical✓ +publisher: Canonical* license: BSD-3 description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -374,7 +374,7 @@ func (s *infoSuite) TestInfoWithLocalNoLicense(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical✓ +publisher: Canonical* license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -391,16 +391,16 @@ func (s *infoSuite) TestInfoWithChannelsAndLocal(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { - case 0, 2: + case 0, 2, 4: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/find") fmt.Fprintln(w, mockInfoJSONWithChannels) - case 1, 3: + case 1, 3, 5: c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") fmt.Fprintln(w, mockInfoJSONNoLicense) default: - c.Fatalf("expected to get 4 requests, now on %d (%v)", n+1, r) + c.Fatalf("expected to get 6 requests, now on %d (%v)", n+1, r) } n++ @@ -410,7 +410,7 @@ func (s *infoSuite) TestInfoWithChannelsAndLocal(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical✓ +publisher: Canonical* license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -420,12 +420,13 @@ tracking: beta refresh-date: 2006-01-02T22:04:07Z channels: 1/stable: 2.10 2018-12-18T15:16:56Z (1) 65kB - - 1/candidate: ↑ - 1/beta: ↑ - 1/edge: ↑ + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 2) // now the same but without abs-time s.ResetStdStreams() @@ -434,6 +435,31 @@ installed: 2.10 (100) 1kB disabled c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap +publisher: Canonical* +license: unset +description: | + GNU hello prints a friendly greeting. This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +tracking: beta +refresh-date: 2006-01-02 +channels: + 1/stable: 2.10 2018-12-18 (1) 65kB - + 1/candidate: ^ + 1/beta: ^ + 1/edge: ^ +installed: 2.10 (100) 1kB disabled +`) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 4) + + // now the same but with unicode on + s.ResetStdStreams() + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"info", "--unicode=always", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `name: hello +summary: The GNU Hello snap publisher: Canonical✓ license: unset description: | @@ -450,7 +476,7 @@ channels: installed: 2.10 (100) 1kB disabled `) c.Check(s.Stderr(), check.Equals, "") - c.Check(n, check.Equals, 4) + c.Check(n, check.Equals, 6) } func (s *infoSuite) TestInfoHumanTimes(c *check.C) { @@ -480,7 +506,7 @@ func (s *infoSuite) TestInfoHumanTimes(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap -publisher: Canonical✓ +publisher: Canonical* license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at diff --git a/cmd/snap/color.go b/cmd/snap/color.go index f0b0e816bba..e9b5ed03fa0 100644 --- a/cmd/snap/color.go +++ b/cmd/snap/color.go @@ -57,6 +57,9 @@ func canUnicode(mode string) bool { case "never": return false } + if !isStdoutTTY { + return false + } var lang string for _, k := range []string{"LC_MESSAGES", "LC_ALL", "LANG"} { lang = os.Getenv(k) diff --git a/cmd/snap/color_test.go b/cmd/snap/color_test.go index e8d391de9a7..68c6445169d 100644 --- a/cmd/snap/color_test.go +++ b/cmd/snap/color_test.go @@ -82,7 +82,11 @@ func (s *SnapSuite) TestCanUnicode(c *check.C) { restore := setEnviron(map[string]string{"LANG": t.lang, "LC_ALL": t.lcAll, "LC_MESSAGES": t.lcMsg}) c.Check(cmdsnap.CanUnicode("never"), check.Equals, false) c.Check(cmdsnap.CanUnicode("always"), check.Equals, true) + restoreIsTTY := cmdsnap.MockIsStdoutTTY(true) c.Check(cmdsnap.CanUnicode("auto"), check.Equals, t.expected) + cmdsnap.MockIsStdoutTTY(false) + c.Check(cmdsnap.CanUnicode("auto"), check.Equals, false) + restoreIsTTY() restore() } } diff --git a/tests/main/snap-info/task.yaml b/tests/main/snap-info/task.yaml index 3f3fdd01410..4ec53d2989e 100644 --- a/tests/main/snap-info/task.yaml +++ b/tests/main/snap-info/task.yaml @@ -20,7 +20,15 @@ execute: | echo "With one non-snap argument, errors out" ! snap info /etc/passwd - snap info basic_1.0_all.snap "$TESTSLIB"/snaps/basic-desktop test-snapd-tools test-snapd-devmode core /etc/passwd test-snapd-python-webserver > out + snap info --unicode=always \ + basic_1.0_all.snap \ + "$TESTSLIB"/snaps/basic-desktop \ + test-snapd-tools \ + test-snapd-devmode \ + core \ + /etc/passwd \ + test-snapd-python-webserver \ + > out PYTHONIOENCODING=utf8 python3 check.py < out #shellcheck source=tests/lib/snaps.sh From 603cd96b0fa80b778d44aef78ee48298f702e16b Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 14:50:55 +0000 Subject: [PATCH 221/580] store: undo workaround for timezone-less released-at the store rolled out the fix \o/ --- store/details_v2.go | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/store/details_v2.go b/store/details_v2.go index b92c6f5cfdc..51de106ddbb 100644 --- a/store/details_v2.go +++ b/store/details_v2.go @@ -20,7 +20,6 @@ package store import ( - "encoding/json" "fmt" "strconv" "time" @@ -86,32 +85,11 @@ type storeSnapMedia struct { // storeInfoChannel is the channel description included in info results type storeInfoChannel struct { - Architecture string `json:"architecture"` - Name string `json:"name"` - Risk string `json:"risk"` - Track string `json:"track"` - ReleasedAt xTime `json:"released-at"` -} - -// time.Time but also try without a timezone -- this is a workaround -// for a store-side issue that already has a fix, that should get -// deployed today (2019-01-09) -type xTime time.Time - -func (tp *xTime) UnmarshalJSON(buf []byte) error { - var str string - if err := json.Unmarshal(buf, &str); err != nil { - return err - } - t, err := time.Parse(time.RFC3339, str) - if err != nil { - t, err = time.Parse("2006-01-02T15:04:05", str) - if err != nil { - return err - } - } - *tp = xTime(t) - return nil + Architecture string `json:"architecture"` + Name string `json:"name"` + Risk string `json:"risk"` + Track string `json:"track"` + ReleasedAt time.Time `json:"released-at"` } // storeInfoChannelSnap is the snap-in-a-channel of which the channel map is made @@ -160,7 +138,7 @@ func infoFromStoreInfo(si *storeInfo) (*snap.Info, error) { Channel: ch.Name, Epoch: s.Epoch, Size: s.Download.Size, - ReleasedAt: time.Time(ch.ReleasedAt).UTC(), + ReleasedAt: ch.ReleasedAt.UTC(), } if !seen[ch.Track] { seen[ch.Track] = true From a645b7e75f340240b972a71af1b99e234e634dc9 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Wed, 9 Jan 2019 15:39:52 +0000 Subject: [PATCH 222/580] tests: update so they don't look for unicode where it won't be I've explicitly added `--unicode=never` to these so they will pass even when running without the changes from this PR (the changes in the PR are sufficiently covered in unit tests already). --- tests/main/install-store/task.yaml | 8 ++++---- tests/main/interfaces-bluez/task.yaml | 4 ++-- tests/main/listing/task.yaml | 8 ++++---- tests/nested/extra-snaps-assertions/task.yaml | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/main/install-store/task.yaml b/tests/main/install-store/task.yaml index f4c986c2810..d0cea594c22 100644 --- a/tests/main/install-store/task.yaml +++ b/tests/main/install-store/task.yaml @@ -13,16 +13,16 @@ environment: execute: | echo "Install from different channels" - expected="(?s)$SNAP_NAME .* from Canonical✓ installed\\n" + expected="(?s)$SNAP_NAME .* from Canonical\\* installed\\n" for channel in edge beta candidate stable do - snap install "$SNAP_NAME" --channel=$channel | grep -Pzq "$expected" + snap install --unicode=never "$SNAP_NAME" --channel=$channel | grep -Pzq "$expected" snap remove "$SNAP_NAME" done echo "Install non-devmode snap with devmode option" - expected="(?s)$SNAP_NAME .* from Canonical✓ installed\\n" - snap install "$SNAP_NAME" --devmode | grep -Pzq "$expected" + expected="(?s)$SNAP_NAME .* from Canonical\\* installed\\n" + snap install --unicode=never "$SNAP_NAME" --devmode | grep -Pzq "$expected" echo "Install devmode snap without devmode option" expected="repeat the command including --devmode" diff --git a/tests/main/interfaces-bluez/task.yaml b/tests/main/interfaces-bluez/task.yaml index a63c16efeff..a982d94f747 100644 --- a/tests/main/interfaces-bluez/task.yaml +++ b/tests/main/interfaces-bluez/task.yaml @@ -11,9 +11,9 @@ environment: SNAP_NAME: bluez execute: | - if ! snap list bluez &> /dev/null ; then + if ! snap list --unicode=never bluez &> /dev/null ; then echo "Installing bluez snap from the store ..." - expected="(?s)$SNAP_NAME .* from Canonical✓ installed\\n" + expected="(?s)$SNAP_NAME .* from Canonical\\* installed\\n" snap install "$SNAP_NAME" | grep -Pzq "$expected" fi diff --git a/tests/main/listing/task.yaml b/tests/main/listing/task.yaml index 5dc9708d775..6d49d35c3c3 100644 --- a/tests/main/listing/task.yaml +++ b/tests/main/listing/task.yaml @@ -29,13 +29,13 @@ execute: | expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +x[0-9]+ +- +- +core.*$' elif [ "$SRU_VALIDATION" = "1" ]; then echo "When sru validation is done the core snap is installed from the store" - expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+[0-9]+\.[0-9a-f]+)? +[0-9]+ +stable +canonical✓ +core.*$' + expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+[0-9]+\.[0-9a-f]+)? +[0-9]+ +stable +canonical\\* +core.*$' elif [ "$SPREAD_BACKEND" = "external" ] || [ "$SPREAD_BACKEND" = "autopkgtest" ]; then - expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +(edge|beta|candidate|stable) +canonical✓ +core.*$' + expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +(edge|beta|candidate|stable) +canonical\\* +core.*$' else - expected="^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\\+git[0-9]+\\.[0-9a-f]+)? +[0-9]+ +$CORE_CHANNEL +canonical✓ +core.*$" + expected="^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\\+git[0-9]+\\.[0-9a-f]+)? +[0-9]+ +$CORE_CHANNEL +canonical\\* +core.*$" fi - snap list | MATCH "$expected" + snap list --unicode=never | MATCH "$expected" echo "List prints installed snaps and versions" snap list | MATCH '^test-snapd-tools +[0-9]+(\.[0-9]+)* +x[0-9]+ +- +- +- *$' diff --git a/tests/nested/extra-snaps-assertions/task.yaml b/tests/nested/extra-snaps-assertions/task.yaml index a23fb9e593b..9001306b3a0 100644 --- a/tests/nested/extra-snaps-assertions/task.yaml +++ b/tests/nested/extra-snaps-assertions/task.yaml @@ -28,4 +28,4 @@ execute: | execute_remote "snap known model" | MATCH "series: 16" echo "Make sure core has an actual revision" - execute_remote "snap list" | MATCH "^core +[0-9]+\\-[0-9.]+ +[0-9]+ +$CORE_CHANNEL +canonical✓ +core" + execute_remote "snap list --unicode=never" | MATCH "^core +[0-9]+\\-[0-9.]+ +[0-9]+ +$CORE_CHANNEL +canonical\\* +core" From 21145543e1c3df8763c6798ee4d64d0712d94f88 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 9 Jan 2019 17:20:43 +0100 Subject: [PATCH 223/580] systemd: add unit tests for new {Add,Remove}MountUnitFile --- systemd/systemd.go | 32 ++++++++++++++- systemd/systemd_test.go | 86 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/systemd/systemd.go b/systemd/systemd.go index 09a8746d33b..f1863732a15 100644 --- a/systemd/systemd.go +++ b/systemd/systemd.go @@ -30,6 +30,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" _ "github.com/snapcore/squashfuse" @@ -56,9 +57,36 @@ var ( // // See https://github.com/systemd/systemd/issues/10872 for the // upstream systemd bug - daemonReloadLock sync.Mutex + daemonReloadLock extMutex ) +// mu is a sync.Mutex that also supports to check if the lock is taken +type extMutex struct { + lock sync.Mutex + muC int32 +} + +// Lock acquires the mutex +func (m *extMutex) Lock() { + m.lock.Lock() + atomic.AddInt32(&m.muC, 1) +} + +// Unlock releases the mutex +func (m *extMutex) Unlock() { + m.lock.Unlock() + atomic.AddInt32(&m.muC, -1) +} + +// Taken will panic with the given error message if the lock is not +// taken when this code runs. This is useful to internally check if +// something is accessed without a valid lock. +func (m *extMutex) Taken(errMsg string) { + if atomic.LoadInt32(&m.muC) != 1 { + panic("internal error: " + errMsg) + } +} + // systemctlCmd calls systemctl with the given args, returning its standard output (and wrapped error) var systemctlCmd = func(args ...string) ([]byte, error) { bs, err := exec.Command("systemctl", args...).CombinedOutput() @@ -190,6 +218,8 @@ func (s *systemd) DaemonReload() error { } func (s *systemd) daemonReloadNoLock() error { + daemonReloadLock.Taken("cannot use daemon-reload without lock") + _, err := systemctlCmd("daemon-reload") return err } diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 78e38a0cde5..6774162df29 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -486,17 +486,23 @@ func (s *SystemdTestSuite) TestMountUnitPath(c *C) { c.Assert(MountUnitPath("/apps/hello/1.1"), Equals, filepath.Join(dirs.SnapServicesDir, "apps-hello-1.1.mount")) } +func makeMockFile(c *C, path string) { + err := os.MkdirAll(filepath.Dir(path), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(path, nil, 0644) + c.Assert(err, IsNil) +} + func (s *SystemdTestSuite) TestAddMountUnit(c *C) { + rootDir := dirs.GlobalRootDir + restore := squashfs.MockUseFuse(false) defer restore() mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap") - err := os.MkdirAll(filepath.Dir(mockSnapPath), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(mockSnapPath, nil, 0644) - c.Assert(err, IsNil) + makeMockFile(c, mockSnapPath) - mountUnitName, err := New("", nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New(rootDir, nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -514,6 +520,12 @@ Options=nodev,ro,x-gdu.hide [Install] WantedBy=multi-user.target `[1:], mockSnapPath)) + + c.Assert(s.argses, DeepEquals, [][]string{ + {"daemon-reload"}, + {"--root", rootDir, "enable", "snap-snapname-123.mount"}, + {"start", "snap-snapname-123.mount"}, + }) } func (s *SystemdTestSuite) TestAddMountUnitForDirs(c *C) { @@ -540,6 +552,12 @@ Options=nodev,ro,x-gdu.hide,bind [Install] WantedBy=multi-user.target `[1:], snapDir)) + + c.Assert(s.argses, DeepEquals, [][]string{ + {"daemon-reload"}, + {"--root", "", "enable", "snap-snapname-x1.mount"}, + {"start", "snap-snapname-x1.mount"}, + }) } func (s *SystemdTestSuite) TestFuseInContainer(c *C) { @@ -673,3 +691,61 @@ func (s *SystemdTestSuite) TestIsActiveErr(c *C) { c.Assert(active, Equals, false) c.Assert(err, ErrorMatches, ".* failed with exit status 1: random-failure\n") } + +func makeMockMountUnit(c *C, mountDir string) string { + mountUnit := MountUnitPath(dirs.StripRootDir(mountDir)) + err := ioutil.WriteFile(mountUnit, nil, 0644) + c.Assert(err, IsNil) + return mountUnit +} + +// FIXME: also test for the "IsMounted" case +func (s *SystemdTestSuite) TestRemoveMountUnit(c *C) { + rootDir := dirs.GlobalRootDir + + mountDir := rootDir + "/snap/foo/42" + mountUnit := makeMockMountUnit(c, mountDir) + err := New(rootDir, nil).RemoveMountUnitFile(mountDir) + + c.Assert(err, IsNil) + // the file is gone + c.Check(osutil.FileExists(mountUnit), Equals, false) + // and the unit is disabled and the daemon reloaded + c.Check(s.argses, DeepEquals, [][]string{ + {"--root", rootDir, "disable", "snap-foo-42.mount"}, + {"daemon-reload"}, + }) +} + +func (s *SystemdTestSuite) TestDaemonReloadMutex(c *C) { + rootDir := dirs.GlobalRootDir + sysd := New(rootDir, nil) + + mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap") + makeMockFile(c, mockSnapPath) + + // create a go-routine that will try to daemon-reload like crazy + stopCh := make(chan bool, 1) + stoppedCh := make(chan bool, 1) + go func() { + for { + sysd.DaemonReload() + select { + case <-stopCh: + close(stoppedCh) + return + default: + //pass + } + } + }() + + // And now add a mount unit file while the go-routine tries to + // daemon-reload. This will be serialized, if not this would + // panic because systemd.daemonReloadNoLock ensures the lock is + // taken when this happens. + _, err := sysd.AddMountUnitFile("foo", "42", mockSnapPath, "/snap/foo/42", "squashfs") + c.Assert(err, IsNil) + close(stopCh) + <-stoppedCh +} From ffc624c345de5064d105f3933f13c38033b367f2 Mon Sep 17 00:00:00 2001 From: Sergio Cazzolato Date: Wed, 9 Jan 2019 14:43:47 -0300 Subject: [PATCH 224/580] Umount lazy instead of retrying --- tests/main/install-store-laaaarge/task.yaml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/main/install-store-laaaarge/task.yaml b/tests/main/install-store-laaaarge/task.yaml index 532dd082bb5..e45552860ea 100644 --- a/tests/main/install-store-laaaarge/task.yaml +++ b/tests/main/install-store-laaaarge/task.yaml @@ -13,17 +13,8 @@ restore: | . "$TESTSLIB/systemd.sh" systemd_stop_units snapd.service snapd.socket - # Iterate trying to umount /tmp which could be busy - for i in $(seq 10); do - if [ "$i" -eq 10 ]; then - echo "Failed to umount /tmp" - exit 1 - fi - if umount /tmp; then - break - fi - sleep 1 - done + # Umount lazy to avoid busy device error + umount -l /tmp systemctl start snapd.{socket,service} execute: | From f87d600af3f666e0712cbe98f789656c011b98ce Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 9 Jan 2019 20:12:51 +0100 Subject: [PATCH 225/580] tests: improve mount-protocol-error spread test Thanks to Maciej and Sergio! --- tests/main/mount-protocol-error/task.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/main/mount-protocol-error/task.yaml b/tests/main/mount-protocol-error/task.yaml index 49a30973635..4100ea9621b 100644 --- a/tests/main/mount-protocol-error/task.yaml +++ b/tests/main/mount-protocol-error/task.yaml @@ -7,11 +7,22 @@ description: | For more discussion on the issue see https://forum.snapcraft.io/t/5682 + https://launchpad.net/bugs/1772016 backends: [-autopkgtest] +# only run on a subset of systems because this test takes a long time to run +systems: [ubuntu-18.04-64, ubuntu-18.10-64, arch-linux-64] + execute: | - for _ in $(seq 50); do - snap install test-snapd-tools test-snapd-public - snap remove test-snapd-tools test-snapd-public - done + snap set system experimental.parallel-instances=true + + names=(test-snapd-tools) + for n in $(seq 9); do + names+=(test-snapd-tools_$n) + done + for i in $(seq 10); do + echo "Install $i" + snap install ${names[@]} + snap remove ${names[@]} + done From 2161fa8e4f6d23ad6d9bed69c0ba70912e03196a Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 10 Jan 2019 07:54:59 +0100 Subject: [PATCH 226/580] tests/main/mount-protocol-error: shellchecks Signed-off-by: Maciej Borzecki --- tests/main/mount-protocol-error/task.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/main/mount-protocol-error/task.yaml b/tests/main/mount-protocol-error/task.yaml index 4100ea9621b..758e7f59e9a 100644 --- a/tests/main/mount-protocol-error/task.yaml +++ b/tests/main/mount-protocol-error/task.yaml @@ -19,10 +19,10 @@ execute: | names=(test-snapd-tools) for n in $(seq 9); do - names+=(test-snapd-tools_$n) + names+=("test-snapd-tools_$n") done for i in $(seq 10); do echo "Install $i" - snap install ${names[@]} - snap remove ${names[@]} + snap install "${names[@]}" + snap remove "${names[@]}" done From edb176d90fe1291dc67c117c28fdac874aa5dba6 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 9 Jan 2019 18:34:12 +0100 Subject: [PATCH 227/580] cmd/snap-confine: join freezer only after setting up user mount This way snap-update-ns can freeze the set of snap processes without freezing itself. Currently this is not an issue but as soon as snap-update-ns needs to freeze running applications while updating per-user mount namespace, it becomes relevant. Signed-off-by: Zygmunt Krynicki --- cmd/snap-confine/snap-confine.c | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/cmd/snap-confine/snap-confine.c b/cmd/snap-confine/snap-confine.c index fd6bc52815d..bce13b731a5 100644 --- a/cmd/snap-confine/snap-confine.c +++ b/cmd/snap-confine/snap-confine.c @@ -268,25 +268,6 @@ int main(int argc, char **argv) sc_maybe_fixup_permissions(); sc_maybe_fixup_udev(); - // Associate each snap process with a dedicated snap freezer - // control group. This simplifies testing if any processes - // belonging to a given snap are still alive. - // See the documentation of the function for details. - - if (getegid() != 0 && saved_gid == 0) { - // Temporarily raise egid so we can chown the freezer cgroup - // under LXD. - if (setegid(0) != 0) { - die("cannot set effective group id to root"); - } - } - sc_cgroup_freezer_join(snap_instance, getpid()); - if (geteuid() == 0 && real_gid != 0) { - if (setegid(real_gid) != 0) { - die("cannot set effective group id to %d", real_gid); - } - } - /* User mount profiles do not apply to non-root users. */ if (real_uid != 0) { debug @@ -320,6 +301,25 @@ int main(int argc, char **argv) } } + // Associate each snap process with a dedicated snap freezer + // control group. This simplifies testing if any processes + // belonging to a given snap are still alive. + // See the documentation of the function for details. + if (getegid() != 0 && saved_gid == 0) { + // Temporarily raise egid so we can chown the freezer cgroup + // under LXD. + if (setegid(0) != 0) { + die("cannot set effective group id to root"); + } + } + sc_cgroup_freezer_join(snap_instance, getpid()); + if (geteuid() == 0 && real_gid != 0) { + if (setegid(real_gid) != 0) { + die("cannot set effective group id to %d", real_gid); + } + } + + sc_unlock(snap_lock_fd); sc_close_mount_ns(group); From c056f9a9f2ecd253cb3c5e1d122cfa9d13c1321b Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 9 Jan 2019 18:35:15 +0100 Subject: [PATCH 228/580] cmd/libsnap: pass --from-snap-confine when invoking snap-update-ns as user This will allow snap-update-ns to understand proper locking semantics when invoked from snap-confine (it will check instead of locking). Signed-off-by: Zygmunt Krynicki --- cmd/libsnap-confine-private/tool.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/libsnap-confine-private/tool.c b/cmd/libsnap-confine-private/tool.c index fc67ebb2447..6a2f839465c 100644 --- a/cmd/libsnap-confine-private/tool.c +++ b/cmd/libsnap-confine-private/tool.c @@ -115,7 +115,7 @@ void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, char *argv[] = { "snap-update-ns", /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ - /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + "--from-snap-confine", /* This tells snap-update-ns that we want to process the per-user profile */ "--user-mounts", snap_name_copy, NULL }; From ab4777eb1f38bdfb4a8c05103e21c6cafeea04f7 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 9 Jan 2019 15:11:50 +0100 Subject: [PATCH 229/580] osutil: add helper for loading fstab from string Signed-off-by: Zygmunt Krynicki --- osutil/mountprofile_linux.go | 5 +++++ osutil/mountprofile_linux_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/osutil/mountprofile_linux.go b/osutil/mountprofile_linux.go index f3dc8722215..98c56629f92 100644 --- a/osutil/mountprofile_linux.go +++ b/osutil/mountprofile_linux.go @@ -48,6 +48,11 @@ func LoadMountProfile(fname string) (*MountProfile, error) { return ReadMountProfile(f) } +// LoadMountProfileText loads a mount profile from a given string. +func LoadMountProfileText(fstab string) (*MountProfile, error) { + return ReadMountProfile(strings.NewReader(fstab)) +} + // Save saves a mount profile (fstab-like) to a given file. // The profile is saved with an atomic write+rename+sync operation. func (p *MountProfile) Save(fname string) error { diff --git a/osutil/mountprofile_linux_test.go b/osutil/mountprofile_linux_test.go index 747ecd831ad..1be6c976ca0 100644 --- a/osutil/mountprofile_linux_test.go +++ b/osutil/mountprofile_linux_test.go @@ -78,6 +78,14 @@ name#-1 dir#-1 type#-1 options#-1 1 1 # inline comment }) } +func (s *profileSuite) TestLoadMountProfileText(c *C) { + p, err := osutil.LoadMountProfileText("tmpfs /tmp tmpfs defaults 0 0") + c.Assert(err, IsNil) + c.Assert(p.Entries, DeepEquals, []osutil.MountEntry{ + {Name: "tmpfs", Dir: "/tmp", Type: "tmpfs", Options: []string{"defaults"}}, + }) +} + // Test that saving a profile to a file works correctly. func (s *profileSuite) TestSaveMountProfile1(c *C) { dir := c.MkDir() From 15d94233ef9e1c6bfe1e55d36d97c41836fdfc8f Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 10 Jan 2019 11:23:41 +0100 Subject: [PATCH 230/580] cmd/snap-update-ns: move XDG code to dedicated file This shrinks the diff from another huge branch and provides clear tests for a small fraction of the per-user mount profile handling logic. Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/export_test.go | 3 ++ cmd/snap-update-ns/main.go | 13 +------- cmd/snap-update-ns/xdg.go | 46 +++++++++++++++++++++++++++++ cmd/snap-update-ns/xdg_test.go | 49 +++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 cmd/snap-update-ns/xdg.go create mode 100644 cmd/snap-update-ns/xdg_test.go diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go index 74865fc68eb..9003385047e 100644 --- a/cmd/snap-update-ns/export_test.go +++ b/cmd/snap-update-ns/export_test.go @@ -49,6 +49,9 @@ var ( // trespassing IsReadOnly = isReadOnly IsPrivateTmpfsCreatedBySnapd = isPrivateTmpfsCreatedBySnapd + // xdg + XdgRuntimeDir = xdgRuntimeDir + ExpandXdgRuntimeDir = expandXdgRuntimeDir ) // SystemCalls encapsulates various system interactions performed by this module. diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index c7baa7c84f4..dbf1b4db6cf 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -22,7 +22,6 @@ package main import ( "fmt" "os" - "strings" "github.com/jessevdk/go-flags" @@ -270,17 +269,7 @@ func applyUserFstab(snapName string) error { return fmt.Errorf("cannot load desired user mount profile of snap %q: %s", snapName, err) } - // Replace XDG_RUNTIME_DIR in mount profile - xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) - for i := range desired.Entries { - if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - if strings.HasPrefix(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { - desired.Entries[i].Dir = strings.Replace(desired.Entries[i].Dir, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) - } - } - + expandXdgRuntimeDir(desired, os.Getuid()) debugShowProfile(desired, "desired mount profile") // TODO: configure the secure helper and inform it about directories that diff --git a/cmd/snap-update-ns/xdg.go b/cmd/snap-update-ns/xdg.go new file mode 100644 index 00000000000..e2409466667 --- /dev/null +++ b/cmd/snap-update-ns/xdg.go @@ -0,0 +1,46 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 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 . + * + */ + +package main + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// xdgRuntimeDir returns the path to XDG_RUNTIME_DIR for a given user ID. +func xdgRuntimeDir(uid int) string { + return fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, uid) +} + +// expandXdgRuntimeDir expands the $XDG_RUNTIME_DIR variable in the given mount profile. +func expandXdgRuntimeDir(profile *osutil.MountProfile, uid int) { + dir := xdgRuntimeDir(uid) + for i := range profile.Entries { + if strings.HasPrefix(profile.Entries[i].Name, "$XDG_RUNTIME_DIR/") { + profile.Entries[i].Name = strings.Replace(profile.Entries[i].Name, "$XDG_RUNTIME_DIR", dir, 1) + } + if strings.HasPrefix(profile.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { + profile.Entries[i].Dir = strings.Replace(profile.Entries[i].Dir, "$XDG_RUNTIME_DIR", dir, 1) + } + } +} diff --git a/cmd/snap-update-ns/xdg_test.go b/cmd/snap-update-ns/xdg_test.go new file mode 100644 index 00000000000..affe9bd2516 --- /dev/null +++ b/cmd/snap-update-ns/xdg_test.go @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2017 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 . + * + */ + +package main_test + +import ( + "bytes" + "strings" + + . "gopkg.in/check.v1" + + update "github.com/snapcore/snapd/cmd/snap-update-ns" + "github.com/snapcore/snapd/osutil" +) + +type xdgSuite struct{} + +var _ = Suite(&xdgSuite{}) + +func (s *xdgSuite) TestXdgRuntimeDir(c *C) { + c.Check(update.XdgRuntimeDir(1234), Equals, "/run/user/1234") +} + +func (s *xdgSuite) TestExpandXdgRuntimeDir(c *C) { + input := "$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0\n" + output := "/run/user/1234/doc/by-app/snap.foo /run/user/1234/doc none bind,rw 0 0\n" + profile, err := osutil.ReadMountProfile(strings.NewReader(input)) + c.Assert(err, IsNil) + update.ExpandXdgRuntimeDir(profile, 1234) + builder := &bytes.Buffer{} + profile.WriteTo(builder) + c.Check(builder.String(), Equals, output) +} From f9e50494f043110d73c77ab2e4795c7dce8ab355 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Wed, 9 Jan 2019 17:26:46 +0100 Subject: [PATCH 231/580] cmd/snap-confine: don't preemptively create .mnt files When snap-confine works with .mnt files (preserved mount namespaces) it usually handles them being absent just fine, only creating them when really strictly necessary (when preserving a mount namespace). While working on some tests I noticed that the per-user mount namespace file would be created (a empty placeholder file) even if that feature is enirely disabled. While the file doesn't "hurt" it was not my intention before. This patch fixes this issue. Signed-off-by: Zygmunt Krynicki --- cmd/snap-confine/ns-support.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c index 5b91bc9e36b..5eab65feb49 100644 --- a/cmd/snap-confine/ns-support.c +++ b/cmd/snap-confine/ns-support.c @@ -457,7 +457,10 @@ int sc_join_preserved_ns(struct sc_mount_ns *group, struct sc_apparmor // NOTE: There is no O_EXCL here because the file can be around but // doesn't have to be a mounted namespace. mnt_fd = openat(group->dir_fd, mnt_fname, - O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0 && errno == ENOENT) { + return ESRCH; + } if (mnt_fd < 0) { die("cannot open preserved mount namespace %s", group->name); } @@ -527,7 +530,10 @@ int sc_join_preserved_per_user_ns(struct sc_mount_ns *group, int mnt_fd SC_CLEANUP(sc_cleanup_close) = -1; mnt_fd = openat(group->dir_fd, mnt_fname, - O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0600); + if (mnt_fd < 0 && errno == ENOENT) { + return ESRCH; + } if (mnt_fd < 0) { die("cannot open preserved mount namespace %s", group->name); } @@ -683,6 +689,7 @@ static void helper_capture_ns(struct sc_mount_ns *group, pid_t parent) die("cannot create file %s", dst); } close(fd); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { die("cannot preserve mount namespace of process %d as %s", (int)parent, dst); @@ -700,6 +707,14 @@ static void helper_capture_per_user_ns(struct sc_mount_ns *group, pid_t parent) debug("capturing per-snap, per-user mount namespace"); sc_must_snprintf(src, sizeof src, "/proc/%d/ns/mnt", (int)parent); sc_must_snprintf(dst, sizeof dst, "%s.%d.mnt", group->name, (int)uid); + + /* Ensure the bind mount destination exists. */ + int fd = open(dst, O_CREAT | O_CLOEXEC | O_NOFOLLOW | O_RDONLY, 0600); + if (fd < 0) { + die("cannot create file %s", dst); + } + close(fd); + if (mount(src, dst, NULL, MS_BIND, NULL) < 0) { die("cannot preserve per-user mount namespace of process %d as %s", (int)parent, dst); } From 475ba3ddfda274f19f497a6f32a118ef7557db65 Mon Sep 17 00:00:00 2001 From: Pawel Stolowski Date: Thu, 10 Jan 2019 12:14:46 +0100 Subject: [PATCH 232/580] Always consider auto-connections in hotplug-connect handler. Some new unit tests. --- overlord/ifacestate/handlers.go | 134 ++++++++++++------------- overlord/ifacestate/ifacestate_test.go | 97 +++++++++++++++++- 2 files changed, 155 insertions(+), 76 deletions(-) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 3db9dbfaaff..9a30a3981f7 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -1258,7 +1258,7 @@ func (m *InterfaceManager) doGadgetConnect(task *state.Task, _ *tomb.Tomb) error return nil } -// doHotplugConnect creates task(s) to (re)create connections in response to hotplug "add" event. +// doHotplugConnect creates task(s) to (re)create old connections or auto-connect viable slots in response to hotplug "add" event. func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) error { st := task.State() st.Lock() @@ -1274,16 +1274,19 @@ func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) erro return fmt.Errorf("internal error: cannot get hotplug task attributes: %s", err) } + slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) + if err != nil { + return err + } + if slot == nil { + return fmt.Errorf("cannot find hotplug slot for interface %s and hotplug key %q", ifaceName, hotplugKey) + } + // find old connections for slots of this device - note we can't ask the repository since we need // to recreate old connections that are only remembered in the state. connsForDevice := findConnsForHotplugKey(conns, ifaceName, hotplugKey) - // we see this device for the first time (or it didn't have any connected slot before) - if len(connsForDevice) == 0 { - return m.autoconnectNewDevice(task, ifaceName, hotplugKey) - } - - // recreate old connections + // find old connections to recreate var recreate []*interfaces.ConnRef for _, id := range connsForDevice { conn := conns[id] @@ -1310,19 +1313,69 @@ func (m *InterfaceManager) doHotplugConnect(task *state.Task, _ *tomb.Tomb) erro recreate = append(recreate, connRef) } - if len(recreate) == 0 { + // find new auto-connections + autochecker, err := newAutoConnectChecker(st) + if err != nil { + return err + } + + instanceName := slot.Snap.InstanceName() + candidates := m.repo.AutoConnectCandidatePlugs(instanceName, slot.Name, autochecker.check) + + var newconns []*interfaces.ConnRef + // Auto-connect the slots + for _, plug := range candidates { + // make sure slot is the only viable + // connection for plug, same check as if we were + // considering auto-connections from plug + candSlots := m.repo.AutoConnectCandidateSlots(plug.Snap.InstanceName(), plug.Name, autochecker.check) + if len(candSlots) != 1 || candSlots[0].String() != slot.String() { + crefs := make([]string, len(candSlots)) + for i, candidate := range candSlots { + crefs[i] = candidate.String() + } + task.Logf("cannot auto-connect slot %s to %s, candidates found: %s", slot, plug, strings.Join(crefs, ", ")) + continue + } + + if err := checkAutoconnectConflicts(st, task, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { + if retry, ok := err.(*state.Retry); ok { + task.Logf("hotplug connect will be retried: %s", retry.Reason) + return err // will retry + } + return fmt.Errorf("hotplug connect conflict check failed: %s", err) + } + connRef := interfaces.NewConnRef(plug, slot) + key := connRef.ID() + if _, ok := conns[key]; ok { + // existing connection, already considered by connsForDevice loop + continue + } + newconns = append(newconns, connRef) + } + + if len(recreate) == 0 && len(newconns) == 0 { return nil } - // Create connect tasks and interface hooks + // Create connect tasks and interface hooks for old connections connectTs := state.NewTaskSet() for _, conn := range recreate { - ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: conns[conn.ID()].Auto}) + wasAutoconnected := conns[conn.ID()].Auto + ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: wasAutoconnected}) if err != nil { return fmt.Errorf("internal error: connect of %q failed: %s", conn, err) } connectTs.AddAll(ts) } + // Create connect tasks and interface hooks for new auto-connections + for _, conn := range newconns { + ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: true}) + if err != nil { + return fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err) + } + connectTs.AddAll(ts) + } if len(connectTs.Tasks()) > 0 { snapstate.InjectTasks(task, connectTs) @@ -1441,67 +1494,6 @@ func (m *InterfaceManager) doHotplugDisconnect(task *state.Task, _ *tomb.Tomb) e return nil } -func (m *InterfaceManager) autoconnectNewDevice(task *state.Task, ifaceName, hotplugKey string) error { - slot, err := m.repo.SlotForHotplugKey(ifaceName, hotplugKey) - if err != nil { - return err - } - - st := task.State() - autochecker, err := newAutoConnectChecker(st) - if err != nil { - return err - } - - instanceName := slot.Snap.InstanceName() - candidates := m.repo.AutoConnectCandidatePlugs(instanceName, slot.Name, autochecker.check) - var newconns []*interfaces.ConnRef - // Auto-connect the slots - for _, plug := range candidates { - // make sure slot is the only viable - // connection for plug, same check as if we were - // considering auto-connections from plug - candSlots := m.repo.AutoConnectCandidateSlots(plug.Snap.InstanceName(), plug.Name, autochecker.check) - if len(candSlots) != 1 || candSlots[0].String() != slot.String() { - crefs := make([]string, len(candSlots)) - for i, candidate := range candSlots { - crefs[i] = candidate.String() - } - task.Logf("cannot auto-connect slot %s to %s, candidates found: %s", slot, plug, strings.Join(crefs, ", ")) - continue - } - - if err := checkAutoconnectConflicts(st, task, plug.Snap.InstanceName(), slot.Snap.InstanceName()); err != nil { - if retry, ok := err.(*state.Retry); ok { - task.Logf("hotplug connect will be retried: %s", retry.Reason) - return err // will retry - } - return fmt.Errorf("auto-connect conflict check failed: %s", err) - } - connRef := interfaces.NewConnRef(plug, slot) - newconns = append(newconns, connRef) - } - - autots := state.NewTaskSet() - // Create connect tasks and interface hooks - for _, conn := range newconns { - ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: true}) - if err != nil { - return fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err) - } - autots.AddAll(ts) - } - - if len(autots.Tasks()) > 0 { - snapstate.InjectTasks(task, autots) - - st.EnsureBefore(0) - } - - task.SetStatus(state.DoneStatus) - return nil -} - // doHotplugSeqWait returns Retry error if there is another change for same hotplug key and a lower sequence number. // Sequence numbers control the order of execution of hotplug-related changes, which would otherwise be executed in // arbitrary order by task runner, leading to unexpected results if multiple events for same device are in flight diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index b95cb298adf..59138cf8627 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -4983,13 +4983,10 @@ func (s *interfaceManagerSuite) setupHotplugConnectTestData(c *C) *state.Change coreInfo := s.mockSnap(c, coreSnapYaml) repo := s.manager(c).Repository() - err := repo.AddInterface(&ifacetest.TestInterface{ - InterfaceName: "test", - }) - c.Assert(err, IsNil) + c.Assert(repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "test"}), IsNil) // mock hotplug slot in the repo and state - err = repo.AddSlot(&snap.SlotInfo{ + err := repo.AddSlot(&snap.SlotInfo{ Snap: coreInfo, Name: "hotplugslot", Interface: "test", @@ -5056,6 +5053,96 @@ func (s *interfaceManagerSuite) TestHotplugConnect(c *C) { }}) } +func (s *interfaceManagerSuite) TestHotplugConnectIgnoresUndesired(c *C) { + s.MockModel(c, nil) + + s.state.Lock() + defer s.state.Unlock() + chg := s.setupHotplugConnectTestData(c) + + // simulate a device that was known and connected before + s.state.Set("conns", map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "undesired": true, + }}) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // no connect task created + c.Check(chg.Tasks(), HasLen, 1) + c.Assert(chg.Err(), IsNil) + + var conns map[string]interface{} + c.Assert(s.state.Get("conns", &conns), IsNil) + c.Assert(conns, DeepEquals, map[string]interface{}{ + "consumer:plug core:hotplugslot": map[string]interface{}{ + "interface": "test", + "hotplug-key": "1234", + "undesired": true, + }}) +} + +func (s *interfaceManagerSuite) TestHotplugConnectSlotMissing(c *C) { + s.MockModel(c, nil) + + repo := s.manager(c).Repository() + coreInfo := s.mockSnap(c, coreSnapYaml) + c.Assert(repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "test"}), IsNil) + c.Assert(repo.AddSlot(&snap.SlotInfo{Snap: coreInfo, Name: "slot", Interface: "test", HotplugKey: "1"}), IsNil) + + s.state.Lock() + defer s.state.Unlock() + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-connect", "") + ifacestate.SetHotplugAttrs(t, "test", "2") + chg.AddTask(t) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot find hotplug slot for interface test and hotplug key "2".*`) +} + +func (s *interfaceManagerSuite) TestHotplugConnectNothingTodo(c *C) { + s.MockModel(c, nil) + + repo := s.manager(c).Repository() + coreInfo := s.mockSnap(c, coreSnapYaml) + + iface := &ifacetest.TestInterface{InterfaceName: "test", AutoConnectCallback: func(*snap.PlugInfo, *snap.SlotInfo) bool { return false }} + c.Assert(repo.AddInterface(iface), IsNil) + c.Assert(repo.AddSlot(&snap.SlotInfo{Snap: coreInfo, Name: "hotplugslot", Interface: "test", HotplugKey: "1"}), IsNil) + + s.state.Lock() + defer s.state.Unlock() + + s.state.Set("hotplug-slots", map[string]interface{}{ + "hotplugslot": map[string]interface{}{ + "name": "hotplugslot", + "interface": "test", + "hotplug-key": "1", + }}) + + chg := s.state.NewChange("hotplug change", "") + t := s.state.NewTask("hotplug-connect", "") + ifacestate.SetHotplugAttrs(t, "test", "1") + chg.AddTask(t) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // no connect tasks created + c.Check(chg.Tasks(), HasLen, 1) + c.Assert(chg.Err(), IsNil) +} + func (s *interfaceManagerSuite) TestHotplugConnectConflictRetry(c *C) { s.MockModel(c, nil) From 1833732cb81ac1c3a56d2a5ea7581178ff48c02f Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 10 Jan 2019 14:00:46 +0100 Subject: [PATCH 233/580] cmd/snap-update-ns: break out variable expansion Signed-off-by: Zygmunt Krynicki --- cmd/snap-update-ns/export_test.go | 6 ++++-- cmd/snap-update-ns/xdg.go | 24 +++++++++++++++++------- cmd/snap-update-ns/xdg_test.go | 7 +++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cmd/snap-update-ns/export_test.go b/cmd/snap-update-ns/export_test.go index 9003385047e..36759b94d8d 100644 --- a/cmd/snap-update-ns/export_test.go +++ b/cmd/snap-update-ns/export_test.go @@ -49,9 +49,11 @@ var ( // trespassing IsReadOnly = isReadOnly IsPrivateTmpfsCreatedBySnapd = isPrivateTmpfsCreatedBySnapd + // xdg - XdgRuntimeDir = xdgRuntimeDir - ExpandXdgRuntimeDir = expandXdgRuntimeDir + XdgRuntimeDir = xdgRuntimeDir + ExpandPrefixVariable = expandPrefixVariable + ExpandXdgRuntimeDir = expandXdgRuntimeDir ) // SystemCalls encapsulates various system interactions performed by this module. diff --git a/cmd/snap-update-ns/xdg.go b/cmd/snap-update-ns/xdg.go index e2409466667..adec1648608 100644 --- a/cmd/snap-update-ns/xdg.go +++ b/cmd/snap-update-ns/xdg.go @@ -32,15 +32,25 @@ func xdgRuntimeDir(uid int) string { return fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, uid) } +// expandPrefixVariable expands variable at the beginning of a path-like string. +func expandPrefixVariable(path, variable, value string) string { + if strings.HasPrefix(path, variable) { + if len(path) == len(variable) { + return value + } + if len(path) > len(variable) && path[len(variable)] == '/' { + return strings.Replace(path, variable, value, 1) + } + } + return path +} + // expandXdgRuntimeDir expands the $XDG_RUNTIME_DIR variable in the given mount profile. func expandXdgRuntimeDir(profile *osutil.MountProfile, uid int) { - dir := xdgRuntimeDir(uid) + variable := "$XDG_RUNTIME_DIR" + value := xdgRuntimeDir(uid) for i := range profile.Entries { - if strings.HasPrefix(profile.Entries[i].Name, "$XDG_RUNTIME_DIR/") { - profile.Entries[i].Name = strings.Replace(profile.Entries[i].Name, "$XDG_RUNTIME_DIR", dir, 1) - } - if strings.HasPrefix(profile.Entries[i].Dir, "$XDG_RUNTIME_DIR/") { - profile.Entries[i].Dir = strings.Replace(profile.Entries[i].Dir, "$XDG_RUNTIME_DIR", dir, 1) - } + profile.Entries[i].Name = expandPrefixVariable(profile.Entries[i].Name, variable, value) + profile.Entries[i].Dir = expandPrefixVariable(profile.Entries[i].Dir, variable, value) } } diff --git a/cmd/snap-update-ns/xdg_test.go b/cmd/snap-update-ns/xdg_test.go index affe9bd2516..d7e3f1d11cc 100644 --- a/cmd/snap-update-ns/xdg_test.go +++ b/cmd/snap-update-ns/xdg_test.go @@ -37,6 +37,13 @@ func (s *xdgSuite) TestXdgRuntimeDir(c *C) { c.Check(update.XdgRuntimeDir(1234), Equals, "/run/user/1234") } +func (s *xdgSuite) TestExpandPrefixVariable(c *C) { + c.Check(update.ExpandPrefixVariable("$FOO", "$FOO", "/foo"), Equals, "/foo") + c.Check(update.ExpandPrefixVariable("$FOO/", "$FOO", "/foo"), Equals, "/foo/") + c.Check(update.ExpandPrefixVariable("$FOO/bar", "$FOO", "/foo"), Equals, "/foo/bar") + c.Check(update.ExpandPrefixVariable("$FOObar", "$FOO", "/foo"), Equals, "$FOObar") +} + func (s *xdgSuite) TestExpandXdgRuntimeDir(c *C) { input := "$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0\n" output := "/run/user/1234/doc/by-app/snap.foo /run/user/1234/doc none bind,rw 0 0\n" From d5ea4fcb78929bceb053a8feb320a452ab9525f3 Mon Sep 17 00:00:00 2001 From: Zygmunt Krynicki Date: Thu, 10 Jan 2019 10:44:06 +0100 Subject: [PATCH 234/580] many: remove .user-fstab files from /run/snapd/ns When snapd is being uninstalled or when tests are removing past state we now need to handle the new .user-fstab files in /run/snapd/ns. Those represent the actual state of the per-user mount namespace that is persisted when the corresponding snapd feature is enabled. The extension matches existing .user-fstab files placed in /var/lib/snapd/mount, for consistency. The files were already handled by snap-discard-ns but not by various helper scripts so here it is. Signed-off-by: Zygmunt Krynicki --- cmd/snap-mgmt/snap-mgmt.sh.in | 5 +++++ packaging/ubuntu-14.04/snapd.postrm | 5 +++++ packaging/ubuntu-16.04/snapd.postrm | 5 +++++ tests/lib/reset.sh | 1 + 4 files changed, 16 insertions(+) diff --git a/cmd/snap-mgmt/snap-mgmt.sh.in b/cmd/snap-mgmt/snap-mgmt.sh.in index b441da394bc..c02a652de72 100644 --- a/cmd/snap-mgmt/snap-mgmt.sh.in +++ b/cmd/snap-mgmt/snap-mgmt.sh.in @@ -130,6 +130,11 @@ purge() { rm -f "$fstab" done fi + if [ "$(find /run/snapd/ns/ -name "*.user-fstab" | wc -l)" -gt 0 ]; then + for fstab in /run/snapd/ns/*.user-fstab; do + rm -f "$fstab" + done + fi umount -l /run/snapd/ns/ || true fi diff --git a/packaging/ubuntu-14.04/snapd.postrm b/packaging/ubuntu-14.04/snapd.postrm index 6ad787f7abd..3f24a0999cb 100644 --- a/packaging/ubuntu-14.04/snapd.postrm +++ b/packaging/ubuntu-14.04/snapd.postrm @@ -109,6 +109,11 @@ if [ "$1" = "purge" ]; then rm -f "$fstab" done fi + if [ "$(find /run/snapd/ns/ -name "*.user-fstab" | wc -l)" -gt 0 ]; then + for fstab in /run/snapd/ns/*.user-fstab; do + rm -f "$fstab" + done + fi umount -l /run/snapd/ns/ || true fi diff --git a/packaging/ubuntu-16.04/snapd.postrm b/packaging/ubuntu-16.04/snapd.postrm index 3b5d60d427f..80d6b968aa8 100644 --- a/packaging/ubuntu-16.04/snapd.postrm +++ b/packaging/ubuntu-16.04/snapd.postrm @@ -112,6 +112,11 @@ if [ "$1" = "purge" ]; then rm -f "$fstab" done fi + if [ "$(find /run/snapd/ns/ -name "*.user-fstab" | wc -l)" -gt 0 ]; then + for fstab in /run/snapd/ns/*.user-fstab; do + rm -f "$fstab" + done + fi umount -l /run/snapd/ns/ || true fi diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh index c86fd7de1dd..11071418355 100755 --- a/tests/lib/reset.sh +++ b/tests/lib/reset.sh @@ -162,6 +162,7 @@ if [ -d /run/snapd/ns ]; then rm -f "$mnt" done rm -f /run/snapd/ns/*.fstab + rm -f /run/snapd/ns/*.user-fstab fi if [ "$REMOTE_STORE" = staging ] && [ "$1" = "--store" ]; then From e1059b1dd1dd17f4a408c46f7986d1b6f9a65fe8 Mon Sep 17 00:00:00 2001 From: "John R. Lenton" Date: Thu, 10 Jan 2019 16:11:10 +0000 Subject: [PATCH 235/580] overlord: drop old v1 store api support from managers test The fake store in `overlord/managers_test.go`'s `mgrSuite` still supported v1 details and metadata, which are no longer used. Dropping that. --- overlord/managers_test.go | 52 --------------------------------------- 1 file changed, 52 deletions(-) diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 1c2c937c489..21f9d041564 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -371,25 +371,6 @@ func fakeSnapID(name string) string { } const ( - searchHit = `{ - "anon_download_url": "@URL@", - "architecture": [ - "all" - ], - "channel": "stable", - "content": "application", - "description": "this is a description", - "developer_id": "devdevdev", - "download_url": "@URL@", - "icon_url": "@ICON@", - "origin": "bar", - "package_name": "@NAME@", - "revision": @REVISION@, - "snap_id": "@SNAPID@", - "summary": "Foo", - "version": "@VERSION@" -}` - snapV2 = `{ "architectures": [ "all" @@ -528,39 +509,6 @@ func (ms *mgrsSuite) mockStore(c *C) *httptest.Server { w.WriteHeader(200) w.Write(asserts.Encode(a)) return - case "details": - w.WriteHeader(200) - io.WriteString(w, fillHit(searchHit, comps[1])) - case "metadata": - dec := json.NewDecoder(r.Body) - var input struct { - Snaps []struct { - SnapID string `json:"snap_id"` - Revision int `json:"revision"` - } `json:"snaps"` - } - err := dec.Decode(&input) - if err != nil { - panic(err) - } - var hits []json.RawMessage - for _, s := range input.Snaps { - name := ms.serveIDtoName[s.SnapID] - if snap.R(s.Revision) == snap.R(ms.serveRevision[name]) { - continue - } - hits = append(hits, json.RawMessage(fillHit(searchHit, name))) - } - w.WriteHeader(200) - output, err := json.Marshal(map[string]interface{}{ - "_embedded": map[string]interface{}{ - "clickindex:package": hits, - }, - }) - if err != nil { - panic(err) - } - w.Write(output) case "download": if ms.hijackServeSnap != nil { ms.hijackServeSnap(w) From 0d7619e00fcb659ff570eac0845bec758bb3d779 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 10 Jan 2019 20:54:56 +0100 Subject: [PATCH 236/580] tests: review/fix the autopkgtest failures in disco * tests: skip proxy test if https{,s}_proxy is set The tinyproxy in the proxy test uses socket.socket(AF_INET) to connect o the target for https and on systems that do not allow direct connections to the outside world 443 port this will fail. * tests: skip network-retry if http{,s}_proxy is set The network-retry test may fail when http_proxy is set because it breaks DNS for the test. However if the proxy is available and defined via an IP instead of a DNS name the test will not work because the DNS is bypassed. * tests: skip snap-service-watchdog in autopkgtest Skip autopkgtest as this test is timing dependent and ADT is often very slow. Also fixes a incorrect print in the watchdog service binary: lib/snaps/test-snapd-service-watchdog/bin/direct which lead to misleading: ``` Jan 09 20:28:55 autopkgtest test-snapd-service-watchdog.direct-watchdog-ok[21555]: INFO:root:watchdog notification every 0s output ``` * address review feedback (thanks to John) * test: review feedback for network-retry (thanks to mborzecki) * tests: do not run some tests in autopkgtest We get timeouts in autopkgtest on amd64 currently so this disables some of the slower running tests in this environment. The real issue is that the cloud that runs the autopkgtest is slow dues to overcommitment it seems. But this is a reasonable workaround for now. * tests: re-enable install-store in ADT, disable snapctl-services/nfs-support --- tests/lib/snaps/test-snapd-service-watchdog/bin/direct | 2 +- tests/main/completion/task.yaml | 3 +++ tests/main/network-retry/task.yaml | 7 +++++++ tests/main/nfs-support/task.yaml | 3 +++ tests/main/parallel-install-interfaces-content/task.yaml | 3 +++ tests/main/parallel-install-services/task.yaml | 3 +++ tests/main/proxy/task.yaml | 5 +++++ tests/main/snap-service-refresh-mode/task.yaml | 3 +++ tests/main/snap-service-stop-mode/task.yaml | 3 +++ tests/main/snap-service-watchdog/task.yaml | 4 ++++ tests/main/snapctl-services/task.yaml | 3 +++ 11 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/lib/snaps/test-snapd-service-watchdog/bin/direct b/tests/lib/snaps/test-snapd-service-watchdog/bin/direct index 62f65421bed..8c15778ddde 100755 --- a/tests/lib/snaps/test-snapd-service-watchdog/bin/direct +++ b/tests/lib/snaps/test-snapd-service-watchdog/bin/direct @@ -41,7 +41,7 @@ def main(opts): else: sleep_time = watchdog_timeout / 2 - logging.info('watchdog notification every %us', sleep_time) + logging.info('watchdog notification every %ss', sleep_time) with closing(socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)) as sock: sock.connect(notify_addr) diff --git a/tests/main/completion/task.yaml b/tests/main/completion/task.yaml index 5375bf84bf7..f7b19920b69 100644 --- a/tests/main/completion/task.yaml +++ b/tests/main/completion/task.yaml @@ -3,6 +3,9 @@ summary: Check different completions # ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 systems: [-ubuntu-core-*, -ubuntu-*-ppc64el] +# takes >6min to run in total +backends: [-autopkgtest] + environment: NAMES: /var/cache/snapd/names diff --git a/tests/main/network-retry/task.yaml b/tests/main/network-retry/task.yaml index 1ad1c4affff..28046135298 100644 --- a/tests/main/network-retry/task.yaml +++ b/tests/main/network-retry/task.yaml @@ -20,6 +20,13 @@ restore: | fi execute: | + if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ] || + [ -n "${HTTPS_PROXY:-}" ] || [ -n "${HTTPS_PROXY:-}" ]; then + # all querries will go through the proxy so breaking DNS will not work + echo "SKIP: cannot run when there is a http proxy set" + exit 0 + fi + echo "Try to install a snap with broken DNS" if snap install test-snapd-tools; then echo "Installing test-snapd-tools with broken DNS should not work" diff --git a/tests/main/nfs-support/task.yaml b/tests/main/nfs-support/task.yaml index 1646b4578cb..8b92ef8e478 100644 --- a/tests/main/nfs-support/task.yaml +++ b/tests/main/nfs-support/task.yaml @@ -9,6 +9,9 @@ details: | # opensuse: the test is failing after retry several times the snapd service reaching the systemd start-limit. systems: [-ubuntu-core-*, -opensuse-*] +# takes >1.5min to run +backends: [-autopkgtest] + prepare: | #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" diff --git a/tests/main/parallel-install-interfaces-content/task.yaml b/tests/main/parallel-install-interfaces-content/task.yaml index 7e96976b175..4798c4150b5 100644 --- a/tests/main/parallel-install-interfaces-content/task.yaml +++ b/tests/main/parallel-install-interfaces-content/task.yaml @@ -1,5 +1,8 @@ summary: check that content interface works with parallel instances +# takes >1.5min to run in total +backends: [-autopkgtest] + details: | Use the content interface along with parallel instances of snaps. Verify that the content is made available to each snap instance when connected. diff --git a/tests/main/parallel-install-services/task.yaml b/tests/main/parallel-install-services/task.yaml index c7fb63c466b..0d38584be46 100644 --- a/tests/main/parallel-install-services/task.yaml +++ b/tests/main/parallel-install-services/task.yaml @@ -1,5 +1,8 @@ summary: Checks for parallel installation of sideloaded snaps containing services +# takes >3min to run +backends: [-autopkgtest] + prepare: | snap set system experimental.parallel-instances=true diff --git a/tests/main/proxy/task.yaml b/tests/main/proxy/task.yaml index 80f6b05a801..f2734293f60 100644 --- a/tests/main/proxy/task.yaml +++ b/tests/main/proxy/task.yaml @@ -12,6 +12,11 @@ execute: | echo "SKIP: need python3" exit 0 fi + if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ] || + [ -n "${HTTPS_PROXY:-}" ] || [ -n "${HTTPS_PROXY:-}" ]; then + echo "SKIP: cannot run when there is another http proxy" + exit 0 + fi systemd-run --service-type=notify --unit tinyproxy -- python3 "$TESTSLIB/tinyproxy/tinyproxy.py" # shellcheck source=tests/lib/systemd.sh diff --git a/tests/main/snap-service-refresh-mode/task.yaml b/tests/main/snap-service-refresh-mode/task.yaml index 7fa54c9f7e1..2d936ce61ec 100644 --- a/tests/main/snap-service-refresh-mode/task.yaml +++ b/tests/main/snap-service-refresh-mode/task.yaml @@ -1,5 +1,8 @@ summary: "Check that refresh-modes works" +# takes >1.5min to run +backends: [-autopkgtest] + kill-timeout: 5m restore: diff --git a/tests/main/snap-service-stop-mode/task.yaml b/tests/main/snap-service-stop-mode/task.yaml index a053caa0d8f..be1b77bd773 100644 --- a/tests/main/snap-service-stop-mode/task.yaml +++ b/tests/main/snap-service-stop-mode/task.yaml @@ -5,6 +5,9 @@ kill-timeout: 5m # journald in ubuntu-14.04 not reliable systems: [-ubuntu-14.04-*] +# takes >1.5min to run +backends: [-autopkgtest] + restore: | rm -f ./*.pid || true # remove to ensure all services are stopped diff --git a/tests/main/snap-service-watchdog/task.yaml b/tests/main/snap-service-watchdog/task.yaml index fae52f1966c..df9c1a658f9 100644 --- a/tests/main/snap-service-watchdog/task.yaml +++ b/tests/main/snap-service-watchdog/task.yaml @@ -1,5 +1,9 @@ summary: Check that snaps can use service-watchdog provided by systemd +# skip autopkgtest as this test is timing dependent and ADT is often +# very slow +backends: [-autopkgtest] + debug: | for service in direct-watchdog-ok direct-watchdog-bad; do systemctl status snap.test-snapd-service-watchdog.$service || true diff --git a/tests/main/snapctl-services/task.yaml b/tests/main/snapctl-services/task.yaml index 549c755debc..3c4be4d9bc9 100644 --- a/tests/main/snapctl-services/task.yaml +++ b/tests/main/snapctl-services/task.yaml @@ -2,6 +2,9 @@ summary: Check that own services can be controlled by snapctl kill-timeout: 5m +# takes >1.5min to run +backends: [-autopkgtest] + environment: SERVICEOPTIONFILE: /var/snap/test-snapd-service/current/service-option From 8e3cf0e2764f67a05e0b6fd9d8c4e120f53b82fd Mon Sep 17 00:00:00 2001 From: timchen119 Date: Fri, 11 Jan 2019 03:55:32 +0800 Subject: [PATCH 237/580] use major() and minor() in instead of MAJOR and MINOR * use major() and minor() in sys/sysmacros.h instead of MAJOR and MINOR macro in linux/kdev_t.h * cmd/snap-confine: use makedev instead of MKDEV We recently found (Thanks to Tim Chen) that MAJOR/MINOR/MKDEV macros are not so great and have issues with larger numbers. Instead we should use major(), minor() and makedev(). This patch completes the transition across the tree. --- cmd/snap-confine/ns-support.c | 6 +++--- cmd/snap-confine/udev-support.c | 26 ++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c index 5b91bc9e36b..d073f3c50fc 100644 --- a/cmd/snap-confine/ns-support.c +++ b/cmd/snap-confine/ns-support.c @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -33,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -249,7 +249,7 @@ static dev_t find_base_snap_device(const char *base_snap_name, sc_first_mountinfo_entry(mi); mie != NULL; mie = sc_next_mountinfo_entry(mie)) { if (sc_streq(mie->mount_dir, base_squashfs_path)) { - base_snap_dev = MKDEV(mie->dev_major, mie->dev_minor); + base_snap_dev = makedev(mie->dev_major, mie->dev_minor); debug("block device of snap %s, revision %s is %d:%d", base_snap_name, base_snap_rev, mie->dev_major, mie->dev_minor); @@ -290,7 +290,7 @@ static bool should_discard_current_ns(dev_t base_snap_dev) // measure. debug("block device of the root filesystem is %d:%d", mie->dev_major, mie->dev_minor); - return base_snap_dev != MKDEV(mie->dev_major, mie->dev_minor); + return base_snap_dev != makedev(mie->dev_major, mie->dev_minor); } die("cannot find mount entry of the root filesystem"); } diff --git a/cmd/snap-confine/udev-support.c b/cmd/snap-confine/udev-support.c index 66159a37899..aea1e35e953 100644 --- a/cmd/snap-confine/udev-support.c +++ b/cmd/snap-confine/udev-support.c @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include #include @@ -108,9 +108,7 @@ void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path) dev_t devnum = udev_device_get_devnum(d); udev_device_unref(d); - unsigned major = MAJOR(devnum); - unsigned minor = MINOR(devnum); - _run_snappy_app_dev_add_majmin(udev_s, path, major, minor); + _run_snappy_app_dev_add_majmin(udev_s, path, major(devnum), minor(devnum)); } /* @@ -264,34 +262,34 @@ void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s) break; } _run_snappy_app_dev_add_majmin(udev_s, nv_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidiactl if (stat(nvctl_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvctl_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidia-uvm if (stat(nvuvm_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvuvm_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidia-modeset if (stat(nvidia_modeset_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvidia_modeset_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/uhid isn't represented in sysfs, so add it to the device cgroup // if it exists and let AppArmor handle the mediation if (stat("/dev/uhid", &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, "/dev/uhid", - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // add the assigned devices while (udev_s->assigned != NULL) { From 47bf74076baec8f06477215c0a22b3ffb19faceb Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 10 Jan 2019 20:54:56 +0100 Subject: [PATCH 238/580] tests: review/fix the autopkgtest failures in disco * tests: skip proxy test if https{,s}_proxy is set The tinyproxy in the proxy test uses socket.socket(AF_INET) to connect o the target for https and on systems that do not allow direct connections to the outside world 443 port this will fail. * tests: skip network-retry if http{,s}_proxy is set The network-retry test may fail when http_proxy is set because it breaks DNS for the test. However if the proxy is available and defined via an IP instead of a DNS name the test will not work because the DNS is bypassed. * tests: skip snap-service-watchdog in autopkgtest Skip autopkgtest as this test is timing dependent and ADT is often very slow. Also fixes a incorrect print in the watchdog service binary: lib/snaps/test-snapd-service-watchdog/bin/direct which lead to misleading: ``` Jan 09 20:28:55 autopkgtest test-snapd-service-watchdog.direct-watchdog-ok[21555]: INFO:root:watchdog notification every 0s output ``` * address review feedback (thanks to John) * test: review feedback for network-retry (thanks to mborzecki) * tests: do not run some tests in autopkgtest We get timeouts in autopkgtest on amd64 currently so this disables some of the slower running tests in this environment. The real issue is that the cloud that runs the autopkgtest is slow dues to overcommitment it seems. But this is a reasonable workaround for now. * tests: re-enable install-store in ADT, disable snapctl-services/nfs-support --- tests/lib/snaps/test-snapd-service-watchdog/bin/direct | 2 +- tests/main/completion/task.yaml | 3 +++ tests/main/network-retry/task.yaml | 7 +++++++ tests/main/nfs-support/task.yaml | 3 +++ tests/main/parallel-install-interfaces-content/task.yaml | 3 +++ tests/main/parallel-install-services/task.yaml | 3 +++ tests/main/proxy/task.yaml | 5 +++++ tests/main/snap-service-refresh-mode/task.yaml | 3 +++ tests/main/snap-service-stop-mode/task.yaml | 3 +++ tests/main/snap-service-watchdog/task.yaml | 4 ++++ tests/main/snapctl-services/task.yaml | 3 +++ 11 files changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/lib/snaps/test-snapd-service-watchdog/bin/direct b/tests/lib/snaps/test-snapd-service-watchdog/bin/direct index 62f65421bed..8c15778ddde 100755 --- a/tests/lib/snaps/test-snapd-service-watchdog/bin/direct +++ b/tests/lib/snaps/test-snapd-service-watchdog/bin/direct @@ -41,7 +41,7 @@ def main(opts): else: sleep_time = watchdog_timeout / 2 - logging.info('watchdog notification every %us', sleep_time) + logging.info('watchdog notification every %ss', sleep_time) with closing(socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)) as sock: sock.connect(notify_addr) diff --git a/tests/main/completion/task.yaml b/tests/main/completion/task.yaml index 5375bf84bf7..f7b19920b69 100644 --- a/tests/main/completion/task.yaml +++ b/tests/main/completion/task.yaml @@ -3,6 +3,9 @@ summary: Check different completions # ppc64el disabled because of https://bugs.launchpad.net/snappy/+bug/1655594 systems: [-ubuntu-core-*, -ubuntu-*-ppc64el] +# takes >6min to run in total +backends: [-autopkgtest] + environment: NAMES: /var/cache/snapd/names diff --git a/tests/main/network-retry/task.yaml b/tests/main/network-retry/task.yaml index 1ad1c4affff..28046135298 100644 --- a/tests/main/network-retry/task.yaml +++ b/tests/main/network-retry/task.yaml @@ -20,6 +20,13 @@ restore: | fi execute: | + if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ] || + [ -n "${HTTPS_PROXY:-}" ] || [ -n "${HTTPS_PROXY:-}" ]; then + # all querries will go through the proxy so breaking DNS will not work + echo "SKIP: cannot run when there is a http proxy set" + exit 0 + fi + echo "Try to install a snap with broken DNS" if snap install test-snapd-tools; then echo "Installing test-snapd-tools with broken DNS should not work" diff --git a/tests/main/nfs-support/task.yaml b/tests/main/nfs-support/task.yaml index 1646b4578cb..8b92ef8e478 100644 --- a/tests/main/nfs-support/task.yaml +++ b/tests/main/nfs-support/task.yaml @@ -9,6 +9,9 @@ details: | # opensuse: the test is failing after retry several times the snapd service reaching the systemd start-limit. systems: [-ubuntu-core-*, -opensuse-*] +# takes >1.5min to run +backends: [-autopkgtest] + prepare: | #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" diff --git a/tests/main/parallel-install-interfaces-content/task.yaml b/tests/main/parallel-install-interfaces-content/task.yaml index 7e96976b175..4798c4150b5 100644 --- a/tests/main/parallel-install-interfaces-content/task.yaml +++ b/tests/main/parallel-install-interfaces-content/task.yaml @@ -1,5 +1,8 @@ summary: check that content interface works with parallel instances +# takes >1.5min to run in total +backends: [-autopkgtest] + details: | Use the content interface along with parallel instances of snaps. Verify that the content is made available to each snap instance when connected. diff --git a/tests/main/parallel-install-services/task.yaml b/tests/main/parallel-install-services/task.yaml index c7fb63c466b..0d38584be46 100644 --- a/tests/main/parallel-install-services/task.yaml +++ b/tests/main/parallel-install-services/task.yaml @@ -1,5 +1,8 @@ summary: Checks for parallel installation of sideloaded snaps containing services +# takes >3min to run +backends: [-autopkgtest] + prepare: | snap set system experimental.parallel-instances=true diff --git a/tests/main/proxy/task.yaml b/tests/main/proxy/task.yaml index 80f6b05a801..f2734293f60 100644 --- a/tests/main/proxy/task.yaml +++ b/tests/main/proxy/task.yaml @@ -12,6 +12,11 @@ execute: | echo "SKIP: need python3" exit 0 fi + if [ -n "${http_proxy:-}" ] || [ -n "${https_proxy:-}" ] || + [ -n "${HTTPS_PROXY:-}" ] || [ -n "${HTTPS_PROXY:-}" ]; then + echo "SKIP: cannot run when there is another http proxy" + exit 0 + fi systemd-run --service-type=notify --unit tinyproxy -- python3 "$TESTSLIB/tinyproxy/tinyproxy.py" # shellcheck source=tests/lib/systemd.sh diff --git a/tests/main/snap-service-refresh-mode/task.yaml b/tests/main/snap-service-refresh-mode/task.yaml index 7fa54c9f7e1..2d936ce61ec 100644 --- a/tests/main/snap-service-refresh-mode/task.yaml +++ b/tests/main/snap-service-refresh-mode/task.yaml @@ -1,5 +1,8 @@ summary: "Check that refresh-modes works" +# takes >1.5min to run +backends: [-autopkgtest] + kill-timeout: 5m restore: diff --git a/tests/main/snap-service-stop-mode/task.yaml b/tests/main/snap-service-stop-mode/task.yaml index a053caa0d8f..be1b77bd773 100644 --- a/tests/main/snap-service-stop-mode/task.yaml +++ b/tests/main/snap-service-stop-mode/task.yaml @@ -5,6 +5,9 @@ kill-timeout: 5m # journald in ubuntu-14.04 not reliable systems: [-ubuntu-14.04-*] +# takes >1.5min to run +backends: [-autopkgtest] + restore: | rm -f ./*.pid || true # remove to ensure all services are stopped diff --git a/tests/main/snap-service-watchdog/task.yaml b/tests/main/snap-service-watchdog/task.yaml index fae52f1966c..df9c1a658f9 100644 --- a/tests/main/snap-service-watchdog/task.yaml +++ b/tests/main/snap-service-watchdog/task.yaml @@ -1,5 +1,9 @@ summary: Check that snaps can use service-watchdog provided by systemd +# skip autopkgtest as this test is timing dependent and ADT is often +# very slow +backends: [-autopkgtest] + debug: | for service in direct-watchdog-ok direct-watchdog-bad; do systemctl status snap.test-snapd-service-watchdog.$service || true diff --git a/tests/main/snapctl-services/task.yaml b/tests/main/snapctl-services/task.yaml index 549c755debc..3c4be4d9bc9 100644 --- a/tests/main/snapctl-services/task.yaml +++ b/tests/main/snapctl-services/task.yaml @@ -2,6 +2,9 @@ summary: Check that own services can be controlled by snapctl kill-timeout: 5m +# takes >1.5min to run +backends: [-autopkgtest] + environment: SERVICEOPTIONFILE: /var/snap/test-snapd-service/current/service-option From c9b12b7ab2a7e701f1ba132d1e3739955c08e28f Mon Sep 17 00:00:00 2001 From: timchen119 Date: Fri, 11 Jan 2019 03:55:32 +0800 Subject: [PATCH 239/580] use major() and minor() in instead of MAJOR and MINOR * use major() and minor() in sys/sysmacros.h instead of MAJOR and MINOR macro in linux/kdev_t.h * cmd/snap-confine: use makedev instead of MKDEV We recently found (Thanks to Tim Chen) that MAJOR/MINOR/MKDEV macros are not so great and have issues with larger numbers. Instead we should use major(), minor() and makedev(). This patch completes the transition across the tree. --- cmd/snap-confine/ns-support.c | 6 +++--- cmd/snap-confine/udev-support.c | 26 ++++++++++++-------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cmd/snap-confine/ns-support.c b/cmd/snap-confine/ns-support.c index 5b91bc9e36b..d073f3c50fc 100644 --- a/cmd/snap-confine/ns-support.c +++ b/cmd/snap-confine/ns-support.c @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -33,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -249,7 +249,7 @@ static dev_t find_base_snap_device(const char *base_snap_name, sc_first_mountinfo_entry(mi); mie != NULL; mie = sc_next_mountinfo_entry(mie)) { if (sc_streq(mie->mount_dir, base_squashfs_path)) { - base_snap_dev = MKDEV(mie->dev_major, mie->dev_minor); + base_snap_dev = makedev(mie->dev_major, mie->dev_minor); debug("block device of snap %s, revision %s is %d:%d", base_snap_name, base_snap_rev, mie->dev_major, mie->dev_minor); @@ -290,7 +290,7 @@ static bool should_discard_current_ns(dev_t base_snap_dev) // measure. debug("block device of the root filesystem is %d:%d", mie->dev_major, mie->dev_minor); - return base_snap_dev != MKDEV(mie->dev_major, mie->dev_minor); + return base_snap_dev != makedev(mie->dev_major, mie->dev_minor); } die("cannot find mount entry of the root filesystem"); } diff --git a/cmd/snap-confine/udev-support.c b/cmd/snap-confine/udev-support.c index 66159a37899..aea1e35e953 100644 --- a/cmd/snap-confine/udev-support.c +++ b/cmd/snap-confine/udev-support.c @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include #include @@ -108,9 +108,7 @@ void run_snappy_app_dev_add(struct snappy_udev *udev_s, const char *path) dev_t devnum = udev_device_get_devnum(d); udev_device_unref(d); - unsigned major = MAJOR(devnum); - unsigned minor = MINOR(devnum); - _run_snappy_app_dev_add_majmin(udev_s, path, major, minor); + _run_snappy_app_dev_add_majmin(udev_s, path, major(devnum), minor(devnum)); } /* @@ -264,34 +262,34 @@ void setup_devices_cgroup(const char *security_tag, struct snappy_udev *udev_s) break; } _run_snappy_app_dev_add_majmin(udev_s, nv_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidiactl if (stat(nvctl_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvctl_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidia-uvm if (stat(nvuvm_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvuvm_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/nvidia-modeset if (stat(nvidia_modeset_path, &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, nvidia_modeset_path, - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // /dev/uhid isn't represented in sysfs, so add it to the device cgroup // if it exists and let AppArmor handle the mediation if (stat("/dev/uhid", &sbuf) == 0) { _run_snappy_app_dev_add_majmin(udev_s, "/dev/uhid", - MAJOR(sbuf.st_rdev), - MINOR(sbuf.st_rdev)); + major(sbuf.st_rdev), + minor(sbuf.st_rdev)); } // add the assigned devices while (udev_s->assigned != NULL) { From 4de7937eae357a9aa181194866c24abafa3f85e1 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 10 Jan 2019 20:58:42 +0100 Subject: [PATCH 240/580] overlord/configstate/configcore: support - and _ in cloud init field names * overlord/configstate/configcore: support - and _ in cloud init fields cloud-init supports both underscores and hyphens as separators in field names. Clients can use either, see the examples provided here: https://cloudinit.readthedocs.io/en/latest/topics/instancedata.html Make sure that snapd supports both both name formats too. Signed-off-by: Maciej Borzecki * overlord/configstate/configcore: tweak the logic of cloud-init integration Drop consistency checks. When clound_name is set, prefer other names with underscore. Signed-off-by: Maciej Borzecki * tests/main/cloud-init: add spread test for cloud-init integration The test makes sense when run on a backend that actually uses cloud-init and sets instance data properly, eg. GCE. Signed-off-by: Maciej Borzecki * tests/main/cloud-init: tweak the test Tweak the test to handle older versions of cloud-init (without the query command support). Make sure the test runs on all Ubuntu images where cloud-init instance data is populated (that excludes 14.04). Signed-off-by: Maciej Borzecki * tests/main/cloud-init: ensure jq is avaialble Signed-off-by: Maciej Borzecki --- overlord/configstate/configcore/cloud.go | 45 +++++++++-- overlord/configstate/configcore/cloud_test.go | 58 ++++++++++++++- tests/main/cloud-init/task.yaml | 74 +++++++++++++++++++ 3 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 tests/main/cloud-init/task.yaml diff --git a/overlord/configstate/configcore/cloud.go b/overlord/configstate/configcore/cloud.go index eb8df0172ce..06b35db7ed6 100644 --- a/overlord/configstate/configcore/cloud.go +++ b/overlord/configstate/configcore/cloud.go @@ -43,6 +43,42 @@ func alreadySeeded(tr config.Conf) (bool, error) { return seeded, nil } +type cloudInitInstanceData struct { + V1 struct { + Region string + Name string + AvailabilityZone string + } +} + +func (c *cloudInitInstanceData) UnmarshalJSON(bs []byte) error { + var instanceDataJSON struct { + V1 struct { + Region string `json:"region"` + // these fields can come with - or _ as separators + Name string `json:"cloud_name"` + AltName string `json:"cloud-name"` + AvailabilityZone string `json:"availability_zone"` + AltAvailabilityZone string `json:"availability-zone"` + } `json:"v1"` + } + + if err := json.Unmarshal(bs, &instanceDataJSON); err != nil { + return err + } + + c.V1.Region = instanceDataJSON.V1.Region + switch { + case instanceDataJSON.V1.Name != "": + c.V1.Name = instanceDataJSON.V1.Name + c.V1.AvailabilityZone = instanceDataJSON.V1.AvailabilityZone + case instanceDataJSON.V1.AltName != "": + c.V1.Name = instanceDataJSON.V1.AltName + c.V1.AvailabilityZone = instanceDataJSON.V1.AltAvailabilityZone + } + return nil +} + func setCloudInfoWhenSeeding(tr config.Conf) error { // if we are during seeding try to capture cloud information seeded, err := alreadySeeded(tr) @@ -63,13 +99,8 @@ func setCloudInfoWhenSeeding(tr config.Conf) error { logger.Noticef("cannot read cloud instance information %q: %v", dirs.CloudInstanceDataFile, err) return nil } - var instanceData struct { - V1 struct { - Name string `json:"cloud-name"` - Region string `json:"region"` - AvailabilityZone string `json:"availability-zone"` - } `json:"v1"` - } + + var instanceData cloudInitInstanceData err = json.Unmarshal(data, &instanceData) if err != nil { logger.Noticef("cannot unmarshal cloud instance information %q: %v", dirs.CloudInstanceDataFile, err) diff --git a/overlord/configstate/configcore/cloud_test.go b/overlord/configstate/configcore/cloud_test.go index 727729b8327..7994145505e 100644 --- a/overlord/configstate/configcore/cloud_test.go +++ b/overlord/configstate/configcore/cloud_test.go @@ -104,12 +104,68 @@ func (s *cloudSuite) TestHandleCloud(c *C) { "cloud-name": "none" } }`, "", "", ""}, + // both _ and - are supported + {`{ + "v1": { + "availability_zone": "nova", + "cloud_name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + {`{ + "v1": { + "availability_zone": "nova", + "availability-zone": "nova", + "cloud_name": "openstack", + "cloud-name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + // cloud_name takes precedence, if set, and other fields follow + {`{ + "v1": { + "availability_zone": "us-east-2b", + "availability-zone": "none", + "cloud_name": "aws", + "cloud_name": "aws", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "aws", "", "us-east-2b"}, + {`{ + "v1": { + "availability_zone": "nova", + "availability-zone": "gibberish", + "cloud_name": "openstack", + "cloud-name": "aws", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + {`{ + "v1": { + "availability_zone": "gibberish", + "availability-zone": "nova", + "cloud_name": null, + "cloud-name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, } err := os.MkdirAll(filepath.Dir(dirs.CloudInstanceDataFile), 0755) c.Assert(err, IsNil) - for _, t := range tests { + for i, t := range tests { + c.Logf("tc: %v", i) os.Remove(dirs.CloudInstanceDataFile) if t.instData != "" { err = ioutil.WriteFile(dirs.CloudInstanceDataFile, []byte(t.instData), 0600) diff --git a/tests/main/cloud-init/task.yaml b/tests/main/cloud-init/task.yaml new file mode 100644 index 00000000000..e50785775d0 --- /dev/null +++ b/tests/main/cloud-init/task.yaml @@ -0,0 +1,74 @@ +summary: Ensure that cloud-init integration works + +description: | + snapd picks up basic cloud information from the host and makes it available + to the snaps. Run the test on a live backend which sets instance data + properly. + +# GCE backend sets instance data +backends: [google] + +prepare: | + if ! command -v jq; then + snap install --devmode jq + fi + +execute: | + if [[ ! -e /run/cloud-init/instance-data.json ]]; then + echo "cloud-init instance data is required to execute the test" + + if [[ "$SPREAD_SYSTEM" == ubuntu-* && "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + # we expect the test to run on all Ubuntu images, excluding 14.04 + echo "the test expected to run on $SPREAD_SYSTEM" + exit 1 + fi + exit 0 + fi + + get_conf() { + # we could use cloud-init query , but that requires cloud-init 18.4+ + # which is not available in all images we use + local kname="$1" + if jq -r '.v1 | keys[]' < /run/cloud-init/instance-data.json | grep -q _; then + kname=${kname/-/_} + else + kname=${kname/_/-} + fi + + jq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json + } + # GCE sets the following in Ubuntu images: + # { + # ... + # "v1": { + # "availability-zone": "us-east1-b", + # "availability_zone": "us-east1-b", + # "cloud-name": "gce", + # "cloud_name": "gce", + # "region": "us-east1" + # ... + # } + # } + + # keys can be queried only using underscore names + cloud_name=$(get_conf cloud_name) + test -n "$cloud_name" + # this shouldn't happen under GCE + if [[ "$cloud_name" == "nocloud" || "$cloud_name" == "none" ]]; then + echo "not a cloud instance, config should be empty" + + nocloud=$(snap get core -d | jq -r '.cloud') + test "$nocloud" = null + exit 0 + fi + + snap_cloud_name=$(snap get core cloud.name) + test "$cloud_name" = "$snap_cloud_name" + + cloud_avzone=$(get_conf availability_zone) + snap_cloud_avzone=$(snap get core cloud.availability-zone) + test "$cloud_avzone" = "$snap_cloud_avzone" + + cloud_region=$(get_conf region) + snap_cloud_region=$(snap get core cloud.region) + test "$cloud_region" = "$snap_cloud_region" From a35595df32710c75ae50bace28061a6ad71e6713 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Thu, 10 Jan 2019 20:58:42 +0100 Subject: [PATCH 241/580] overlord/configstate/configcore: support - and _ in cloud init field names * overlord/configstate/configcore: support - and _ in cloud init fields cloud-init supports both underscores and hyphens as separators in field names. Clients can use either, see the examples provided here: https://cloudinit.readthedocs.io/en/latest/topics/instancedata.html Make sure that snapd supports both both name formats too. Signed-off-by: Maciej Borzecki * overlord/configstate/configcore: tweak the logic of cloud-init integration Drop consistency checks. When clound_name is set, prefer other names with underscore. Signed-off-by: Maciej Borzecki * tests/main/cloud-init: add spread test for cloud-init integration The test makes sense when run on a backend that actually uses cloud-init and sets instance data properly, eg. GCE. Signed-off-by: Maciej Borzecki * tests/main/cloud-init: tweak the test Tweak the test to handle older versions of cloud-init (without the query command support). Make sure the test runs on all Ubuntu images where cloud-init instance data is populated (that excludes 14.04). Signed-off-by: Maciej Borzecki * tests/main/cloud-init: ensure jq is avaialble Signed-off-by: Maciej Borzecki --- overlord/configstate/configcore/cloud.go | 45 +++++++++-- overlord/configstate/configcore/cloud_test.go | 58 ++++++++++++++- tests/main/cloud-init/task.yaml | 74 +++++++++++++++++++ 3 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 tests/main/cloud-init/task.yaml diff --git a/overlord/configstate/configcore/cloud.go b/overlord/configstate/configcore/cloud.go index eb8df0172ce..06b35db7ed6 100644 --- a/overlord/configstate/configcore/cloud.go +++ b/overlord/configstate/configcore/cloud.go @@ -43,6 +43,42 @@ func alreadySeeded(tr config.Conf) (bool, error) { return seeded, nil } +type cloudInitInstanceData struct { + V1 struct { + Region string + Name string + AvailabilityZone string + } +} + +func (c *cloudInitInstanceData) UnmarshalJSON(bs []byte) error { + var instanceDataJSON struct { + V1 struct { + Region string `json:"region"` + // these fields can come with - or _ as separators + Name string `json:"cloud_name"` + AltName string `json:"cloud-name"` + AvailabilityZone string `json:"availability_zone"` + AltAvailabilityZone string `json:"availability-zone"` + } `json:"v1"` + } + + if err := json.Unmarshal(bs, &instanceDataJSON); err != nil { + return err + } + + c.V1.Region = instanceDataJSON.V1.Region + switch { + case instanceDataJSON.V1.Name != "": + c.V1.Name = instanceDataJSON.V1.Name + c.V1.AvailabilityZone = instanceDataJSON.V1.AvailabilityZone + case instanceDataJSON.V1.AltName != "": + c.V1.Name = instanceDataJSON.V1.AltName + c.V1.AvailabilityZone = instanceDataJSON.V1.AltAvailabilityZone + } + return nil +} + func setCloudInfoWhenSeeding(tr config.Conf) error { // if we are during seeding try to capture cloud information seeded, err := alreadySeeded(tr) @@ -63,13 +99,8 @@ func setCloudInfoWhenSeeding(tr config.Conf) error { logger.Noticef("cannot read cloud instance information %q: %v", dirs.CloudInstanceDataFile, err) return nil } - var instanceData struct { - V1 struct { - Name string `json:"cloud-name"` - Region string `json:"region"` - AvailabilityZone string `json:"availability-zone"` - } `json:"v1"` - } + + var instanceData cloudInitInstanceData err = json.Unmarshal(data, &instanceData) if err != nil { logger.Noticef("cannot unmarshal cloud instance information %q: %v", dirs.CloudInstanceDataFile, err) diff --git a/overlord/configstate/configcore/cloud_test.go b/overlord/configstate/configcore/cloud_test.go index 727729b8327..7994145505e 100644 --- a/overlord/configstate/configcore/cloud_test.go +++ b/overlord/configstate/configcore/cloud_test.go @@ -104,12 +104,68 @@ func (s *cloudSuite) TestHandleCloud(c *C) { "cloud-name": "none" } }`, "", "", ""}, + // both _ and - are supported + {`{ + "v1": { + "availability_zone": "nova", + "cloud_name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + {`{ + "v1": { + "availability_zone": "nova", + "availability-zone": "nova", + "cloud_name": "openstack", + "cloud-name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + // cloud_name takes precedence, if set, and other fields follow + {`{ + "v1": { + "availability_zone": "us-east-2b", + "availability-zone": "none", + "cloud_name": "aws", + "cloud_name": "aws", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "aws", "", "us-east-2b"}, + {`{ + "v1": { + "availability_zone": "nova", + "availability-zone": "gibberish", + "cloud_name": "openstack", + "cloud-name": "aws", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, + {`{ + "v1": { + "availability_zone": "gibberish", + "availability-zone": "nova", + "cloud_name": null, + "cloud-name": "openstack", + "instance-id": "b5", + "local-hostname": "b5", + "region": null + } +}`, "openstack", "", "nova"}, } err := os.MkdirAll(filepath.Dir(dirs.CloudInstanceDataFile), 0755) c.Assert(err, IsNil) - for _, t := range tests { + for i, t := range tests { + c.Logf("tc: %v", i) os.Remove(dirs.CloudInstanceDataFile) if t.instData != "" { err = ioutil.WriteFile(dirs.CloudInstanceDataFile, []byte(t.instData), 0600) diff --git a/tests/main/cloud-init/task.yaml b/tests/main/cloud-init/task.yaml new file mode 100644 index 00000000000..e50785775d0 --- /dev/null +++ b/tests/main/cloud-init/task.yaml @@ -0,0 +1,74 @@ +summary: Ensure that cloud-init integration works + +description: | + snapd picks up basic cloud information from the host and makes it available + to the snaps. Run the test on a live backend which sets instance data + properly. + +# GCE backend sets instance data +backends: [google] + +prepare: | + if ! command -v jq; then + snap install --devmode jq + fi + +execute: | + if [[ ! -e /run/cloud-init/instance-data.json ]]; then + echo "cloud-init instance data is required to execute the test" + + if [[ "$SPREAD_SYSTEM" == ubuntu-* && "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + # we expect the test to run on all Ubuntu images, excluding 14.04 + echo "the test expected to run on $SPREAD_SYSTEM" + exit 1 + fi + exit 0 + fi + + get_conf() { + # we could use cloud-init query , but that requires cloud-init 18.4+ + # which is not available in all images we use + local kname="$1" + if jq -r '.v1 | keys[]' < /run/cloud-init/instance-data.json | grep -q _; then + kname=${kname/-/_} + else + kname=${kname/_/-} + fi + + jq -r ".[\"v1\"][\"$kname\"]" < /run/cloud-init/instance-data.json + } + # GCE sets the following in Ubuntu images: + # { + # ... + # "v1": { + # "availability-zone": "us-east1-b", + # "availability_zone": "us-east1-b", + # "cloud-name": "gce", + # "cloud_name": "gce", + # "region": "us-east1" + # ... + # } + # } + + # keys can be queried only using underscore names + cloud_name=$(get_conf cloud_name) + test -n "$cloud_name" + # this shouldn't happen under GCE + if [[ "$cloud_name" == "nocloud" || "$cloud_name" == "none" ]]; then + echo "not a cloud instance, config should be empty" + + nocloud=$(snap get core -d | jq -r '.cloud') + test "$nocloud" = null + exit 0 + fi + + snap_cloud_name=$(snap get core cloud.name) + test "$cloud_name" = "$snap_cloud_name" + + cloud_avzone=$(get_conf availability_zone) + snap_cloud_avzone=$(snap get core cloud.availability-zone) + test "$cloud_avzone" = "$snap_cloud_avzone" + + cloud_region=$(get_conf region) + snap_cloud_region=$(snap get core cloud.region) + test "$cloud_region" = "$snap_cloud_region" From e034be60ee13ed2c1a8bed0801b6f7d5521cbde8 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 10 Jan 2019 20:59:47 +0100 Subject: [PATCH 242/580] release 2.37~pre1 --- packaging/ubuntu-16.04/changelog | 361 +++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 47c89e3f28a..3fc3cff343e 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,364 @@ +snapd (2.37~pre1) xenial; urgency=medium + + * New upstream release, LP: #1811233 + - systemd: allow only a single daemon-reload at the same time + - cmd/snap: only auto-enable unicode to a tty + - cmd/snap: right-align revision and size in info's channel map + - dirs, interfaces/builtin/desktop: system fontconfig cache path is + different on Fedora + - tests: fix "No space left on device" issue on amazon-linux + - store: undo workaround for timezone-less released-at + - store, snap, cmd/snap: channels have released-at + - snap-confine: fix incorrect use "src" var in mount-support.c + - release: support probing SELinux state + - release-tools: display self-help + - interface: add new `{personal,system}-files` interface + - snap: give Epoch an Equal method + - many: remove unused interface code + - interfaces/many: use 'unsafe' with docker-support change_profile + rules + - run-checks: stop running HEAD of staticcheck + - release: use sync.Once around lazy intialized state + - overlord/ifacestate: include interface name in the hotplug- + disconnect task summary + - spread: show free space in debug output + - cmd/snap: attempt to restore SELinux context of snap user + directories + - image: do not write empty etc/cloud + - tests: skip snapd snap on reset for core systems + - cmd/snap-discard-ns: fix umount(2) typo + - overlord/ifacestate: hotplug-remove-slot task handler + - overlord/ifacestate: handler for hotplug-disconnect task + - ifacestate/hotplug: updateDevice helper + - tests: reset snapd state on tests restore + - interfaces: return security setup errors + - overlord: make InstallMany work like UpdateMany, issuing a single + request to get candidates + - systemd/systemd.go: add missing tests for systemd.IsActive + - overlord/ifacestate: addHotplugSeqWaitTask helper + - cmd/snap-confine: refactor call to snap-update-ns --user-mounts + - tests: new backend used to run upgrade test suite + - travis: short circuit failures in static and unit tests travis job + - cmd: automatically fix localized