diff options
99 files changed, 2247 insertions, 850 deletions
diff --git a/asserts/asserts.go b/asserts/asserts.go index 13e01f575d..de556c1d88 100644 --- a/asserts/asserts.go +++ b/asserts/asserts.go @@ -227,11 +227,11 @@ func parseHeaders(head []byte) (map[string]string, error) { // BODY can be arbitrary, // SIGNATURE is the signature // -// A header entry for a single line value (no "\n" in it) looks like: +// A header entry for a single line value (no '\n' in it) looks like: // // NAME ": " VALUE // -// A header entry for a multiline value (a value with "\n"s in it) looks like: +// A header entry for a multiline value (a value with '\n's in it) looks like: // // NAME ":\n" 1-space indented VALUE // @@ -240,12 +240,17 @@ func parseHeaders(head []byte) (map[string]string, error) { // type // authority-id (the signer id) // +// Further for a given assertion type all the primary key headers +// must be non empty and must not contain '/'. +// // The following headers expect integer values and if omitted // otherwise are assumed to be 0: // // revision (a positive int) // body-length (expected to be equal to the length of BODY) // +// Typically list values in headers are expected to be comma separated. +// Times are expected to be in the RFC3339 format: "2006-01-02T15:04:05Z07:00". func Decode(serializedAssertion []byte) (Assertion, error) { // copy to get an independent backstorage that can't be mutated later assertionSnapshot := make([]byte, len(serializedAssertion)) diff --git a/daemon/api_test.go b/daemon/api_test.go index 6759747bc2..1b8ddc2a2f 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -67,7 +67,7 @@ type apiSuite struct { var _ = check.Suite(&apiSuite{}) -func (s *apiSuite) Snap(name, channel string, auther store.Authenticator) (*snap.Info, error) { +func (s *apiSuite) Snap(name, channel string, devmode bool, auther store.Authenticator) (*snap.Info, error) { s.auther = auther if len(s.rsnaps) > 0 { return s.rsnaps[0], s.err diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index 15da96539c..c925107bc8 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -23,6 +23,8 @@ import ( "net" "net/http" "net/http/httptest" + "os" + "path/filepath" "testing" "time" @@ -42,6 +44,8 @@ var _ = check.Suite(&daemonSuite{}) func (s *daemonSuite) SetUpTest(c *check.C) { dirs.SetRootDir(c.MkDir()) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, check.IsNil) } func (s *daemonSuite) TearDownTest(c *check.C) { diff --git a/dirs/dirs.go b/dirs/dirs.go index 652cd4991a..79ed033f53 100644 --- a/dirs/dirs.go +++ b/dirs/dirs.go @@ -42,10 +42,13 @@ var ( SnapMetaDir string SnapdSocket string + SnapSeedDir string + SnapAssertsDBDir string SnapTrustedAccountKey string - SnapStateFile string + SnapStateFile string + SnapFirstBootStamp string SnapBinariesDir string SnapServicesDir string @@ -96,6 +99,12 @@ func SetRootDir(rootdir string) { SnapStateFile = filepath.Join(rootdir, snappyDir, "state.json") + SnapSeedDir = filepath.Join(rootdir, snappyDir, "seed") + + // NOTE: if you change stampFile, update the condition in + // snapd.firstboot.service to match + SnapFirstBootStamp = filepath.Join(rootdir, snappyDir, "firstboot", "stamp") + SnapBinariesDir = filepath.Join(SnapSnapsDir, "bin") SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system") SnapBusPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d") diff --git a/docs/interfaces.md b/docs/interfaces.md index d56e1f4a24..ade4610fe4 100644 --- a/docs/interfaces.md +++ b/docs/interfaces.md @@ -64,7 +64,8 @@ Auto-Connect: yes Can access non-hidden files in user's `$HOME` to read/write/lock. This is restricted because it gives file access to the user's -`$HOME`. +`$HOME`. This interface is auto-connected on classic systems and +manually connected on non-classic. Usage: reserved Auto-Connect: yes @@ -78,6 +79,32 @@ allows adjusting settings of other applications. Usage: reserved Auto-Connect: yes +### optical-drive + +Can access the first optical drive in read-only mode. Suitable for CD/DVD playback. + +Usage: common +Auto-Connect: yes + +### mpris + +Can access media players implementing the Media Player Remote Interfacing +Specification (mpris) when the interface is specified as a plug. + +Media players implementing mpris can be accessed by connected clients when +specified as a slot. + +Usage: common +Auto-Connect: no + +### camera + +Can access the first video camera. Suitable for programs wanting to use the +webcams. + +Usage: common +Auto-Connect: no + ## Supported Interfaces - Advanced ### cups-control diff --git a/firstboot/firstboot.go b/firstboot/firstboot.go new file mode 100644 index 0000000000..1ab05c65f8 --- /dev/null +++ b/firstboot/firstboot.go @@ -0,0 +1,80 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package firstboot + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func HasRun() bool { + return osutil.FileExists(dirs.SnapFirstBootStamp) +} + +func StampFirstBoot() error { + // filepath.Dir instead of firstbootDir directly to ease testing + stampDir := filepath.Dir(dirs.SnapFirstBootStamp) + + if _, err := os.Stat(stampDir); os.IsNotExist(err) { + if err := os.MkdirAll(stampDir, 0755); err != nil { + return err + } + } + + return osutil.AtomicWriteFile(dirs.SnapFirstBootStamp, []byte{}, 0644, 0) +} + +var globs = []string{"/sys/class/net/eth*", "/sys/class/net/en*"} +var ethdir = "/etc/network/interfaces.d" +var ifup = "/sbin/ifup" + +func EnableFirstEther() error { + var eths []string + for _, glob := range globs { + eths, _ = filepath.Glob(glob) + if len(eths) != 0 { + break + } + } + if len(eths) == 0 { + return nil + } + eth := filepath.Base(eths[0]) + ethfile := filepath.Join(ethdir, eth) + data := fmt.Sprintf("allow-hotplug %[1]s\niface %[1]s inet dhcp\n", eth) + + if err := osutil.AtomicWriteFile(ethfile, []byte(data), 0644, 0); err != nil { + return err + } + + ifup := exec.Command(ifup, eth) + ifup.Stdout = os.Stdout + ifup.Stderr = os.Stderr + if err := ifup.Run(); err != nil { + return err + } + + return nil +} diff --git a/firstboot/firstboot_test.go b/firstboot/firstboot_test.go new file mode 100644 index 0000000000..2205ad753c --- /dev/null +++ b/firstboot/firstboot_test.go @@ -0,0 +1,95 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package firstboot + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" +) + +func TestStore(t *testing.T) { TestingT(t) } + +type FirstBootTestSuite struct { + globs []string + ethdir string + ifup string + e error +} + +var _ = Suite(&FirstBootTestSuite{}) + +func (s *FirstBootTestSuite) SetUpTest(c *C) { + tempdir := c.MkDir() + dirs.SetRootDir(tempdir) + + s.globs = globs + globs = nil + s.ethdir = ethdir + ethdir = c.MkDir() + s.ifup = ifup + ifup = "/bin/true" + + s.e = nil +} + +func (s *FirstBootTestSuite) TearDownTest(c *C) { + globs = s.globs + ethdir = s.ethdir + ifup = s.ifup +} + +func (s *FirstBootTestSuite) TestEnableFirstEther(c *C) { + c.Check(EnableFirstEther(), IsNil) + fs, _ := filepath.Glob(filepath.Join(ethdir, "*")) + c.Assert(fs, HasLen, 0) +} + +func (s *FirstBootTestSuite) TestEnableFirstEtherSomeEth(c *C) { + dir := c.MkDir() + _, err := os.Create(filepath.Join(dir, "eth42")) + c.Assert(err, IsNil) + + globs = []string{filepath.Join(dir, "eth*")} + c.Check(EnableFirstEther(), IsNil) + fs, _ := filepath.Glob(filepath.Join(ethdir, "*")) + c.Assert(fs, HasLen, 1) + bs, err := ioutil.ReadFile(fs[0]) + c.Assert(err, IsNil) + c.Check(string(bs), Equals, "allow-hotplug eth42\niface eth42 inet dhcp\n") + +} + +func (s *FirstBootTestSuite) TestEnableFirstEtherBadEthDir(c *C) { + dir := c.MkDir() + _, err := os.Create(filepath.Join(dir, "eth42")) + c.Assert(err, IsNil) + + ethdir = "/no/such/thing" + globs = []string{filepath.Join(dir, "eth*")} + err = EnableFirstEther() + c.Check(err, NotNil) + c.Check(os.IsNotExist(err), Equals, true) +} diff --git a/integration-tests/tests/home_interface_test.go b/integration-tests/tests/home_interface_test.go index adc9cd5845..64c7657564 100644 --- a/integration-tests/tests/home_interface_test.go +++ b/integration-tests/tests/home_interface_test.go @@ -27,6 +27,7 @@ import ( "github.com/snapcore/snapd/integration-tests/testutils/cli" "github.com/snapcore/snapd/integration-tests/testutils/data" + "github.com/snapcore/snapd/release" "gopkg.in/check.v1" ) @@ -36,7 +37,9 @@ var _ = check.Suite(&homeInterfaceSuite{ sampleSnaps: []string{data.HomeConsumerSnapName}, slot: "home", plug: "home-consumer", - autoconnect: true}}) + // we only auto-connect on classic + autoconnect: release.OnClassic, + }}) type homeInterfaceSuite struct { interfaceSuite diff --git a/integration-tests/tests/log_observe_interface_test.go b/integration-tests/tests/log_observe_interface_test.go deleted file mode 100644 index 1021bbc5e6..0000000000 --- a/integration-tests/tests/log_observe_interface_test.go +++ /dev/null @@ -1,51 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -// +build !excludeintegration - -/* - * 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 <http://www.gnu.org/licenses/>. - * - */ - -package tests - -import ( - "gopkg.in/check.v1" - - "github.com/snapcore/snapd/integration-tests/testutils/cli" - "github.com/snapcore/snapd/integration-tests/testutils/data" -) - -var _ = check.Suite(&logObserveInterfaceSuite{ - interfaceSuite: interfaceSuite{ - sampleSnaps: []string{data.LogObserveConsumerSnapName}, - slot: "log-observe", - plug: "log-observe-consumer"}}) - -type logObserveInterfaceSuite struct { - interfaceSuite -} - -func (s *logObserveInterfaceSuite) TestConnectedPlugAllowsLogObserve(c *check.C) { - cli.ExecCommand(c, "sudo", "snap", "connect", - s.plug+":"+s.slot, "ubuntu-core:"+s.slot) - - output := cli.ExecCommand(c, "log-observe-consumer") - c.Assert(output, check.Equals, "ok\n") -} - -func (s *logObserveInterfaceSuite) TestDisconnectedPlugDisablesLogObserve(c *check.C) { - output := cli.ExecCommand(c, "log-observe-consumer") - c.Assert(output, check.Equals, "tail: cannot open '/var/log/syslog' for reading: Permission denied\nerror accessing log\n") -} diff --git a/integration-tests/tests/network_bind_interface_test.go b/integration-tests/tests/network_bind_interface_test.go deleted file mode 100644 index a3fa4ec9b4..0000000000 --- a/integration-tests/tests/network_bind_interface_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -// +build !excludeintegration - -/* - * 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 <http://www.gnu.org/licenses/>. - * - */ - -package tests - -import ( - "gopkg.in/check.v1" - - "github.com/snapcore/snapd/integration-tests/testutils/cli" - "github.com/snapcore/snapd/integration-tests/testutils/data" - "github.com/snapcore/snapd/integration-tests/testutils/wait" -) - -const providerURL = "http://127.0.0.1:8081" - -var _ = check.Suite(&networkBindInterfaceSuite{ - interfaceSuite: interfaceSuite{ - sampleSnaps: []string{data.NetworkBindConsumerSnapName, data.NetworkConsumerSnapName}, - slot: "network-bind", - plug: "network-bind-consumer", - autoconnect: true}}) - -type networkBindInterfaceSuite struct { - interfaceSuite -} - -func (s *networkBindInterfaceSuite) TestPlugDisconnectionDisablesClientConnection(c *check.C) { - wait.ForActiveService(c, "snap.network-bind-consumer.network-consumer.service") - - output := cli.ExecCommand(c, "network-consumer", providerURL) - c.Assert(output, check.Equals, "ok\n") - - cli.ExecCommand(c, "sudo", "snap", "disconnect", - s.plug+":"+s.slot, "ubuntu-core:"+s.slot) - - output = cli.ExecCommand(c, "snap", "interfaces") - c.Assert(output, check.Matches, disconnectedPattern(s.slot, s.plug)) - - output, err := cli.ExecCommandErr("network-consumer", providerURL) - c.Assert(err, check.NotNil) -} diff --git a/integration-tests/tests/network_interface_test.go b/integration-tests/tests/network_interface_test.go deleted file mode 100644 index 80b6f315f2..0000000000 --- a/integration-tests/tests/network_interface_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -// +build !excludeintegration - -/* - * 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 <http://www.gnu.org/licenses/>. - * - */ - -package tests - -import ( - "github.com/snapcore/snapd/integration-tests/testutils/cli" - "github.com/snapcore/snapd/integration-tests/testutils/data" - - "gopkg.in/check.v1" -) - -var _ = check.Suite(&networkInterfaceSuite{ - interfaceSuite: interfaceSuite{ - sampleSnaps: []string{data.NetworkBindConsumerSnapName, data.NetworkConsumerSnapName}, - slot: "network", - plug: "network-consumer", - autoconnect: true}}) - -type networkInterfaceSuite struct { - interfaceSuite -} - -func (s *networkInterfaceSuite) TestPlugDisconnectionDisablesFunctionality(c *check.C) { - providerURL := "http://127.0.0.1:8081" - - output := cli.ExecCommand(c, "network-consumer", providerURL) - c.Assert(output, check.Equals, "ok\n") - - cli.ExecCommand(c, "sudo", "snap", "disconnect", - s.plug+":"+s.slot, "ubuntu-core:"+s.slot) - - output = cli.ExecCommand(c, "snap", "interfaces") - c.Assert(output, check.Matches, disconnectedPattern(s.slot, s.plug)) - - output, err := cli.ExecCommandErr("network-consumer", providerURL) - c.Check(err, check.NotNil) -} diff --git a/integration-tests/tests/snap_install_test.go b/integration-tests/tests/snap_install_test.go deleted file mode 100644 index 49e83c9890..0000000000 --- a/integration-tests/tests/snap_install_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -// +build !excludeintegration - -/* - * Copyright (C) 2015, 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 <http://www.gnu.org/licenses/>. - * - */ - -package tests - -import ( - "fmt" - "os" - - "github.com/snapcore/snapd/integration-tests/testutils/build" - "github.com/snapcore/snapd/integration-tests/testutils/cli" - "github.com/snapcore/snapd/integration-tests/testutils/common" - "github.com/snapcore/snapd/integration-tests/testutils/data" - - "gopkg.in/check.v1" -) - -var _ = check.Suite(&installSuite{}) - -type installSuite struct { - common.SnappySuite -} - -func (s *installSuite) TestCallFailBinaryFromInstalledSnap(c *check.C) { - c.Skip("port to snapd") - - snapPath, err := build.LocalSnap(c, data.BasicBinariesSnapName) - defer os.Remove(snapPath) - c.Assert(err, check.IsNil, check.Commentf("Error building local snap: %s", err)) - common.InstallSnap(c, snapPath) - defer common.RemoveSnap(c, data.BasicBinariesSnapName) - - _, err = cli.ExecCommandErr("basic-binaries.fail") - c.Assert(err, check.NotNil, check.Commentf("The binary did not fail")) -} - -// SNAP_INSTALL_004: with already installed snap name and same version -func (s *installSuite) TestInstallWithAlreadyInstalledSnapAndSameVersionMustFail(c *check.C) { - snapName := "hello-world" - - common.InstallSnap(c, snapName) - defer common.RemoveSnap(c, snapName) - - expected := fmt.Sprintf(`error: cannot install "%s": snap "%[1]s" already installed\n`, snapName) - actual, err := cli.ExecCommandErr("sudo", "snap", "install", snapName) - - c.Assert(err, check.NotNil) - c.Assert(actual, check.Matches, expected) -} diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index 204b9b39bf..0e892814e6 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -127,33 +127,45 @@ var ( // backend delegates writing those files to higher layers. func (b *Backend) combineSnippets(snapInfo *snap.Info, devMode bool, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { for _, appInfo := range snapInfo.Apps { - policy := defaultTemplate - if devMode { - policy = attachPattern.ReplaceAll(policy, attachComplain) - } - policy = templatePattern.ReplaceAllFunc(policy, func(placeholder []byte) []byte { - switch { - case bytes.Equal(placeholder, placeholderVar): - return templateVariables(appInfo) - case bytes.Equal(placeholder, placeholderProfileAttach): - return []byte(fmt.Sprintf("profile \"%s\"", appInfo.SecurityTag())) - case bytes.Equal(placeholder, placeholderSnippets): - return bytes.Join(snippets[appInfo.Name], []byte("\n")) - } - return nil - }) if content == nil { content = make(map[string]*osutil.FileState) } - fname := appInfo.SecurityTag() - content[fname] = &osutil.FileState{ - Content: policy, - Mode: 0644, + addContent(appInfo.SecurityTag(), snapInfo, devMode, snippets, content) + } + + for _, hookInfo := range snapInfo.Hooks { + if content == nil { + content = make(map[string]*osutil.FileState) } + addContent(hookInfo.SecurityTag(), snapInfo, devMode, snippets, content) } + return content, nil } +func addContent(securityTag string, snapInfo *snap.Info, devMode bool, snippets map[string][][]byte, content map[string]*osutil.FileState) { + policy := defaultTemplate + if devMode { + policy = attachPattern.ReplaceAll(policy, attachComplain) + } + policy = templatePattern.ReplaceAllFunc(policy, func(placeholder []byte) []byte { + switch { + case bytes.Equal(placeholder, placeholderVar): + return templateVariables(snapInfo) + case bytes.Equal(placeholder, placeholderProfileAttach): + return []byte(fmt.Sprintf("profile \"%s\"", securityTag)) + case bytes.Equal(placeholder, placeholderSnippets): + return bytes.Join(snippets[securityTag], []byte("\n")) + } + return nil + }) + + content[securityTag] = &osutil.FileState{ + Content: policy, + Mode: 0644, + } +} + func reloadProfiles(profiles []string) error { for _, profile := range profiles { fname := filepath.Join(dirs.SnapAppArmorDir, profile) diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 5cae8caec8..d81ab4c9cf 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -114,6 +114,20 @@ func (s *backendSuite) TestInstallingSnapWritesAndLoadsProfiles(c *C) { }) } +func (s *backendSuite) TestInstallingSnapWithHookWritesAndLoadsProfiles(c *C) { + devMode := false + s.InstallSnap(c, devMode, backendtest.HookYaml, 1) + profile := filepath.Join(dirs.SnapAppArmorDir, "snap.foo.hook.test-hook") + + // Verify that profile "snap.foo.hook.test-hook" was created + _, err := os.Stat(profile) + c.Check(err, IsNil) + // apparmor_parser was used to load that file + c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), profile}, + }) +} + func (s *backendSuite) TestProfilesAreAlwaysLoaded(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1, 1) @@ -148,6 +162,26 @@ func (s *backendSuite) TestRemovingSnapRemovesAndUnloadsProfiles(c *C) { } } +func (s *backendSuite) TestRemovingSnapWithHookRemovesAndUnloadsProfiles(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.HookYaml, 1) + s.parserCmd.ForgetCalls() + s.RemoveSnap(c, snapInfo) + profile := filepath.Join(dirs.SnapAppArmorDir, "snap.foo.hook.test-hook") + // file called "snap.foo.hook.test-hook" was removed + _, err := os.Stat(profile) + c.Check(os.IsNotExist(err), Equals, true) + // apparmor cache file was removed + cache := filepath.Join(dirs.AppArmorCacheDir, "snap.foo.hook.test-hook") + _, err = os.Stat(cache) + c.Check(os.IsNotExist(err), Equals, true) + // apparmor_parser was used to unload the profile + c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--remove", "snap.foo.hook.test-hook"}, + }) + } +} + func (s *backendSuite) TestUpdatingSnapMakesNeccesaryChanges(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1, 1) @@ -183,6 +217,29 @@ func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) { } } +func (s *backendSuite) TestUpdatingSnapToOneWithMoreHooks(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1WithNmbd, 1) + s.parserCmd.ForgetCalls() + // NOTE: the revision is kept the same to just test on the new application being added + snapInfo = s.UpdateSnap(c, snapInfo, devMode, backendtest.SambaYamlWithHook, 1) + smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") + nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd") + hookProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.hook.test-hook") + + // Verify that profile "snap.samba.hook.test-hook" was created + _, err := os.Stat(hookProfile) + c.Check(err, IsNil) + // apparmor_parser was used to load the both profiles + c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), hookProfile}, + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), nmbdProfile}, + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile}, + }) + s.RemoveSnap(c, snapInfo) + } +} + func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1WithNmbd, 1) @@ -203,6 +260,29 @@ func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) { } } +func (s *backendSuite) TestUpdatingSnapToOneWithFewerHooks(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlWithHook, 1) + s.parserCmd.ForgetCalls() + // NOTE: the revision is kept the same to just test on the application being removed + snapInfo = s.UpdateSnap(c, snapInfo, devMode, backendtest.SambaYamlV1WithNmbd, 1) + smbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.smbd") + nmbdProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.nmbd") + hookProfile := filepath.Join(dirs.SnapAppArmorDir, "snap.samba.hook.test-hook") + + // Verify profile "snap.samba.hook.test-hook" was removed + _, err := os.Stat(hookProfile) + c.Check(os.IsNotExist(err), Equals, true) + // apparmor_parser was used to remove the unused profile + c.Check(s.parserCmd.Calls(), DeepEquals, [][]string{ + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), nmbdProfile}, + {"apparmor_parser", "--replace", "--write-cache", "-O", "no-expr-simplify", fmt.Sprintf("--cache-loc=%s/var/cache/apparmor", s.RootDir), smbdProfile}, + {"apparmor_parser", "--remove", "snap.samba.hook.test-hook"}, + }) + s.RemoveSnap(c, snapInfo) + } +} + func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) { snapInfo, err := snap.InfoFromSnapYaml([]byte(backendtest.SambaYamlV1)) c.Assert(err, IsNil) @@ -230,7 +310,6 @@ type combineSnippetsScenario struct { } const commonPrefix = ` -@{APP_NAME}="smbd" @{SNAP_NAME}="samba" @{SNAP_REVISION}="1" @{INSTALL_DIR}="/snap"` diff --git a/interfaces/apparmor/template_vars.go b/interfaces/apparmor/template_vars.go index d000649193..2a2eea02d1 100644 --- a/interfaces/apparmor/template_vars.go +++ b/interfaces/apparmor/template_vars.go @@ -28,11 +28,10 @@ import ( // templateVariables returns text defining apparmor variables that can be used in the // apparmor template and by apparmor snippets. -func templateVariables(appInfo *snap.AppInfo) []byte { +func templateVariables(info *snap.Info) []byte { var buf bytes.Buffer - fmt.Fprintf(&buf, "@{APP_NAME}=\"%s\"\n", appInfo.Name) - fmt.Fprintf(&buf, "@{SNAP_NAME}=\"%s\"\n", appInfo.Snap.Name()) - fmt.Fprintf(&buf, "@{SNAP_REVISION}=\"%s\"\n", appInfo.Snap.Revision) + fmt.Fprintf(&buf, "@{SNAP_NAME}=\"%s\"\n", info.Name()) + fmt.Fprintf(&buf, "@{SNAP_REVISION}=\"%s\"\n", info.Revision) fmt.Fprintf(&buf, "@{INSTALL_DIR}=\"/snap\"") return buf.Bytes() } diff --git a/interfaces/backendtest/backendtest.go b/interfaces/backendtest/backendtest.go index 33631ab59f..c0c1a95720 100644 --- a/interfaces/backendtest/backendtest.go +++ b/interfaces/backendtest/backendtest.go @@ -79,6 +79,26 @@ apps: slots: iface: ` +const SambaYamlWithHook = ` +name: samba +apps: + smbd: + nmbd: +hooks: + test-hook: + plugs: [iface] +slots: + iface: +` +const HookYaml = ` +name: foo +version: 1 +developer: acme +hooks: + test-hook: +plugs: + iface: +` // Support code for tests diff --git a/interfaces/builtin/all.go b/interfaces/builtin/all.go index 6443759a63..e761acb486 100644 --- a/interfaces/builtin/all.go +++ b/interfaces/builtin/all.go @@ -29,6 +29,7 @@ var allInterfaces = []interfaces.Interface{ &LocationControlInterface{}, &LocationObserveInterface{}, &ModemManagerInterface{}, + &MprisInterface{}, &NetworkManagerInterface{}, &PppInterface{}, &SerialPortInterface{}, @@ -51,6 +52,8 @@ var allInterfaces = []interfaces.Interface{ NewOpenglInterface(), NewPulseAudioInterface(), NewCupsControlInterface(), + NewOpticalDriveInterface(), + NewCameraInterface(), } // Interfaces returns all of the built-in interfaces. diff --git a/interfaces/builtin/all_test.go b/interfaces/builtin/all_test.go index 3290fcc245..f64ee88d22 100644 --- a/interfaces/builtin/all_test.go +++ b/interfaces/builtin/all_test.go @@ -36,6 +36,7 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, Contains, &builtin.BluezInterface{}) c.Check(all, Contains, &builtin.LocationControlInterface{}) c.Check(all, Contains, &builtin.LocationObserveInterface{}) + c.Check(all, Contains, &builtin.MprisInterface{}) c.Check(all, Contains, &builtin.SerialPortInterface{}) c.Check(all, DeepContains, builtin.NewFirewallControlInterface()) c.Check(all, DeepContains, builtin.NewGsettingsInterface()) @@ -56,4 +57,6 @@ func (s *AllSuite) TestInterfaces(c *C) { c.Check(all, DeepContains, builtin.NewOpenglInterface()) c.Check(all, DeepContains, builtin.NewPulseAudioInterface()) c.Check(all, DeepContains, builtin.NewCupsControlInterface()) + c.Check(all, DeepContains, builtin.NewOpticalDriveInterface()) + c.Check(all, DeepContains, builtin.NewCameraInterface()) } diff --git a/integration-tests/tests/snap_interfaces_test.go b/interfaces/builtin/camera.go index 7cee2099cb..c3cce4765e 100644 --- a/integration-tests/tests/snap_interfaces_test.go +++ b/interfaces/builtin/camera.go @@ -1,5 +1,4 @@ // -*- Mode: Go; indent-tabs-mode: t -*- -// +build !excludeintegration /* * Copyright (C) 2016 Canonical Ltd @@ -18,30 +17,21 @@ * */ -package tests +package builtin import ( - "fmt" - - "gopkg.in/check.v1" - - "github.com/snapcore/snapd/integration-tests/testutils/cli" - "github.com/snapcore/snapd/integration-tests/testutils/common" + "github.com/snapcore/snapd/interfaces" ) -var _ = check.Suite(&interfacesCliTest{}) - -type interfacesCliTest struct { - common.SnappySuite -} - -// SNAP_INTERFACES_006: snap interfaces -i=<slot> -func (s *interfacesCliTest) TestFilterBySlot(c *check.C) { - plug := "network" - - expected := fmt.Sprintf("Slot +Plug\n:%s +-\n", plug) - - actual := cli.ExecCommand(c, "snap", "interfaces", "-i", plug) - - c.Assert(actual, check.Matches, expected) +const cameraConnectedPlugAppArmor = ` +/dev/video0 rw, +` + +// NewCameraInterface returns a new "camera" interface. +func NewCameraInterface() interfaces.Interface { + return &commonInterface{ + name: "camera", + connectedPlugAppArmor: cameraConnectedPlugAppArmor, + reservedForOS: true, + } } diff --git a/interfaces/builtin/home.go b/interfaces/builtin/home.go index ca33b2be75..7ffd61045d 100644 --- a/interfaces/builtin/home.go +++ b/interfaces/builtin/home.go @@ -21,6 +21,7 @@ package builtin import ( "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/release" ) // http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/view/head:/data/apparmor/policygroups/ubuntu-core/16.04/home @@ -41,6 +42,10 @@ owner @{HOME}/sn[^a]** rwk, owner @{HOME}/sna[^p]** rwk, # allow creating a few files not caught above owner @{HOME}/{s,sn,sna}{,/} rwk, + +# allow access to gvfs mounts (only allow writes to files, not mount point) +owner /run/user/[0-9]*/gvfs/** r, +owner /run/user/[0-9]*/gvfs/*/** w, ` // NewHomeInterface returns a new "home" interface. @@ -49,6 +54,6 @@ func NewHomeInterface() interfaces.Interface { name: "home", connectedPlugAppArmor: homeConnectedPlugAppArmor, reservedForOS: true, - autoConnect: true, + autoConnect: release.OnClassic, } } diff --git a/interfaces/builtin/home_test.go b/interfaces/builtin/home_test.go index 829523aca5..0cf03d7223 100644 --- a/interfaces/builtin/home_test.go +++ b/interfaces/builtin/home_test.go @@ -24,6 +24,7 @@ import ( "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" ) @@ -123,6 +124,16 @@ func (s *HomeInterfaceSuite) TestUnexpectedSecuritySystems(c *C) { c.Assert(snippet, IsNil) } -func (s *HomeInterfaceSuite) TestAutoConnect(c *C) { - c.Check(s.iface.AutoConnect(), Equals, true) +func (s *HomeInterfaceSuite) TestAutoConnectOnClassic(c *C) { + restore := release.MockOnClassic(true) + defer restore() + iface := builtin.NewHomeInterface() + c.Check(iface.AutoConnect(), Equals, true) +} + +func (s *HomeInterfaceSuite) TestAutoConnectOnCore(c *C) { + restore := release.MockOnClassic(false) + defer restore() + iface := builtin.NewHomeInterface() + c.Check(iface.AutoConnect(), Equals, false) } diff --git a/interfaces/builtin/mpris.go b/interfaces/builtin/mpris.go new file mode 100644 index 0000000000..e7ba838d3d --- /dev/null +++ b/interfaces/builtin/mpris.go @@ -0,0 +1,234 @@ +// -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "bytes" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/release" +) + +var mprisPermanentSlotAppArmor = []byte(` +# Description: Allow operating as an MPRIS player. +# Usage: common + +# DBus accesses +#include <abstractions/dbus-session-strict> + +# https://specifications.freedesktop.org/mpris-spec/latest/ +# allow binding to the well-known DBus mpris interface based on the snap's name +dbus (bind) + bus=session + name="org.mpris.MediaPlayer2.@{SNAP_NAME}{,.*}", + +# register as a player +dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member="{Request,Release}Name" + peer=(name=org.freedesktop.DBus, label=unconfined), + +dbus (send) + bus=system + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus + member="GetConnectionUnix{ProcessID,User}" + peer=(name=org.freedesktop.DBus, label=unconfined), + +dbus (send) + bus=session + path=/org/mpris/MediaPlayer2 + interface=org.freedesktop.DBus.Properties + member="{GetAll,PropertiesChanged}" + peer=(name=org.freedesktop.DBus, label=unconfined), + +dbus (send) + bus=session + path=/org/mpris/MediaPlayer2 + interface="org.mpris.MediaPlayer2{,.Player}" + peer=(name=org.freedesktop.DBus, label=unconfined), + +# we can always connect to ourselves +dbus (receive) + bus=session + path=/org/mpris/MediaPlayer2 + peer=(label=@{profile_name}), +`) + +var mprisConnectedSlotAppArmor = []byte(` +# Allow connected clients to interact with the player +dbus (receive) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/mpris/MediaPlayer2 + peer=(label=###PLUG_SECURITY_TAGS###), +dbus (receive) + bus=session + interface=org.freedesktop.DBus.Introspectable + path="/{,org,org/mpris,org/mpris/MediaPlayer2}" + peer=(label=###PLUG_SECURITY_TAGS###), + +dbus (receive) + bus=session + interface="org.mpris.MediaPlayer2{,.*}" + path=/org/mpris/MediaPlayer2 + peer=(label=###PLUG_SECURITY_TAGS###), +`) + +var mprisConnectedSlotAppArmorClassic = []byte(` +# Allow unconfined clients to interact with the player on classic +dbus (receive) + bus=session + path=/org/mpris/MediaPlayer2 + peer=(label=unconfined), +`) + +var mprisConnectedPlugAppArmor = []byte(` +# Description: Allow connecting to an MPRIS player. +# Usage: common + +#include <abstractions/dbus-session-strict> + +# Find the mpris player +dbus (send) + bus=session + path=/org/freedesktop/DBus + interface=org.freedesktop.DBus.Introspectable + peer=(name="org.freedesktop.DBus", label="unconfined"), +dbus (send) + bus=session + path=/{,org,org/mpris,org/mpris/MediaPlayer2} + interface=org.freedesktop.DBus.Introspectable + peer=(name="org.freedesktop.DBus", label="unconfined"), +# This reveals all names on the session bus +dbus (send) + bus=session + path=/ + interface=org.freedesktop.DBus + member=ListNames + peer=(name="org.freedesktop.DBus", label="unconfined"), + +# Communicate with the mpris player +dbus (send) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/mpris/MediaPlayer2 + peer=(label=###SLOT_SECURITY_TAGS###), +dbus (send) + bus=session + path=/org/mpris/MediaPlayer2 + peer=(label=###SLOT_SECURITY_TAGS###), +`) + +var mprisPermanentSlotSecComp = []byte(` +getsockname +recvmsg +sendmsg +sendto +`) + +var mprisConnectedPlugSecComp = []byte(` +getsockname +recvmsg +sendmsg +sendto +`) + +type MprisInterface struct{} + +func (iface *MprisInterface) Name() string { + return "mpris" +} + +func (iface *MprisInterface) PermanentPlugSnippet(plug *interfaces.Plug, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityDBus, interfaces.SecurityAppArmor, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount: + return nil, nil + default: + return nil, interfaces.ErrUnknownSecurity + } +} + +func (iface *MprisInterface) ConnectedPlugSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + old := []byte("###SLOT_SECURITY_TAGS###") + new := slotAppLabelExpr(slot) + snippet := bytes.Replace(mprisConnectedPlugAppArmor, old, new, -1) + return snippet, nil + case interfaces.SecurityDBus: + return nil, nil + case interfaces.SecuritySecComp: + return mprisConnectedPlugSecComp, nil + case interfaces.SecurityUDev: + return nil, nil + default: + return nil, interfaces.ErrUnknownSecurity + } +} + +func (iface *MprisInterface) PermanentSlotSnippet(slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + snippet := mprisPermanentSlotAppArmor + // on classic, allow unconfined remotes to control the player + // (eg, indicator-sound) + if release.OnClassic { + snippet = append(snippet, mprisConnectedSlotAppArmorClassic...) + } + return snippet, nil + case interfaces.SecurityDBus: + return nil, nil + case interfaces.SecuritySecComp: + return mprisPermanentSlotSecComp, nil + case interfaces.SecurityUDev: + return nil, nil + default: + return nil, interfaces.ErrUnknownSecurity + } +} + +func (iface *MprisInterface) ConnectedSlotSnippet(plug *interfaces.Plug, slot *interfaces.Slot, securitySystem interfaces.SecuritySystem) ([]byte, error) { + switch securitySystem { + case interfaces.SecurityAppArmor: + old := []byte("###PLUG_SECURITY_TAGS###") + new := plugAppLabelExpr(plug) + snippet := bytes.Replace(mprisConnectedSlotAppArmor, old, new, -1) + return snippet, nil + case interfaces.SecurityDBus, interfaces.SecuritySecComp, interfaces.SecurityUDev, interfaces.SecurityMount: + return nil, nil + default: + return nil, interfaces.ErrUnknownSecurity + } +} + +func (iface *MprisInterface) SanitizePlug(slot *interfaces.Plug) error { + return nil +} + +func (iface *MprisInterface) SanitizeSlot(slot *interfaces.Slot) error { + return nil +} + +func (iface *MprisInterface) AutoConnect() bool { + return true +} diff --git a/interfaces/builtin/mpris_test.go b/interfaces/builtin/mpris_test.go new file mode 100644 index 0000000000..29032d0a7e --- /dev/null +++ b/interfaces/builtin/mpris_test.go @@ -0,0 +1,230 @@ +// -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +package builtin_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type MprisInterfaceSuite struct { + iface interfaces.Interface + slot *interfaces.Slot + plug *interfaces.Plug +} + +var _ = Suite(&MprisInterfaceSuite{ + iface: &builtin.MprisInterface{}, + slot: &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "mpris"}, + Name: "mpris-player", + Interface: "mpris", + }, + }, + plug: &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "mpris"}, + Name: "mpris-client", + Interface: "mpris", + }, + }, +}) + +func (s *MprisInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "mpris") +} + +// The label glob when all apps are bound to the mpris slot +func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelAll(c *C) { + app1 := &snap.AppInfo{Name: "app1"} + app2 := &snap.AppInfo{Name: "app2"} + slot := &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + } + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.*"),`) +} + +// The label uses alternation when some, but not all, apps is bound to the mpris slot +func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelSome(c *C) { + app1 := &snap.AppInfo{Name: "app1"} + app2 := &snap.AppInfo{Name: "app2"} + app3 := &snap.AppInfo{Name: "app3"} + slot := &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + } + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.{app1,app2}"),`) +} + +// The label uses short form when exactly one app is bound to the mpris slot +func (s *MprisInterfaceSuite) TestConnectedPlugSnippetUsesSlotLabelOne(c *C) { + app := &snap.AppInfo{Name: "app"} + slot := &interfaces.Slot{ + SlotInfo: &snap.SlotInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app": app}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app": app}, + }, + } + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.app"),`) +} + +// The label glob when all apps are bound to the mpris plug +func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelAll(c *C) { + app1 := &snap.AppInfo{Name: "app1"} + app2 := &snap.AppInfo{Name: "app2"} + plug := &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + } + snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.*"),`) +} + +// The label uses alternation when some, but not all, apps is bound to the mpris plug +func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelSome(c *C) { + app1 := &snap.AppInfo{Name: "app1"} + app2 := &snap.AppInfo{Name: "app2"} + app3 := &snap.AppInfo{Name: "app3"} + plug := &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2, "app3": app3}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app1": app1, "app2": app2}, + }, + } + snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.{app1,app2}"),`) +} + +// The label uses short form when exactly one app is bound to the mpris plug +func (s *MprisInterfaceSuite) TestConnectedSlotSnippetUsesPlugLabelOne(c *C) { + app := &snap.AppInfo{Name: "app"} + plug := &interfaces.Plug{ + PlugInfo: &snap.PlugInfo{ + Snap: &snap.Info{ + SuggestedName: "mpris", + Apps: map[string]*snap.AppInfo{"app": app}, + }, + Name: "mpris", + Interface: "mpris", + Apps: map[string]*snap.AppInfo{"app": app}, + }, + } + snippet, err := s.iface.ConnectedSlotSnippet(plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(string(snippet), testutil.Contains, `peer=(label="snap.mpris.app"),`) +} + +func (s *MprisInterfaceSuite) TestUnusedSecuritySystems(c *C) { + systems := [...]interfaces.SecuritySystem{interfaces.SecuritySecComp, + interfaces.SecurityDBus, interfaces.SecurityUDev} + for _, system := range systems { + snippet, err := s.iface.PermanentPlugSnippet(s.plug, system) + c.Assert(err, IsNil) + c.Assert(snippet, IsNil) + snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, system) + c.Assert(err, IsNil) + c.Assert(snippet, IsNil) + } + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, interfaces.SecurityUDev) + c.Assert(err, IsNil) + c.Assert(snippet, IsNil) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, interfaces.SecurityUDev) + c.Assert(err, IsNil) + c.Assert(snippet, IsNil) + snippet, err = s.iface.PermanentPlugSnippet(s.plug, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, IsNil) +} + +func (s *MprisInterfaceSuite) TestUsedSecuritySystems(c *C) { + systems := [...]interfaces.SecuritySystem{interfaces.SecurityAppArmor, + interfaces.SecuritySecComp} + for _, system := range systems { + snippet, err := s.iface.ConnectedPlugSnippet(s.plug, s.slot, system) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, system) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) + } + snippet, err := s.iface.ConnectedSlotSnippet(s.plug, s.slot, interfaces.SecurityAppArmor) + c.Assert(err, IsNil) + c.Assert(snippet, Not(IsNil)) +} + +func (s *MprisInterfaceSuite) TestUnexpectedSecuritySystems(c *C) { + snippet, err := s.iface.PermanentPlugSnippet(s.plug, "foo") + c.Assert(err, Equals, interfaces.ErrUnknownSecurity) + c.Assert(snippet, IsNil) + snippet, err = s.iface.ConnectedPlugSnippet(s.plug, s.slot, "foo") + c.Assert(err, Equals, interfaces.ErrUnknownSecurity) + c.Assert(snippet, IsNil) + snippet, err = s.iface.PermanentSlotSnippet(s.slot, "foo") + c.Assert(err, Equals, interfaces.ErrUnknownSecurity) + c.Assert(snippet, IsNil) + snippet, err = s.iface.ConnectedSlotSnippet(s.plug, s.slot, "foo") + c.Assert(err, Equals, interfaces.ErrUnknownSecurity) + c.Assert(snippet, IsNil) +} diff --git a/interfaces/builtin/optical_drive.go b/interfaces/builtin/optical_drive.go new file mode 100644 index 0000000000..5b9e5dbc7d --- /dev/null +++ b/interfaces/builtin/optical_drive.go @@ -0,0 +1,39 @@ +// -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +package builtin + +import ( + "github.com/snapcore/snapd/interfaces" +) + +const opticalDriveConnectedPlugAppArmor = ` +/dev/sr0 r, +/dev/scd0 r, +` + +// NewOpticalDriveInterface returns a new "optical-drive" interface. +func NewOpticalDriveInterface() interfaces.Interface { + return &commonInterface{ + name: "optical-drive", + connectedPlugAppArmor: opticalDriveConnectedPlugAppArmor, + reservedForOS: true, + autoConnect: true, + } +} diff --git a/interfaces/dbus/backend.go b/interfaces/dbus/backend.go index 2f14041ea9..0f92166eeb 100644 --- a/interfaces/dbus/backend.go +++ b/interfaces/dbus/backend.go @@ -89,7 +89,8 @@ func (b *Backend) Remove(snapName string) error { // affecting a given snap into a content map applicable to EnsureDirState. func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { for _, appInfo := range snapInfo.Apps { - appSnippets := snippets[appInfo.Name] + securityTag := appInfo.SecurityTag() + appSnippets := snippets[securityTag] if len(appSnippets) == 0 { continue } @@ -103,7 +104,7 @@ func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]b if content == nil { content = make(map[string]*osutil.FileState) } - fname := fmt.Sprintf("%s.conf", appInfo.SecurityTag()) + fname := fmt.Sprintf("%s.conf", securityTag) content[fname] = &osutil.FileState{Content: buf.Bytes(), Mode: 0644} } return content, nil diff --git a/interfaces/mount/backend.go b/interfaces/mount/backend.go index 6e953d9a10..cea7f33ca7 100644 --- a/interfaces/mount/backend.go +++ b/interfaces/mount/backend.go @@ -89,7 +89,8 @@ func (b *Backend) Remove(snapName string) error { // affecting a given snap into a content map applicable to EnsureDirState. func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { for _, appInfo := range snapInfo.Apps { - appSnippets := snippets[appInfo.Name] + securityTag := appInfo.SecurityTag() + appSnippets := snippets[securityTag] if len(appSnippets) == 0 { continue } @@ -101,7 +102,7 @@ func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]b if content == nil { content = make(map[string]*osutil.FileState) } - fname := fmt.Sprintf("%s.fstab", appInfo.SecurityTag()) + fname := fmt.Sprintf("%s.fstab", securityTag) content[fname] = &osutil.FileState{Content: buf.Bytes(), Mode: 0644} } return content, nil diff --git a/interfaces/naming.go b/interfaces/naming.go index 7c41ad409b..e1f303ee91 100644 --- a/interfaces/naming.go +++ b/interfaces/naming.go @@ -20,11 +20,11 @@ package interfaces import ( - "fmt" + "github.com/snapcore/snapd/snap" ) // SecurityTagGlob returns a pattern that matches all security tags belonging to // the same snap as the given app. func SecurityTagGlob(snapName string) string { - return fmt.Sprintf("snap.%s.%s", snapName, "*") + return snap.AppSecurityTag(snapName, "*") } diff --git a/interfaces/repo.go b/interfaces/repo.go index 97f1ef704c..3f774e3978 100644 --- a/interfaces/repo.go +++ b/interfaces/repo.go @@ -451,8 +451,8 @@ func (r *Repository) Interfaces() *Interfaces { } // SecuritySnippetsForSnap collects all of the snippets of a given security -// system that affect a given snap. The return value is indexed by app name -// within that snap. +// system that affect a given snap. The return value is indexed by app/hook +// security tag within that snap. func (r *Repository) SecuritySnippetsForSnap(snapName string, securitySystem SecuritySystem) (map[string][][]byte, error) { r.m.Lock() defer r.m.Unlock() @@ -472,7 +472,8 @@ func (r *Repository) securitySnippetsForSnap(snapName string, securitySystem Sec } if snippet != nil { for appName := range slot.Apps { - snippets[appName] = append(snippets[appName], snippet) + securityTag := snap.AppSecurityTag(snapName, appName) + snippets[securityTag] = append(snippets[securityTag], snippet) } } // Add connection-specific snippet specific to each plug @@ -485,7 +486,8 @@ func (r *Repository) securitySnippetsForSnap(snapName string, securitySystem Sec continue } for appName := range slot.Apps { - snippets[appName] = append(snippets[appName], snippet) + securityTag := snap.AppSecurityTag(snapName, appName) + snippets[securityTag] = append(snippets[securityTag], snippet) } } } @@ -499,7 +501,12 @@ func (r *Repository) securitySnippetsForSnap(snapName string, securitySystem Sec } if snippet != nil { for appName := range plug.Apps { - snippets[appName] = append(snippets[appName], snippet) + securityTag := snap.AppSecurityTag(snapName, appName) + snippets[securityTag] = append(snippets[securityTag], snippet) + } + for hookName := range plug.Hooks { + securityTag := snap.HookSecurityTag(snapName, hookName) + snippets[securityTag] = append(snippets[securityTag], snippet) } } // Add connection-specific snippet specific to each slot @@ -512,7 +519,12 @@ func (r *Repository) securitySnippetsForSnap(snapName string, securitySystem Sec continue } for appName := range plug.Apps { - snippets[appName] = append(snippets[appName], snippet) + securityTag := snap.AppSecurityTag(snapName, appName) + snippets[securityTag] = append(snippets[securityTag], snippet) + } + for hookName := range plug.Hooks { + securityTag := snap.HookSecurityTag(snapName, hookName) + snippets[securityTag] = append(snippets[securityTag], snippet) } } } diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index 88acba632d..3aa1a63c42 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -49,6 +49,8 @@ func (s *RepositorySuite) SetUpTest(c *C) { name: consumer apps: app: +hooks: + test-hook: plugs: plug: interface: interface @@ -61,6 +63,8 @@ plugs: name: producer apps: app: +hooks: + test-hook: slots: slot: interface: interface @@ -767,14 +771,17 @@ func (s *RepositorySuite) TestSlotSnippetsForSnapSuccess(c *C) { snippets, err := repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity) c.Assert(err, IsNil) c.Check(snippets, DeepEquals, map[string][][]byte{ - "app": [][]byte{ + "snap.consumer.app": [][]byte{ + []byte(`static plug snippet`), + }, + "snap.consumer.hook.test-hook": [][]byte{ []byte(`static plug snippet`), }, }) snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity) c.Assert(err, IsNil) c.Check(snippets, DeepEquals, map[string][][]byte{ - "app": [][]byte{ + "snap.producer.app": [][]byte{ []byte(`static slot snippet`), }, }) @@ -784,7 +791,11 @@ func (s *RepositorySuite) TestSlotSnippetsForSnapSuccess(c *C) { snippets, err = repo.SecuritySnippetsForSnap(s.plug.Snap.Name(), testSecurity) c.Assert(err, IsNil) c.Check(snippets, DeepEquals, map[string][][]byte{ - "app": [][]byte{ + "snap.consumer.app": [][]byte{ + []byte(`static plug snippet`), + []byte(`connection-specific plug snippet`), + }, + "snap.consumer.hook.test-hook": [][]byte{ []byte(`static plug snippet`), []byte(`connection-specific plug snippet`), }, @@ -792,7 +803,7 @@ func (s *RepositorySuite) TestSlotSnippetsForSnapSuccess(c *C) { snippets, err = repo.SecuritySnippetsForSnap(s.slot.Snap.Name(), testSecurity) c.Assert(err, IsNil) c.Check(snippets, DeepEquals, map[string][][]byte{ - "app": [][]byte{ + "snap.producer.app": [][]byte{ []byte(`static slot snippet`), []byte(`connection-specific slot snippet`), }, diff --git a/interfaces/seccomp/backend.go b/interfaces/seccomp/backend.go index 491f11f5bd..6d66c4d4b9 100644 --- a/interfaces/seccomp/backend.go +++ b/interfaces/seccomp/backend.go @@ -95,24 +95,37 @@ func (b *Backend) Remove(snapName string) error { // affecting a given snap into a content map applicable to EnsureDirState. func (b *Backend) combineSnippets(snapInfo *snap.Info, devMode bool, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { for _, appInfo := range snapInfo.Apps { - var buf bytes.Buffer - if devMode { - // NOTE: This is going to be understood by ubuntu-core-launcher - buf.WriteString("@complain\n") - } - buf.Write(defaultTemplate) - for _, snippet := range snippets[appInfo.Name] { - buf.Write(snippet) - buf.WriteRune('\n') - } if content == nil { content = make(map[string]*osutil.FileState) } - fname := appInfo.SecurityTag() - content[fname] = &osutil.FileState{ - Content: buf.Bytes(), - Mode: 0644, + addContent(appInfo.SecurityTag(), devMode, snippets, content) + } + + for _, hookInfo := range snapInfo.Hooks { + if content == nil { + content = make(map[string]*osutil.FileState) } + addContent(hookInfo.SecurityTag(), devMode, snippets, content) } + return content, nil } + +func addContent(securityTag string, devMode bool, snippets map[string][][]byte, content map[string]*osutil.FileState) { + var buffer bytes.Buffer + if devMode { + // NOTE: This is understood by ubuntu-core-launcher + buffer.WriteString("@complain\n") + } + + buffer.Write(defaultTemplate) + for _, snippet := range snippets[securityTag] { + buffer.Write(snippet) + buffer.WriteRune('\n') + } + + content[securityTag] = &osutil.FileState{ + Content: buffer.Bytes(), + Mode: 0644, + } +} diff --git a/interfaces/seccomp/backend_test.go b/interfaces/seccomp/backend_test.go index d30b8a903a..079bb9fd04 100644 --- a/interfaces/seccomp/backend_test.go +++ b/interfaces/seccomp/backend_test.go @@ -68,6 +68,16 @@ func (s *backendSuite) TestInstallingSnapWritesProfiles(c *C) { c.Check(err, IsNil) } +func (s *backendSuite) TestInstallingSnapWritesHookProfiles(c *C) { + devMode := false + s.InstallSnap(c, devMode, backendtest.HookYaml, 0) + profile := filepath.Join(dirs.SnapSeccompDir, "snap.foo.hook.test-hook") + + // Verify that profile named "snap.foo.hook.test-hook" was created. + _, err := os.Stat(profile) + c.Check(err, IsNil) +} + func (s *backendSuite) TestRemovingSnapRemovesProfiles(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1, 0) @@ -79,6 +89,18 @@ func (s *backendSuite) TestRemovingSnapRemovesProfiles(c *C) { } } +func (s *backendSuite) TestRemovingSnapRemovesHookProfiles(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.HookYaml, 0) + s.RemoveSnap(c, snapInfo) + profile := filepath.Join(dirs.SnapSeccompDir, "snap.foo.hook.test-hook") + + // Verify that profile "snap.foo.hook.test-hook" was removed. + _, err := os.Stat(profile) + c.Check(os.IsNotExist(err), Equals, true) + } +} + func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1, 0) @@ -91,6 +113,19 @@ func (s *backendSuite) TestUpdatingSnapToOneWithMoreApps(c *C) { } } +func (s *backendSuite) TestUpdatingSnapToOneWithHooks(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1, 0) + snapInfo = s.UpdateSnap(c, snapInfo, devMode, backendtest.SambaYamlWithHook, 0) + profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.hook.test-hook") + + // Verify that profile "snap.samba.hook.test-hook" was created. + _, err := os.Stat(profile) + c.Check(err, IsNil) + s.RemoveSnap(c, snapInfo) + } +} + func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) { for _, devMode := range []bool{true, false} { snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlV1WithNmbd, 0) @@ -103,6 +138,19 @@ func (s *backendSuite) TestUpdatingSnapToOneWithFewerApps(c *C) { } } +func (s *backendSuite) TestUpdatingSnapToOneWithNoHooks(c *C) { + for _, devMode := range []bool{true, false} { + snapInfo := s.InstallSnap(c, devMode, backendtest.SambaYamlWithHook, 0) + snapInfo = s.UpdateSnap(c, snapInfo, devMode, backendtest.SambaYamlV1, 0) + profile := filepath.Join(dirs.SnapSeccompDir, "snap.samba.hook.test-hook") + + // Verify that profile snap.samba.hook.test-hook was removed. + _, err := os.Stat(profile) + c.Check(os.IsNotExist(err), Equals, true) + s.RemoveSnap(c, snapInfo) + } +} + func (s *backendSuite) TestRealDefaultTemplateIsNormallyUsed(c *C) { snapInfo, err := snap.InfoFromSnapYaml([]byte(backendtest.SambaYamlV1)) c.Assert(err, IsNil) diff --git a/interfaces/seccomp/template.go b/interfaces/seccomp/template.go index f5c5497df0..05d825edd1 100644 --- a/interfaces/seccomp/template.go +++ b/interfaces/seccomp/template.go @@ -479,4 +479,9 @@ writev pwrite pwrite64 pwritev + +# FIXME: remove this after LP: #1446748 is implemented +# This is an older interface and single entry point that can be used instead +# of socket(), bind(), connect(), etc individually. +socketcall `) diff --git a/interfaces/udev/backend.go b/interfaces/udev/backend.go index a230f80b99..9376a6f264 100644 --- a/interfaces/udev/backend.go +++ b/interfaces/udev/backend.go @@ -95,7 +95,8 @@ func ensureDirState(dir, glob string, content map[string]*osutil.FileState, snap // affecting a given snap into a content map applicable to EnsureDirState. func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]byte) (content map[string]*osutil.FileState, err error) { for _, appInfo := range snapInfo.Apps { - appSnippets := snippets[appInfo.Name] + securityTag := appInfo.SecurityTag() + appSnippets := snippets[securityTag] if len(appSnippets) == 0 { continue } @@ -108,7 +109,7 @@ func (b *Backend) combineSnippets(snapInfo *snap.Info, snippets map[string][][]b if content == nil { content = make(map[string]*osutil.FileState) } - fname := fmt.Sprintf("70-%s.rules", appInfo.SecurityTag()) + fname := fmt.Sprintf("70-%s.rules", securityTag) content[fname] = &osutil.FileState{Content: buf.Bytes(), Mode: 0644} } return content, nil diff --git a/overlord/export_test.go b/overlord/export_test.go index fcde13c48d..ac4a4cdbf0 100644 --- a/overlord/export_test.go +++ b/overlord/export_test.go @@ -21,6 +21,8 @@ package overlord import ( "time" + + "github.com/snapcore/snapd/overlord/state" ) // MockEnsureInterval sets the overlord ensure interval for tests. @@ -53,3 +55,20 @@ func MockEnsureNext(o *Overlord, t time.Time) { func (o *Overlord) Engine() *StateEngine { return o.stateEng } + +var ( + PopulateStateFromInstalled = populateStateFromInstalled + Migrations = migrations +) + +// MockPatches lets mock the current patch level and available migrations. +func MockPatches(level int, m map[int]func(*state.State) error) (restore func()) { + prevLevel := patchLevel + prevMigrations := migrations + patchLevel = level + migrations = m + return func() { + patchLevel = prevLevel + migrations = prevMigrations + } +} diff --git a/overlord/firstboot.go b/overlord/firstboot.go index 5616b5cccb..2936355a0c 100644 --- a/overlord/firstboot.go +++ b/overlord/firstboot.go @@ -20,53 +20,119 @@ package overlord import ( + "errors" "fmt" + "path/filepath" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/firstboot" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" - "github.com/snapcore/snapd/snappy" + "github.com/snapcore/snapd/snap" +) + +var ( + // ErrNotFirstBoot is an error that indicates that the first boot has already + // run + ErrNotFirstBoot = errors.New("this is not your first boot") ) func populateStateFromInstalled() error { - all, err := (&snappy.Overlord{}).Installed() + if osutil.FileExists(dirs.SnapStateFile) { + return fmt.Errorf("cannot create state: state %q already exists", dirs.SnapStateFile) + } + + ovld, err := New() if err != nil { return err } + st := ovld.State() - if osutil.FileExists(dirs.SnapStateFile) { - return fmt.Errorf("cannot create state: state %q already exists", dirs.SnapStateFile) + all, err := filepath.Glob(filepath.Join(dirs.SnapSeedDir, "snaps", "*.snap")) + if err != nil { + return err + } + + tsAll := []*state.TaskSet{} + for i, snapPath := range all { + + fmt.Printf("Installing %s\n", snapPath) + + st.Lock() + + // XXX: needing to know the name here is too early + + // everything will be sideloaded for now - that is + // ok, we will support adding assertions soon + snapf, err := snap.Open(snapPath) + if err != nil { + return err + } + info, err := snap.ReadInfoFromSnapFile(snapf, nil) + if err != nil { + return err + } + ts, err := snapstate.InstallPath(st, info.Name(), snapPath, "", 0) + + if i > 0 { + ts.WaitAll(tsAll[i-1]) + } + st.Unlock() + + if err != nil { + return err + } + + tsAll = append(tsAll, ts) + } + if len(tsAll) == 0 { + return nil } - st := state.New(&overlordStateBackend{ - path: dirs.SnapStateFile, - }) st.Lock() - defer st.Unlock() + msg := fmt.Sprintf("First boot seeding") + chg := st.NewChange("seed", msg) + for _, ts := range tsAll { + chg.AddAll(ts) + } + st.Unlock() - for _, sn := range all { - // no need to do a snapstate.Get() because this is firstboot - info := sn.Info() + // do it and wait for ready + ovld.Loop() + + st.EnsureBefore(0) + <-chg.Ready() + + st.Lock() + status := chg.Status() + st.Unlock() + if status != state.DoneStatus { + ovld.Stop() + return fmt.Errorf("cannot run seed change: %s", chg.Err()) - var snapst snapstate.SnapState - snapst.Sequence = append(snapst.Sequence, &info.SideInfo) - snapst.Channel = info.Channel - snapst.Active = sn.IsActive() - snapstate.Set(st, sn.Name(), &snapst) } - return nil + return ovld.Stop() } -// FIXME: -// This is not the final way we will do the state sync. This is just -// an intermediate step to have working images again. We need to -// figure out how we want first-boot to look like. +// FirstBoot will do some initial boot setup and then sync the +// state func FirstBoot() error { - if err := snappy.FirstBoot(); err != nil { + if firstboot.HasRun() { + return ErrNotFirstBoot + } + if err := firstboot.EnableFirstEther(); err != nil { + logger.Noticef("Failed to bring up ethernet: %s", err) + } + + // snappy will be in a very unhappy state if this happens, + // because populateStateFromInstalled will error if there + // is a state file already + if err := populateStateFromInstalled(); err != nil { return err } - return populateStateFromInstalled() + return firstboot.StampFirstBoot() } diff --git a/overlord/firstboot_test.go b/overlord/firstboot_test.go new file mode 100644 index 0000000000..01353dbe49 --- /dev/null +++ b/overlord/firstboot_test.go @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package overlord_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type FirstBootTestSuite struct { + systemctl *testutil.MockCmd +} + +var _ = Suite(&FirstBootTestSuite{}) + +func (s *FirstBootTestSuite) SetUpTest(c *C) { + tempdir := c.MkDir() + dirs.SetRootDir(tempdir) + + // mock the world! + err := os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(dirs.SnapServicesDir, 0755) + c.Assert(err, IsNil) + os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1") + s.systemctl = testutil.MockCommand(c, "systemctl", "") +} + +func (s *FirstBootTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("/") + os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") + s.systemctl.Restore() +} + +func (s *FirstBootTestSuite) TestTwoRuns(c *C) { + c.Assert(overlord.FirstBoot(), IsNil) + _, err := os.Stat(dirs.SnapFirstBootStamp) + c.Assert(err, IsNil) + + c.Assert(overlord.FirstBoot(), Equals, overlord.ErrNotFirstBoot) +} + +func (s *FirstBootTestSuite) TestNoErrorWhenNoGadget(c *C) { + c.Assert(overlord.FirstBoot(), IsNil) + _, err := os.Stat(dirs.SnapFirstBootStamp) + c.Assert(err, IsNil) +} + +func (s *FirstBootTestSuite) TestPopulateFromInstalledErrorsOnState(c *C) { + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + err = ioutil.WriteFile(dirs.SnapStateFile, nil, 0644) + c.Assert(err, IsNil) + + err = overlord.PopulateStateFromInstalled() + c.Assert(err, ErrorMatches, "cannot create state: state .* already exists") +} + +func (s *FirstBootTestSuite) TestPopulateFromInstalledSimpleNoSideInfo(c *C) { + // put a firstboot snap into the SnapBlobDir + snapYaml := `name: foo +version: 1.0` + mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil) + targetSnapFile := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile)) + err := os.Rename(mockSnapFile, targetSnapFile) + c.Assert(err, IsNil) + + // run the firstboot stuff + err = overlord.PopulateStateFromInstalled() + c.Assert(err, IsNil) + + // and check the snap got correctly installed + c.Check(osutil.FileExists(filepath.Join(dirs.SnapSnapsDir, "foo", "x1", "meta", "snap.yaml")), Equals, true) +} diff --git a/overlord/managers_test.go b/overlord/managers_test.go index a670dfb37b..3c87785301 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -63,6 +63,8 @@ var _ = Suite(&mgrsSuite{}) func (ms *mgrsSuite) SetUpTest(c *C) { ms.tempdir = c.MkDir() dirs.SetRootDir(ms.tempdir) + err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) + c.Assert(err, IsNil) os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1") diff --git a/overlord/migrations.go b/overlord/migrations.go new file mode 100644 index 0000000000..87eac8094f --- /dev/null +++ b/overlord/migrations.go @@ -0,0 +1,96 @@ +// -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +package overlord + +import ( + "fmt" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/state" +) + +// patchLevel is the current implemented patch level of the state format and content. +var patchLevel = 0 + +// PatchLevel returns the implemented patch level for state format and content. +func PatchLevel() int { + return patchLevel +} + +// initialize state at the current implemented patch level. +func initialize(s *state.State) { + s.Lock() + defer s.Unlock() + s.Set("patch-level", patchLevel) +} + +// migrate executes migrations that bridge state format changes as +// identified by patch level increments that can be dealt with as +// one-shot. +func migrate(s *state.State) error { + var level int + s.Lock() + err := s.Get("patch-level", &level) + s.Unlock() + if err != nil && err != state.ErrNoState { + return err + } + if level == patchLevel { + // already at right level, nothing to do + return nil + } + if level > patchLevel { + return fmt.Errorf("cannot downgrade: snapd is too old for the current state patch level %d", level) + } + + for level != patchLevel { + logger.Noticef("Running migration from state patch level %d to %d", level, level+1) + err := runMigration(s, level) + if err != nil { + logger.Noticef("Cannnot migrate: %v", err) + return fmt.Errorf("cannot migrate from state patch level %d to %d: %v", level, level+1, err) + } + level++ + } + + return nil +} + +func runMigration(s *state.State, level int) error { + m := migrations[level] + if m == nil { + return fmt.Errorf("no supported migration") + } + s.Lock() + defer s.Unlock() + + err := m(s) + if err != nil { + return err + } + + s.Set("patch-level", level+1) + + return nil +} + +// migrations maps from patch level L to migration function for L to L+1. +// Migration functions are run with the state lock held. +var migrations = map[int]func(s *state.State) error{} diff --git a/overlord/overlord.go b/overlord/overlord.go index da94d2fba9..c8fcd6314c 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -23,6 +23,7 @@ package overlord import ( "fmt" "os" + "path/filepath" "sync" "time" @@ -107,16 +108,34 @@ func New() (*Overlord, error) { func loadState(backend state.Backend) (*state.State, error) { if !osutil.FileExists(dirs.SnapStateFile) { - return state.New(backend), nil + // fail fast, mostly interesting for tests, this dir is setup + // by the snapd package + stateDir := filepath.Dir(dirs.SnapStateFile) + if !osutil.IsDirectory(stateDir) { + return nil, fmt.Errorf("fatal: directory %q must be present", stateDir) + } + s := state.New(backend) + initialize(s) + return s, nil } r, err := os.Open(dirs.SnapStateFile) if err != nil { - return nil, fmt.Errorf("failed to read the state file: %s", err) + return nil, fmt.Errorf("cannot read the state file: %s", err) } defer r.Close() - return state.ReadState(backend, r) + s, err := state.ReadState(backend, r) + if err != nil { + return nil, err + } + + // one-shot migrations + err = migrate(s) + if err != nil { + return nil, err + } + return s, nil } func (o *Overlord) ensureTimerSetup() { diff --git a/overlord/overlord_test.go b/overlord/overlord_test.go index d6ca588ce2..23a6fa9c68 100644 --- a/overlord/overlord_test.go +++ b/overlord/overlord_test.go @@ -20,9 +20,12 @@ package overlord_test import ( + "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" + "sort" "syscall" "testing" "time" @@ -54,6 +57,9 @@ func (ovs *overlordSuite) TearDownTest(c *C) { } func (ovs *overlordSuite) TestNew(c *C) { + restore := overlord.MockPatches(1, nil) + defer restore() + o, err := overlord.New() c.Assert(err, IsNil) c.Check(o, NotNil) @@ -65,16 +71,22 @@ func (ovs *overlordSuite) TestNew(c *C) { s := o.State() c.Check(s, NotNil) c.Check(o.Engine().State(), Equals, s) + + s.Lock() + defer s.Unlock() + var patchLevel int + s.Get("patch-level", &patchLevel) + c.Check(patchLevel, Equals, 1) } func (ovs *overlordSuite) TestNewWithGoodState(c *C) { - fakeState := []byte(`{"data":{"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`) + fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`, overlord.PatchLevel())) err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) c.Assert(err, IsNil) o, err := overlord.New() - c.Assert(err, IsNil) + state := o.State() c.Assert(err, IsNil) state.Lock() @@ -82,7 +94,14 @@ func (ovs *overlordSuite) TestNewWithGoodState(c *C) { d, err := state.MarshalJSON() c.Assert(err, IsNil) - c.Assert(string(d), DeepEquals, string(fakeState)) + + var got, expected map[string]interface{} + err = json.Unmarshal(d, &got) + c.Assert(err, IsNil) + err = json.Unmarshal(fakeState, &expected) + c.Assert(err, IsNil) + + c.Check(got, DeepEquals, expected) } func (ovs *overlordSuite) TestNewWithInvalidState(c *C) { @@ -94,6 +113,136 @@ func (ovs *overlordSuite) TestNewWithInvalidState(c *C) { c.Assert(err, ErrorMatches, "EOF") } +func (ovs *overlordSuite) TestNewNoDowngrade(c *C) { + overlord.MockPatches(2, nil) + + fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`, 3)) + err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) + c.Assert(err, IsNil) + + _, err = overlord.New() + c.Assert(err, ErrorMatches, `cannot downgrade: snapd is too old for the current state patch level 3`) +} + +func (ovs *overlordSuite) TestNewWithMigrations(c *C) { + m12 := func(s *state.State) error { + s.Set("m12", true) + return nil + } + m23 := func(s *state.State) error { + s.Set("m23", true) + return nil + } + overlord.MockPatches(3, map[int]func(*state.State) error{ + 1: m12, + 2: m23, + }) + + fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`, 1)) + err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) + c.Assert(err, IsNil) + + o, err := overlord.New() + c.Assert(err, IsNil) + + state := o.State() + c.Assert(err, IsNil) + state.Lock() + defer state.Unlock() + + var level int + var m12f, m23f bool + err = state.Get("patch-level", &level) + c.Assert(err, IsNil) + c.Check(level, Equals, 3) + + err = state.Get("m12", &m12f) + c.Assert(err, IsNil) + c.Check(m12f, Equals, true) + + err = state.Get("m23", &m23f) + c.Assert(err, IsNil) + c.Check(m12f, Equals, true) +} + +func (ovs *overlordSuite) TestNewWithMissingMigrations(c *C) { + m23 := func(s *state.State) error { + s.Set("m23", true) + return nil + } + overlord.MockPatches(3, map[int]func(*state.State) error{ + 2: m23, + }) + + fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`, 1)) + err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) + c.Assert(err, IsNil) + + _, err = overlord.New() + c.Assert(err, ErrorMatches, `cannot migrate from state patch level 1 to 2: no supported migration`) +} + +func (ovs *overlordSuite) TestNewWithMigrationError(c *C) { + m12 := func(s *state.State) error { + return fmt.Errorf("m12 failed") + } + m23 := func(s *state.State) error { + s.Set("m23", true) + return nil + } + overlord.MockPatches(3, map[int]func(*state.State) error{ + 1: m12, + 2: m23, + }) + + fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"some":"data"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0}`, 1)) + err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) + c.Assert(err, IsNil) + + _, err = overlord.New() + c.Assert(err, ErrorMatches, `cannot migrate from state patch level 1 to 2: m12 failed`) + + r, err := os.Open(dirs.SnapStateFile) + c.Assert(err, IsNil) + defer r.Close() + + s, err := state.ReadState(nil, r) + c.Assert(err, IsNil) + + s.Lock() + defer s.Unlock() + + var level int + var m12f, m23f bool + err = s.Get("patch-level", &level) + c.Assert(err, IsNil) + c.Check(level, Equals, 1) + + err = s.Get("m12", &m12f) + c.Assert(err, Equals, state.ErrNoState) + + err = s.Get("m23", &m23f) + c.Assert(err, Equals, state.ErrNoState) +} + +func (ovs *overlordSuite) TestMigrationsSanity(c *C) { + if overlord.PatchLevel() == 0 { + c.Assert(len(overlord.Migrations), Equals, 0) + c.Skip("patch level still at 0, no migrations") + } + from := make([]int, 0, len(overlord.Migrations)) + for l, _ := range overlord.Migrations { + from = append(from, l) + } + sort.Ints(from) + // all steps present + for i := 1; i < len(from); i++ { + c.Check(from[i], Equals, from[i-1]+1) + } + // ends at previous of implemented patch level + c.Check(from[len(from)-1], Equals, overlord.PatchLevel()-1) +} + type witnessManager struct { state *state.State expectedEnsure int diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index b12353571a..d3d69b3603 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -28,7 +28,7 @@ import ( // A StoreService can find, list available updates and download snaps. type StoreService interface { - Snap(name, channel string, auther store.Authenticator) (*snap.Info, error) + Snap(name, channel string, devmode bool, auther store.Authenticator) (*snap.Info, error) Find(query, channel string, auther store.Authenticator) ([]*snap.Info, error) ListRefresh([]*store.RefreshCandidate, store.Authenticator) ([]*snap.Info, error) SuggestedCurrency() string diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index 64fbe8b8ad..b351b83a17 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -54,7 +54,7 @@ type fakeStore struct { fakeTotalProgress int } -func (f *fakeStore) Snap(name, channel string, auther store.Authenticator) (*snap.Info, error) { +func (f *fakeStore) Snap(name, channel string, devmode bool, auther store.Authenticator) (*snap.Info, error) { revno := snap.R(11) if channel == "channel-for-7" { revno.N = 7 diff --git a/overlord/snapstate/check_snap.go b/overlord/snapstate/check_snap.go index df2ad634b5..33aaeb6cda 100644 --- a/overlord/snapstate/check_snap.go +++ b/overlord/snapstate/check_snap.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/snapcore/snapd/arch" + "github.com/snapcore/snapd/firstboot" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -53,7 +54,7 @@ func checkAssumes(s *snap.Info) error { var openSnapFile = backend.OpenSnapFile // checkSnap ensures that the snap can be installed. -func checkSnap(state *state.State, snapFilePath string, curInfo *snap.Info, flags Flags) error { +func checkSnap(st *state.State, snapFilePath string, curInfo *snap.Info, flags Flags) error { // XXX: actually verify snap before using content from it unless dev-mode s, _, err := openSnapFile(snapFilePath, nil) @@ -75,21 +76,28 @@ func checkSnap(state *state.State, snapFilePath string, curInfo *snap.Info, flag if s.Type != snap.TypeGadget { return nil } - state.Lock() - defer state.Unlock() - if currentGadget, err := GadgetInfo(state); err == nil { - // TODO: actually compare snap ids, from current gadget and candidate - if currentGadget.Name() == s.Name() { - return nil - } - - return fmt.Errorf("cannot replace gadget snap with a different one") - } else if release.OnClassic { + // gadget specific checks + if release.OnClassic { // for the time being return fmt.Errorf("cannot install a gadget snap on classic") } - // there should always be a gadget snap on devices - return fmt.Errorf("cannot find original gadget snap") + st.Lock() + defer st.Unlock() + currentGadget, err := GadgetInfo(st) + // in firstboot we have no gadget yet - that is ok + if err == state.ErrNoState && !firstboot.HasRun() { + return nil + } + if err != nil { + return fmt.Errorf("cannot find original gadget snap") + } + + // TODO: actually compare snap ids, from current gadget and candidate + if currentGadget.Name() != s.Name() { + return fmt.Errorf("cannot replace gadget snap with a different one") + } + + return nil } diff --git a/overlord/snapstate/check_snap_test.go b/overlord/snapstate/check_snap_test.go index b848c9dbe9..8b8d55de13 100644 --- a/overlord/snapstate/check_snap_test.go +++ b/overlord/snapstate/check_snap_test.go @@ -21,6 +21,9 @@ package snapstate_test import ( "fmt" + "io/ioutil" + "os" + "path/filepath" . "gopkg.in/check.v1" @@ -107,6 +110,9 @@ assumes: [common-data-dir]` } func (s *checkSnapSuite) TestCheckSnapGadgetUpdate(c *C) { + reset := release.MockOnClassic(false) + defer reset() + st := state.New(nil) st.Lock() defer st.Unlock() @@ -143,6 +149,9 @@ version: 2 } func (s *checkSnapSuite) TestCheckSnapGadgetAdditionProhibited(c *C) { + reset := release.MockOnClassic(false) + defer reset() + st := state.New(nil) st.Lock() defer st.Unlock() @@ -179,6 +188,11 @@ version: 2 } func (s *checkSnapSuite) TestCheckSnapGadgetMissingPrior(c *C) { + err := os.MkdirAll(filepath.Dir(dirs.SnapFirstBootStamp), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapFirstBootStamp, nil, 0644) + c.Assert(err, IsNil) + reset := release.MockOnClassic(false) defer reset() diff --git a/overlord/snapstate/prepare_snap_test.go b/overlord/snapstate/prepare_snap_test.go new file mode 100644 index 0000000000..5275edb692 --- /dev/null +++ b/overlord/snapstate/prepare_snap_test.go @@ -0,0 +1,81 @@ +// -*- 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 <http://www.gnu.org/licenses/>. + * + */ + +package snapstate_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +type prepareSnapSuite struct { + state *state.State + snapmgr *snapstate.SnapManager + + fakeBackend *fakeSnappyBackend + + reset func() +} + +var _ = Suite(&prepareSnapSuite{}) + +func (s *prepareSnapSuite) SetUpTest(c *C) { + s.fakeBackend = &fakeSnappyBackend{} + s.state = state.New(nil) + + var err error + s.snapmgr, err = snapstate.Manager(s.state) + c.Assert(err, IsNil) + s.snapmgr.AddForeignTaskHandlers(s.fakeBackend) + + snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) + + s.reset = snapstate.MockReadInfo(s.fakeBackend.ReadInfo) +} + +func (s *prepareSnapSuite) TearDownTest(c *C) { + s.reset() +} + +func (s *prepareSnapSuite) TestDoPrepareSnapSimple(c *C) { + s.state.Lock() + t := s.state.NewTask("prepare-snap", "test") + t.Set("snap-setup", &snapstate.SnapSetup{ + Name: "foo", + }) + s.state.NewChange("dummy", "...").AddTask(t) + + s.state.Unlock() + + s.snapmgr.Ensure() + s.snapmgr.Wait() + + s.state.Lock() + defer s.state.Unlock() + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "foo", &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Candidate, DeepEquals, &snap.SideInfo{ + Revision: snap.R(-1), + }) + c.Check(t.Status(), Equals, state.DoneStatus) +} diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index fed1721338..eb7103d0fc 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -295,7 +295,7 @@ func (m *SnapManager) doDownloadSnap(t *state.Task, _ *tomb.Tomb) error { auther = user.Authenticator() } - storeInfo, err := m.store.Snap(ss.Name, ss.Channel, auther) + storeInfo, err := m.store.Snap(ss.Name, ss.Channel, ss.DevMode(), auther) if err != nil { return err } diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 05d8313d18..5cb4fd5517 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -57,28 +57,22 @@ const ( // 0x40000000 >> iota ) -func doInstall(s *state.State, curActive bool, snapName, snapPath, channel string, userID int, flags Flags) (*state.TaskSet, error) { - if err := checkChangeConflict(s, snapName); err != nil { +func doInstall(s *state.State, curActive bool, ss *SnapSetup) (*state.TaskSet, error) { + if err := checkChangeConflict(s, ss.Name); err != nil { return nil, err } - if snapPath == "" && channel == "" { - channel = "stable" + if ss.SnapPath == "" && ss.Channel == "" { + ss.Channel = "stable" } var prepare *state.Task - ss := SnapSetup{ - Channel: channel, - UserID: userID, - Flags: SnapSetupFlags(flags), - } - ss.Name = snapName - ss.SnapPath = snapPath - if snapPath != "" { - prepare = s.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q"), snapPath)) + if ss.SnapPath != "" { + prepare = s.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q"), ss.SnapPath)) } else { - prepare = s.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q from channel %q"), snapName, channel)) + prepare = s.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q from channel %q"), ss.Name, ss.Channel)) } + prepare.Set("snap-setup", ss) tasks := []*state.Task{prepare} @@ -88,31 +82,31 @@ func doInstall(s *state.State, curActive bool, snapName, snapPath, channel strin } // mount - mount := s.NewTask("mount-snap", fmt.Sprintf(i18n.G("Mount snap %q"), snapName)) + mount := s.NewTask("mount-snap", fmt.Sprintf(i18n.G("Mount snap %q"), ss.Name)) addTask(mount) mount.WaitFor(prepare) precopy := mount if curActive { // unlink-current-snap (will stop services for copy-data) - unlink := s.NewTask("unlink-current-snap", fmt.Sprintf(i18n.G("Make current revision for snap %q unavailable"), snapName)) + unlink := s.NewTask("unlink-current-snap", fmt.Sprintf(i18n.G("Make current revision for snap %q unavailable"), ss.Name)) addTask(unlink) unlink.WaitFor(mount) precopy = unlink } // copy-data (needs stopped services by unlink) - copyData := s.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), snapName)) + copyData := s.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), ss.Name)) addTask(copyData) copyData.WaitFor(precopy) // security - setupSecurity := s.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q security profiles"), snapName)) + setupSecurity := s.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q security profiles"), ss.Name)) addTask(setupSecurity) setupSecurity.WaitFor(copyData) // finalize (wrappers+current symlink) - linkSnap := s.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q available to the system"), snapName)) + linkSnap := s.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q available to the system"), ss.Name)) addTask(linkSnap) linkSnap.WaitFor(setupSecurity) @@ -148,7 +142,14 @@ func Install(s *state.State, name, channel string, userID int, flags Flags) (*st return nil, fmt.Errorf("snap %q already installed", name) } - return doInstall(s, false, name, "", channel, userID, flags) + ss := &SnapSetup{ + Name: name, + Channel: channel, + UserID: userID, + Flags: SnapSetupFlags(flags), + } + + return doInstall(s, false, ss) } // InstallPath returns a set of tasks for installing snap from a file path. @@ -160,7 +161,14 @@ func InstallPath(s *state.State, name, path, channel string, flags Flags) (*stat return nil, err } - return doInstall(s, snapst.Active, name, path, channel, 0, flags) + ss := &SnapSetup{ + Name: name, + SnapPath: path, + Channel: channel, + Flags: SnapSetupFlags(flags), + } + + return doInstall(s, snapst.Active, ss) } // TryPath returns a set of tasks for trying a snap from a file path. @@ -187,8 +195,14 @@ func Update(s *state.State, name, channel string, userID int, flags Flags) (*sta channel = snapst.Channel } - // TODO: pass the right UserID - return doInstall(s, snapst.Active, name, "", channel, userID, flags) + ss := &SnapSetup{ + Name: name, + Channel: channel, + UserID: userID, + Flags: SnapSetupFlags(flags), + } + + return doInstall(s, snapst.Active, ss) } func removeInactiveRevision(s *state.State, name string, revision snap.Revision) *state.TaskSet { @@ -435,6 +449,9 @@ func GadgetInfo(s *state.State) (*snap.Info, error) { return nil, err } for snapName, snapState := range stateMap { + if snapState.Current() == nil { + continue + } snapInfo, err := readInfo(snapName, snapState.Current()) if err != nil { logger.Noticef("cannot retrieve info for snap %q: %s", snapName, err) diff --git a/snap/implicit.go b/snap/implicit.go index 812b46df89..318a139ee7 100644 --- a/snap/implicit.go +++ b/snap/implicit.go @@ -53,6 +53,8 @@ var implicitClassicSlots = []string{ "unity7", "x11", "modem-manager", + "optical-drive", + "camera", } // AddImplicitSlots adds implicitly defined slots to a given snap. diff --git a/snap/implicit_test.go b/snap/implicit_test.go index b425d26225..75fb2d68ca 100644 --- a/snap/implicit_test.go +++ b/snap/implicit_test.go @@ -56,7 +56,7 @@ func (s *InfoSnapYamlTestSuite) TestAddImplicitSlotsOnClassic(c *C) { c.Assert(info.Slots["unity7"].Interface, Equals, "unity7") c.Assert(info.Slots["unity7"].Name, Equals, "unity7") c.Assert(info.Slots["unity7"].Snap, Equals, info) - c.Assert(info.Slots, HasLen, 22) + c.Assert(info.Slots, HasLen, 24) } func (s *InfoSnapYamlTestSuite) TestImplicitSlotsAreRealInterfaces(c *C) { diff --git a/snap/info.go b/snap/info.go index ddd08cedeb..2d4415c83e 100644 --- a/snap/info.go +++ b/snap/info.go @@ -68,6 +68,16 @@ func MountDir(name string, revision Revision) string { return filepath.Join(dirs.SnapSnapsDir, name, revision.String()) } +// AppSecurityTag returns the application-specific security tag. +func AppSecurityTag(snapName, appName string) string { + return fmt.Sprintf("snap.%s.%s", snapName, appName) +} + +// HookSecurityTag returns the hook-specific security tag. +func HookSecurityTag(snapName, hookName string) string { + return fmt.Sprintf("snap.%s.hook.%s", snapName, hookName) +} + // SideInfo holds snap metadata that is crucial for the tracking of // snaps and for the working of the system offline and which is not // included in snap.yaml or for which the store is the canonical @@ -186,6 +196,11 @@ func (s *Info) CommonDataHomeDir() string { return filepath.Join(dirs.SnapDataHomeGlob, s.Name(), "common") } +// NeedsDevMode retursn whether the snap needs devmode. +func (s *Info) NeedsDevMode() bool { + return s.Confinement == DevmodeConfinement +} + // sanity check that Info is a PlaceInfo var _ PlaceInfo = (*Info)(nil) @@ -253,7 +268,7 @@ type HookInfo struct { // Security tags are used by various security subsystems as "profile names" and // sometimes also as a part of the file name. func (app *AppInfo) SecurityTag() string { - return fmt.Sprintf("snap.%s.%s", app.Snap.Name(), app.Name) + return AppSecurityTag(app.Snap.Name(), app.Name) } // WrapperPath returns the path to wrapper invoking the app binary. @@ -326,7 +341,7 @@ func (app *AppInfo) Env() []string { // Security tags are used by various security subsystems as "profile names" and // sometimes also as a part of the file name. func (hook *HookInfo) SecurityTag() string { - return fmt.Sprintf("snap.%s.hook.%s", hook.Snap.Name(), hook.Name) + return HookSecurityTag(hook.Snap.Name(), hook.Name) } func infoFromSnapYamlWithSideInfo(meta []byte, si *SideInfo) (*Info, error) { diff --git a/snap/info_test.go b/snap/info_test.go index 38fb2c5daf..642a8f4916 100644 --- a/snap/info_test.go +++ b/snap/info_test.go @@ -127,11 +127,8 @@ func (s *infoSuite) TestReadInfo(c *C) { c.Check(snapInfo2, DeepEquals, snapInfo1) } +// makeTestSnap here can also be used to produce broken snaps (differently from snaptest.MakeTestSnapWithFiles)! func makeTestSnap(c *C, yaml string) string { - return makeTestSnapWithHooks(c, yaml, nil) -} - -func makeTestSnapWithHooks(c *C, yaml string, hookNames []string) string { tmp := c.MkDir() snapSource := filepath.Join(tmp, "snapsrc") @@ -141,18 +138,6 @@ func makeTestSnapWithHooks(c *C, yaml string, hookNames []string) string { err = ioutil.WriteFile(filepath.Join(snapSource, "meta", "snap.yaml"), []byte(yaml), 0644) c.Assert(err, IsNil) - // make the requested hooks - if len(hookNames) > 0 { - hooksDir := filepath.Join(snapSource, "meta", "hooks") - err := os.MkdirAll(filepath.Join(hooksDir), 0755) - c.Assert(err, IsNil) - - for _, hookName := range hookNames { - err = ioutil.WriteFile(filepath.Join(hooksDir, hookName), nil, 0644) - c.Assert(err, IsNil) - } - } - dest := filepath.Join(tmp, "foo.snap") snap := squashfs.New(dest) err = snap.Build(snapSource) @@ -161,6 +146,14 @@ func makeTestSnapWithHooks(c *C, yaml string, hookNames []string) string { return dest } +// produce descrs for empty hooks suitable for snaptest.PopulateDir +func emptyHooks(hookNames ...string) (emptyHooks [][]string) { + for _, hookName := range hookNames { + emptyHooks = append(emptyHooks, []string{filepath.Join("meta", "hooks", hookName), ""}) + } + return +} + func (s *infoSuite) TestReadInfoFromSnapFile(c *C) { yaml := `name: foo version: 1.0 @@ -349,7 +342,7 @@ hooks: func (s *infoSuite) TestReadInfoFromSnapFileCatchesInvalidImplicitHook(c *C) { yaml := `name: foo version: 1.0` - snapPath := makeTestSnapWithHooks(c, yaml, []string{"abc123"}) + snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks("abc123")) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) @@ -361,13 +354,14 @@ version: 1.0` func (s *infoSuite) checkInstalledSnapAndSnapFile(c *C, yaml string, hooks []string, checker func(c *C, info *snap.Info)) { // First check installed snap sideInfo := &snap.SideInfo{Revision: snap.R(42)} - info := snaptest.MockSnapWithHooks(c, yaml, sideInfo, hooks) - info, err := snap.ReadInfo(info.Name(), sideInfo) + info0 := snaptest.MockSnap(c, yaml, sideInfo) + snaptest.PopulateDir(info0.MountDir(), emptyHooks(hooks...)) + info, err := snap.ReadInfo(info0.Name(), sideInfo) c.Check(err, IsNil) checker(c, info) // Now check snap file - snapPath := makeTestSnapWithHooks(c, yaml, hooks) + snapPath := snaptest.MakeTestSnapWithFiles(c, yaml, emptyHooks(hooks...)) snapf, err := snap.Open(snapPath) c.Assert(err, IsNil) info, err = snap.ReadInfoFromSnapFile(snapf, nil) diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index d27fcb65c3..b08df1ea8e 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -55,28 +55,6 @@ func MockSnap(c *check.C, yamlText string, sideInfo *snap.SideInfo) *snap.Info { return snapInfo } -// MockSnapWithHooks puts a snap.yaml file on disk along with hooks so to mock an installed snap, based on the provided arguments. -// -// The caller is responsible for mocking root directory with dirs.SetRootDir() -// and for altering the overlord state if required. -func MockSnapWithHooks(c *check.C, yamlText string, sideInfo *snap.SideInfo, hookNames []string) *snap.Info { - snapInfo := MockSnap(c, yamlText, sideInfo) - - // Now create the requested hooks (if any) - if len(hookNames) > 0 { - hooksDir := snapInfo.HooksDir() - err := os.MkdirAll(filepath.Join(hooksDir), 0755) - c.Assert(err, check.IsNil) - - for _, hookName := range hookNames { - err = ioutil.WriteFile(filepath.Join(hooksDir, hookName), nil, 0644) - c.Assert(err, check.IsNil) - } - } - - return snapInfo -} - // PopulateDir populates the directory with files specified as pairs of relative file path and its content. Useful to add extra files to a snap. func PopulateDir(dir string, files [][]string) { for _, filenameAndContent := range files { diff --git a/snap/squashfs/squashfs.go b/snap/squashfs/squashfs.go index 4c9578aff5..062306b8df 100644 --- a/snap/squashfs/squashfs.go +++ b/snap/squashfs/squashfs.go @@ -72,6 +72,11 @@ func (s *Snap) Install(targetPath, mountDir string) error { } } + // nothing to do, happens on e.g. first-boot + if s.path == targetPath { + return nil + } + // FIXME: cp.CopyFile() has no preserve attribute flag yet return runCommand("cp", "-a", s.path, targetPath) } diff --git a/snappy/errors.go b/snappy/errors.go index bc78c199ca..0bd2fd96b5 100644 --- a/snappy/errors.go +++ b/snappy/errors.go @@ -95,10 +95,6 @@ var ( // accepting a license is required, but no license file is provided ErrLicenseNotProvided = errors.New("snap.yaml requires license, but no license was provided") - // ErrNotFirstBoot is an error that indicates that the first boot has already - // run - ErrNotFirstBoot = errors.New("this is not your first boot") - // ErrNotImplemented may be returned when an implementation of // an interface is partial. ErrNotImplemented = errors.New("not implemented") diff --git a/snappy/firstboot.go b/snappy/firstboot.go deleted file mode 100644 index aeee5c36a0..0000000000 --- a/snappy/firstboot.go +++ /dev/null @@ -1,163 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2014-2015 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -package snappy - -import ( - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/progress" - - "gopkg.in/yaml.v2" -) - -var ( - errNoSnapToConfig = errors.New("configuring an invalid snappy package") - errNoSnapToActivate = errors.New("activating an invalid snappy package") -) - -func wrapConfig(pkgName string, conf interface{}) ([]byte, error) { - configWrap := map[string]map[string]interface{}{ - "config": map[string]interface{}{ - pkgName: conf, - }, - } - - return yaml.Marshal(configWrap) -} - -var newSnapMap = newSnapMapImpl - -func newSnapMapImpl() (map[string]*Snap, error) { - all, err := (&Overlord{}).Installed() - if err != nil { - return nil, err - } - - m := make(map[string]*Snap, 2*len(all)) - for _, snap := range all { - info := snap.Info() - m[FullName(info)] = snap - m[BareName(info)] = snap - } - - return m, nil -} - -type activator interface { - SetActive(sp *Snap, active bool, meter progress.Meter) error -} - -var getActivator = func() activator { - return &Overlord{} -} - -// enableInstalledSnaps activates the installed preinstalled snaps -// on the first boot -func enableInstalledSnaps() error { - all, err := (&Overlord{}).Installed() - if err != nil { - return nil - } - - activator := getActivator() - pb := progress.MakeProgressBar() - for _, sn := range all { - logger.Noticef("Acitvating %s", FullName(sn.Info())) - if err := activator.SetActive(sn, true, pb); err != nil { - // we don't want this to fail for now - logger.Noticef("failed to activate %s: %s", FullName(sn.Info()), err) - } - } - - return nil -} - -// FirstBoot checks whether it's the first boot, and if so enables the -// first ethernet device and runs gadgetConfig (as well as flagging that -// it run) -func FirstBoot() error { - if firstBootHasRun() { - return ErrNotFirstBoot - } - defer stampFirstBoot() - defer enableFirstEther() - - return enableInstalledSnaps() -} - -// NOTE: if you change stampFile, update the condition in -// snapd.firstboot.service to match -var stampFile = "/var/lib/snapd/firstboot/stamp" - -func stampFirstBoot() error { - // filepath.Dir instead of firstbootDir directly to ease testing - stampDir := filepath.Dir(stampFile) - - if _, err := os.Stat(stampDir); os.IsNotExist(err) { - if err := os.MkdirAll(stampDir, 0755); err != nil { - return err - } - } - - return osutil.AtomicWriteFile(stampFile, []byte{}, 0644, 0) -} - -var globs = []string{"/sys/class/net/eth*", "/sys/class/net/en*"} -var ethdir = "/etc/network/interfaces.d" -var ifup = "/sbin/ifup" - -func enableFirstEther() error { - var eths []string - for _, glob := range globs { - eths, _ = filepath.Glob(glob) - if len(eths) != 0 { - break - } - } - if len(eths) == 0 { - return nil - } - eth := filepath.Base(eths[0]) - ethfile := filepath.Join(ethdir, eth) - data := fmt.Sprintf("allow-hotplug %[1]s\niface %[1]s inet dhcp\n", eth) - - if err := osutil.AtomicWriteFile(ethfile, []byte(data), 0644, 0); err != nil { - return err - } - - ifup := exec.Command(ifup, eth) - ifup.Stdout = os.Stdout - ifup.Stderr = os.Stderr - if err := ifup.Run(); err != nil { - return err - } - - return nil -} - -func firstBootHasRun() bool { - return osutil.FileExists(stampFile) -} diff --git a/snappy/firstboot_test.go b/snappy/firstboot_test.go deleted file mode 100644 index 1d74750f1a..0000000000 --- a/snappy/firstboot_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2015 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -package snappy - -import ( - "io/ioutil" - "os" - "path/filepath" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/systemd" -) - -type FirstBootTestSuite struct { - gadgetConfig map[string]interface{} - globs []string - ethdir string - ifup string - e error - snapMap map[string]*Snap - snapMapErr error -} - -var _ = Suite(&FirstBootTestSuite{}) - -func (s *FirstBootTestSuite) SetUpTest(c *C) { - tempdir := c.MkDir() - dirs.SetRootDir(tempdir) - os.MkdirAll(dirs.SnapSnapsDir, 0755) - stampFile = filepath.Join(c.MkDir(), "stamp") - - // mock the world! - systemd.SystemctlCmd = func(cmd ...string) ([]byte, error) { - return []byte("ActiveState=inactive\n"), nil - } - - err := os.MkdirAll(filepath.Join(tempdir, "etc", "systemd", "system", "multi-user.target.wants"), 0755) - c.Assert(err, IsNil) - - s.globs = globs - globs = nil - s.ethdir = ethdir - ethdir = c.MkDir() - s.ifup = ifup - ifup = "/bin/true" - newSnapMap = s.newSnapMap - - s.e = nil - s.snapMap = nil - s.snapMapErr = nil -} - -func (s *FirstBootTestSuite) TearDownTest(c *C) { - globs = s.globs - ethdir = s.ethdir - ifup = s.ifup - newSnapMap = newSnapMapImpl -} - -func (s *FirstBootTestSuite) newSnapMap() (map[string]*Snap, error) { - return s.snapMap, s.snapMapErr -} - -func (s *FirstBootTestSuite) TestTwoRuns(c *C) { - c.Assert(FirstBoot(), IsNil) - _, err := os.Stat(stampFile) - c.Assert(err, IsNil) - - c.Assert(FirstBoot(), Equals, ErrNotFirstBoot) -} - -func (s *FirstBootTestSuite) TestNoErrorWhenNoGadget(c *C) { - c.Assert(FirstBoot(), IsNil) - _, err := os.Stat(stampFile) - c.Assert(err, IsNil) -} - -func (s *FirstBootTestSuite) TestEnableFirstEther(c *C) { - c.Check(enableFirstEther(), IsNil) - fs, _ := filepath.Glob(filepath.Join(ethdir, "*")) - c.Assert(fs, HasLen, 0) -} - -func (s *FirstBootTestSuite) TestEnableFirstEtherSomeEth(c *C) { - dir := c.MkDir() - _, err := os.Create(filepath.Join(dir, "eth42")) - c.Assert(err, IsNil) - - globs = []string{filepath.Join(dir, "eth*")} - c.Check(enableFirstEther(), IsNil) - fs, _ := filepath.Glob(filepath.Join(ethdir, "*")) - c.Assert(fs, HasLen, 1) - bs, err := ioutil.ReadFile(fs[0]) - c.Assert(err, IsNil) - c.Check(string(bs), Equals, "allow-hotplug eth42\niface eth42 inet dhcp\n") - -} - -func (s *FirstBootTestSuite) TestEnableFirstEtherBadEthDir(c *C) { - dir := c.MkDir() - _, err := os.Create(filepath.Join(dir, "eth42")) - c.Assert(err, IsNil) - - ethdir = "/no/such/thing" - globs = []string{filepath.Join(dir, "eth*")} - err = enableFirstEther() - c.Check(err, NotNil) - c.Check(os.IsNotExist(err), Equals, true) -} - -var mockOSYaml = ` -name: ubuntu-core -version: 1.0 -type: os -` - -var mockKernelYaml = ` -name: canonical-linux-pc -version: 1.0 -type: kernel -` - -func (s *FirstBootTestSuite) ensureSystemSnapIsEnabledOnFirstBoot(c *C, yaml string, expectActivated bool) { - _, err := makeInstalledMockSnap(yaml, 11) - c.Assert(err, IsNil) - - all, err := (&Overlord{}).Installed() - c.Check(err, IsNil) - c.Assert(all, HasLen, 1) - c.Check(all[0].IsActive(), Equals, false) - - c.Assert(FirstBoot(), IsNil) - - all, err = (&Overlord{}).Installed() - c.Check(err, IsNil) - c.Assert(all, HasLen, 1) - c.Check(all[0].IsActive(), Equals, expectActivated) -} - -func (s *FirstBootTestSuite) TestSystemSnapsEnablesOS(c *C) { - s.ensureSystemSnapIsEnabledOnFirstBoot(c, mockOSYaml, true) -} - -func (s *FirstBootTestSuite) TestSystemSnapsEnablesKernel(c *C) { - s.ensureSystemSnapIsEnabledOnFirstBoot(c, mockKernelYaml, true) -} - -func (s *FirstBootTestSuite) TestSystemSnapsDoesEnableApps(c *C) { - s.ensureSystemSnapIsEnabledOnFirstBoot(c, "", true) -} diff --git a/snappy/install.go b/snappy/install.go index 328c3bf30d..8bc84d2b32 100644 --- a/snappy/install.go +++ b/snappy/install.go @@ -119,7 +119,9 @@ func doInstall(name, channel string, flags LegacyInstallFlags, meter progress.Me return "", err } - snap, err := mStore.Snap(name, channel, nil) + // devmode false preserves the old behaviour but we might want + // it to be set from flags instead. + snap, err := mStore.Snap(name, channel, false, nil) if err != nil { return "", err } diff --git a/spread.yaml b/spread.yaml index bed701af7e..9b8c69ace4 100644 --- a/spread.yaml +++ b/spread.yaml @@ -9,7 +9,8 @@ backends: linode: key: $(echo $SPREAD_LINODE_KEY) systems: - - ubuntu-16.04-grub + - ubuntu-16.04-64-grub + - ubuntu-16.04-32-grub path: /gopath/src/github.com/snapcore/snapd @@ -18,9 +19,15 @@ exclude: prepare: | [ "$REUSE_PROJECT" != 1 ] || exit 0 + + # apt update is hanging on security.ubuntu.com with IPv6. + sysctl -w net.ipv6.conf.all.disable_ipv6=1 + trap "sysctl -w net.ipv6.conf.all.disable_ipv6=0" EXIT + apt purge -y snapd || true apt update apt build-dep -y ./ + test -d /home/test || adduser --quiet --disabled-password --gecos '' test chown test.test -R .. sudo -i -u test /bin/sh -c "cd $PWD && DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -tc -b -Zgzip" @@ -29,22 +36,41 @@ prepare: | # Disable burst limit so resetting the state quickly doesn't create problems. mkdir -p /etc/systemd/system/snapd.service.d - echo "[Unit]\nStartLimitInterval=0" >> /etc/systemd/system/snapd.service.d/local.conf + cat <<EOF > /etc/systemd/system/snapd.service.d/local.conf + [Unit] + StartLimitInterval=0 + [Service] + Environment=SNAPD_DEBUG_HTTP=7 + EOF - #snapbuild: get dependencies and build; we need to get the deps again because of the GOPATH redef + # Build snapbuild. apt install -y git - go get gopkg.in/check.v1 gopkg.in/yaml.v2 - go build -o $GOPATH/bin/snapbuild ./tests/lib/snapbuild + go get ./tests/lib/snapbuild + + # Snapshot the state including core. + if [ ! -f snapd-state.tar.gz ]; then + ! snap list | grep core || exit 1 + snap install hello-world + snap list | grep core + snap remove hello-world + rmdir /snap/hello-world # Should be done by snapd. + + systemctl stop snapd + systemctl daemon-reload + mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')" + services="$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')" + for unit in $services $mounts; do + systemctl stop $unit + done + tar czf snapd-state.tar.gz /var/lib/snapd /snap /etc/systemd/system/snap-*core*.mount + systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ + for unit in $mounts $services; do + systemctl start $unit + done + fi suites: tests/: summary: Full-system tests for snapd restore-each: | - echo Resetting snapd state... - systemctl stop snapd || true - umount /var/lib/snapd/snaps/*.snap 2>&1 || true - rm -rf /snap/* - rm -rf /var/lib/snapd/* - rm -f /etc/systemd/system/snap-*.{mount,service} - rm -f /etc/systemd/system/multi-user.target.wants/snap-*.mount - systemctl start snapd + $SPREAD_PATH/tests/lib/reset.sh --reuse-core diff --git a/store/store.go b/store/store.go index 51529d4a92..d53537345e 100644 --- a/store/store.go +++ b/store/store.go @@ -218,7 +218,7 @@ func NewUbuntuStoreSnapRepository(cfg *SnapUbuntuStoreConfig, storeID string) *S } // small helper that sets the correct http headers for the ubuntu store -func (s *SnapUbuntuStoreRepository) setUbuntuStoreHeaders(req *http.Request, channel string, auther Authenticator) { +func (s *SnapUbuntuStoreRepository) setUbuntuStoreHeaders(req *http.Request, channel string, devmode bool, auther Authenticator) { if auther != nil { auther.Authenticate(req) } @@ -232,6 +232,10 @@ func (s *SnapUbuntuStoreRepository) setUbuntuStoreHeaders(req *http.Request, cha req.Header.Set("X-Ubuntu-Device-Channel", channel) } + if devmode { + req.Header.Set("X-Ubuntu-Confinement", "devmode") + } + if s.storeID != "" { req.Header.Set("X-Ubuntu-Store", s.storeID) } @@ -301,7 +305,7 @@ func (s *SnapUbuntuStoreRepository) getPurchasesFromURL(url *url.URL, channel st return nil, err } - s.setUbuntuStoreHeaders(req, channel, auther) + s.setUbuntuStoreHeaders(req, channel, false, auther) resp, err := s.client.Do(req) if err != nil { @@ -414,8 +418,7 @@ func mustBuy(prices map[string]float64, purchases []*purchase) bool { } // Snap returns the snap.Info for the store hosted snap with the given name or an error. -func (s *SnapUbuntuStoreRepository) Snap(name, channel string, auther Authenticator) (*snap.Info, error) { - +func (s *SnapUbuntuStoreRepository) Snap(name, channel string, devmode bool, auther Authenticator) (*snap.Info, error) { u := *s.searchURI // make a copy, so we can mutate it q := u.Query() @@ -429,7 +432,7 @@ func (s *SnapUbuntuStoreRepository) Snap(name, channel string, auther Authentica } // set headers - s.setUbuntuStoreHeaders(req, channel, auther) + s.setUbuntuStoreHeaders(req, channel, devmode, auther) resp, err := s.client.Do(req) if err != nil { @@ -498,7 +501,7 @@ func (s *SnapUbuntuStoreRepository) Find(searchTerm string, channel string, auth } // set headers - s.setUbuntuStoreHeaders(req, channel, auther) + s.setUbuntuStoreHeaders(req, channel, false, auther) resp, err := s.client.Do(req) if err != nil { @@ -616,7 +619,7 @@ func (s *SnapUbuntuStoreRepository) ListRefresh(installed []*RefreshCandidate, a // set headers // the updates call is a special snowflake right now // (see LP: #1427155) - s.setUbuntuStoreHeaders(req, "", auther) + s.setUbuntuStoreHeaders(req, "", false, auther) resp, err := s.client.Do(req) if err != nil { @@ -672,7 +675,7 @@ func (s *SnapUbuntuStoreRepository) Download(remoteSnap *snap.Info, pbar progres if err != nil { return "", err } - s.setUbuntuStoreHeaders(req, "", auther) + s.setUbuntuStoreHeaders(req, "", remoteSnap.NeedsDevMode(), auther) if err := download(remoteSnap.Name(), w, req, pbar); err != nil { return "", err diff --git a/store/store_test.go b/store/store_test.go index 6ff660e97c..9787022b4e 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -172,15 +172,17 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryHeaders(c *C) { req, err := http.NewRequest("GET", "http://example.com", nil) c.Assert(err, IsNil) - t.store.setUbuntuStoreHeaders(req, "", nil) + t.store.setUbuntuStoreHeaders(req, "", false, nil) c.Check(req.Header.Get("X-Ubuntu-Release"), Equals, "16") c.Check(req.Header.Get("X-Ubuntu-Device-Channel"), Equals, "") + c.Check(req.Header.Get("X-Ubuntu-Confinement"), Equals, "") - t.store.setUbuntuStoreHeaders(req, "chan", nil) + t.store.setUbuntuStoreHeaders(req, "chan", true, nil) c.Check(req.Header.Get("Authorization"), Equals, "") c.Check(req.Header.Get("X-Ubuntu-Device-Channel"), Equals, "chan") + c.Check(req.Header.Get("X-Ubuntu-Confinement"), Equals, "devmode") } const ( @@ -308,6 +310,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) { q := r.URL.Query() c.Check(q.Get("q"), Equals, "package_name:\"hello-world\"") c.Check(r.Header.Get("X-Ubuntu-Device-Channel"), Equals, "edge") + c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "devmode") w.Header().Set("X-Suggested-Currency", "GBP") w.WriteHeader(http.StatusOK) @@ -327,7 +330,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) { c.Assert(repo, NotNil) // the actual test - result, err := repo.Snap("hello-world", "edge", nil) + result, err := repo.Snap("hello-world", "edge", true, nil) c.Assert(err, IsNil) c.Check(result.Name(), Equals, "hello-world") c.Check(result.Architectures, DeepEquals, []string{"all"}) @@ -388,7 +391,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsSetsAuth(c *C) { c.Assert(repo, NotNil) authenticator := &fakeAuthenticator{} - snap, err := repo.Snap("hello-world", "edge", authenticator) + snap, err := repo.Snap("hello-world", "edge", false, authenticator) c.Assert(snap, NotNil) c.Assert(err, IsNil) c.Check(snap.MustBuy, Equals, false) @@ -420,7 +423,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) { c.Assert(repo, NotNil) // the actual test - _, err = repo.Snap("hello-world", "edge", nil) + _, err = repo.Snap("hello-world", "edge", false, nil) c.Assert(err, ErrorMatches, `Ubuntu CPI service returned unexpected HTTP status code 5.. while looking for snap "hello-world" in channel "edge" \[OOPS-[a-f0-9A-F]*\]`) } @@ -473,7 +476,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) { c.Assert(repo, NotNil) // the actual test - result, err := repo.Snap("no-such-pkg", "edge", nil) + result, err := repo.Snap("no-such-pkg", "edge", false, nil) c.Assert(err, NotNil) c.Assert(result, IsNil) } @@ -1045,7 +1048,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { c.Check(repo.SuggestedCurrency(), Equals, "USD") // we should soon have a suggested currency - result, err := repo.Snap("hello-world", "edge", nil) + result, err := repo.Snap("hello-world", "edge", false, nil) c.Assert(err, IsNil) c.Assert(result, NotNil) c.Check(repo.SuggestedCurrency(), Equals, "GBP") @@ -1053,7 +1056,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { suggestedCurrency = "EUR" // checking the currency updates - result, err = repo.Snap("hello-world", "edge", nil) + result, err = repo.Snap("hello-world", "edge", false, nil) c.Assert(err, IsNil) c.Assert(result, NotNil) c.Check(repo.SuggestedCurrency(), Equals, "EUR") diff --git a/tests/abort/task.yaml b/tests/abort/task.yaml index 516718d293..01b6176515 100644 --- a/tests/abort/task.yaml +++ b/tests/abort/task.yaml @@ -5,27 +5,27 @@ execute: | echo "Abort with invalid id" invalidID="10000000" expected="error: cannot find change with id \"$invalidID\"" - actual=$(sudo snap abort $invalidID 2>&1) || EXPECTED_FAILED="abort-invalid" + actual=$(snap abort $invalidID 2>&1) || EXPECTED_FAILED="abort-invalid" [ "$EXPECTED_FAILED" = "abort-invalid" ] || exit 1 echo "$actual" | grep -Pq "$expected" || exit 1 echo "Abort with valid id - error" subdirPath="/snap/$SNAP_NAME/current/foo" - sudo mkdir -p $subdirPath + mkdir -p $subdirPath snap install $SNAP_NAME || EXPECTED_FAILED=install [ "$EXPECTED_FAILED" = "install" ] || exit 1 id="1" expected="error: cannot abort change $id with nothing pending" - actual=$(sudo snap abort $id 2>&1) || EXPECTED_FAILED="abort-error" + actual=$(snap abort $id 2>&1) || EXPECTED_FAILED="abort-error" [ "$EXPECTED_FAILED" = "abort-error" ] || exit 1 echo "$actual" | grep -Pq "$expected" || exit 1 - sudo rm -rf $subdirPath + rm -rf $subdirPath echo "Abort with valid id - done" - sudo snap install $SNAP_NAME + snap install $SNAP_NAME id="2" expected="error: cannot abort change $id with nothing pending" - actual=$(sudo snap abort $id 2>&1) || EXPECTED_FAILED="abort-done" + actual=$(snap abort $id 2>&1) || EXPECTED_FAILED="abort-done" [ "$EXPECTED_FAILED" = "abort-done" ] || exit 1 echo "$actual" | grep -Pq "$expected" || exit 1 diff --git a/tests/basics/task.yaml b/tests/basics/task.yaml deleted file mode 100644 index f87109f34a..0000000000 --- a/tests/basics/task.yaml +++ /dev/null @@ -1,13 +0,0 @@ -summary: Check that basic interactions work -execute: | - echo Core is not there by default... - snap list | grep core && exit 1 - - echo Installing something pulls it... - snap install hello-world - snap list | grep core - - echo Test a few basic properties... - hello-world.echo | grep Hello - hello-world.env | grep SNAP_NAME=hello-world - hello-world.evil && exit 1 || true diff --git a/tests/gccgo/task.yaml b/tests/gccgo/task.yaml index 68b221f48e..b023e0d0b4 100644 --- a/tests/gccgo/task.yaml +++ b/tests/gccgo/task.yaml @@ -1,14 +1,17 @@ summary: Check that snapd builds with gccgo prepare: | echo Installing gccgo-6 and pretending it is the default go - sudo apt install -y gccgo-6 - sudo ln -s /usr/bin/go-6 /usr/local/bin/go + apt install -y gccgo-6 + ln -s /usr/bin/go-6 /usr/local/bin/go restore: | rm -f /usr/local/bin/go - sudo apt-get autoremove -y gccgo-6 + apt-get autoremove -y gccgo-6 execute: | echo Ensure we really build with gccgo go version|grep gccgo echo Build the deb with gccgo and run the tests as part of the build sudo -i -u test /bin/sh -c "cd /gopath/src/github.com/snapcore/snapd && dpkg-buildpackage -tc -Zgzip" +# Tests run during package build take a while. +warn-timeout: 8m +kill-timeout: 20m diff --git a/tests/install-errors/task.yaml b/tests/install-errors/task.yaml index 81ba770814..e58e1ebab1 100644 --- a/tests/install-errors/task.yaml +++ b/tests/install-errors/task.yaml @@ -1,4 +1,22 @@ summary: Checks for cli errors installing snaps +environment: + SIDELOAD_SNAP_NAME: basic-binaries + STORE_SNAP_NAME: hello-world + SNAP_FILE: "./$[SIDELOAD_SNAP_NAME]_1.0_all.snap" + +prepare: | + echo "Given a snap with a failing command is installed" + snapbuild ../lib/snaps/$SIDELOAD_SNAP_NAME . + snap install $SNAP_FILE + + echo "And a snap from the store is installed" + snap install $STORE_SNAP_NAME + +restore: | + snap remove $SIDELOAD_SNAP_NAME + snap remove $STORE_SNAP_NAME + rm -f $SNAP_FILE + execute: | echo "Install unexisting snap prints error" expected="(?s)error: cannot perform the following tasks:\n\ @@ -7,14 +25,36 @@ execute: | [ "$EXPECTED_FAILURE" = "unexisting" ] || exit 1 echo "$actual" | grep -Pzq "$expected" + echo "============================================" + echo "Install without snap name shows error" expected="(?s)error: the required argument \`<snap>\` was not provided\n" actual=$(snap install 2>&1) || EXPECTED_FAILURE="nosnap" [ "$EXPECTED_FAILURE" = "nosnap" ] || exit 1 echo "$actual" | grep -Pzq "$expected" + echo "============================================" + echo "Install points to login when not authenticated" expected="snap login --help" actual=$(sudo -i -u test /bin/sh -c "snap install hello-world 2>&1") || EXPECTED_FAILURE="unauthenticated" [ "$EXPECTED_FAILURE" = "unauthenticated" ] || exit 1 echo "$actual" | grep -Pzq "$expected" + + echo "============================================" + + echo "When a failing command from a snap is called" + basic-binaries.fail || EXPECTED_FAILURE="command-failed" + + echo "Then it must fail" + [ "$EXPECTED_FAILURE" = "command-failed" ] || exit 1 + + echo "============================================" + + echo "When we try to install a snap already installed from the store" + snap install $STORE_SNAP_NAME || EXPECTED_FAILURE="install-failed" + + echo "Then it must fail" + [ "$EXPECTED_FAILURE" = "install-failed" ] || exit 1 + + echo "============================================" diff --git a/tests/install-sideload/task.yaml b/tests/install-sideload/task.yaml index 722a5f9160..76e77e0e2c 100644 --- a/tests/install-sideload/task.yaml +++ b/tests/install-sideload/task.yaml @@ -2,12 +2,11 @@ summary: Checks for snap sideload install prepare: | for snap in basic basic-binaries basic-desktop do - snapbuild ./../fixtures/snaps/$snap . + snapbuild ../lib/snaps/$snap . done restore: | for snap in basic basic-binaries basic-desktop do - sudo snap remove $snap rm ./${snap}_1.0_all.snap done execute: | @@ -15,16 +14,16 @@ execute: | expected="(?s)Name +Version +Rev +Developer +Notes\n\ basic +.*? *\n\ .*" - actual=$(sudo snap install ./basic_1.0_all.snap) + actual=$(snap install ./basic_1.0_all.snap) echo "$actual" | grep -Pzq "$expected" || exit 1 echo "Sideloaded snap executes commands" - sudo snap install ./basic-binaries_1.0_all.snap + snap install ./basic-binaries_1.0_all.snap basic-binaries.success [ "$(basic-binaries.echo)" = "From basic-binaries snap" ] || exit 1 echo "Sideload desktop snap" - sudo snap install ./basic-desktop_1.0_all.snap + snap install ./basic-desktop_1.0_all.snap expected="\[Desktop Entry\]\n\ Name=Echo\n\ Comment=It echos stuff\n\ diff --git a/tests/install-store/task.yaml b/tests/install-store/task.yaml index 9b24b8f6bf..c7ee93a044 100644 --- a/tests/install-store/task.yaml +++ b/tests/install-store/task.yaml @@ -1,19 +1,36 @@ summary: Checks for special cases of snap install from the store environment: SNAP_NAME: hello-world + DEVMODE_SNAP: devmode-world execute: | echo "Install from different channels" expected="(?s)Name +Version +Rev +Developer +Notes\n\ $SNAP_NAME .*? canonical +-\n" for channel in edge beta candidate stable do - actual=$(sudo snap install $SNAP_NAME --channel=$channel) + actual=$(snap install $SNAP_NAME --channel=$channel) echo "$actual" | grep -Pzq "$expected" || exit 1 - sudo snap remove $SNAP_NAME + snap remove $SNAP_NAME done - echo "Install with devmode option" + echo "Install non-devmode snap with devmode option" expected="(?s)Name +Version +Rev +Developer +Notes\n\ $SNAP_NAME .*? canonical +devmode\n" - actual=$(sudo snap install $SNAP_NAME --devmode) + actual=$(snap install $SNAP_NAME --devmode) echo "$actual" | grep -Pzq "$expected" || exit 1 + + echo "Install devmode snap without devmode option" + expected="snap not found" + actual=$(snap install --channel beta $DEVMODE_SNAP 2>&1 && exit 1 || true) + echo "$actual" | grep -Pzq "$expected" + + echo "Install devmode snap from stable" + expected="snap not found" + actual=$(snap install --devmode $DEVMODE_SNAP 2>&1 && exit 1 || true) + echo "$actual" | grep -Pzq "$expected" + + echo "Install devmode snap from beta with devmode option" + expected="(?s)Name +Version +Rev +Developer +Notes\n\ + $DEVMODE_SNAP .* +devmode" + actual=$(snap install --channel beta --devmode $DEVMODE_SNAP) + echo "$actual" | grep -Pzq "$expected" diff --git a/tests/interfaces-cli/task.yaml b/tests/interfaces-cli/task.yaml new file mode 100644 index 0000000000..38d069f966 --- /dev/null +++ b/tests/interfaces-cli/task.yaml @@ -0,0 +1,34 @@ +summary: Check the interfaces command + +environment: + SNAP_NAME: network-consumer + SNAP_FILE: "./$[SNAP_NAME]_1.0_all.snap" + PLUG: network + +prepare: | + echo "Given a snap with the $PLUG plug is installed" + snapbuild ../lib/snaps/$SNAP_NAME . + snap install $SNAP_FILE + +restore: | + rm -f $SNAP_FILE + +execute: | + expected="(?s)Slot +Plug\n\ + :$PLUG +$SNAP_NAME" + + echo "When the interfaces list is restricted by slot" + actual=$(snap interfaces -i $PLUG) + + echo "Then only the requested slots are shown" + echo "$actual" | grep -Pzq "$expected" + + echo "===============================================" + + echo "When the interfaces list is restricted by slot and snap" + actual=$(snap interfaces -i $PLUG $SNAP_NAME) + + echo "Then only the requested slots are shown" + echo "$actual" | grep -Pzq "$expected" + + echo "===============================================" diff --git a/tests/interfaces-log-observe/task.yaml b/tests/interfaces-log-observe/task.yaml new file mode 100644 index 0000000000..d86f32035a --- /dev/null +++ b/tests/interfaces-log-observe/task.yaml @@ -0,0 +1,66 @@ +summary: | + + The log-observe interface allows a snap to read system logs and set kernel + log rate-limiting. + + A snap which defines the log-observe plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + +environment: + SNAP_NAME: log-observe-consumer + SNAP_FILE: "./$[SNAP_NAME]_1.0_all.snap" + PLUG: log-observe + +kill-wait: 1m + +prepare: | + echo "Given a snap declaring the $PLUG plug is installed" + snapbuild ../lib/snaps/$SNAP_NAME . + snap install $SNAP_FILE + +restore: | + rm -f $SNAP_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :$PLUG +$SNAP_NAME" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +$SNAP_NAME:$PLUG" + + echo "Then the snap is not listed as connected" + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:$PLUG ubuntu-core:$PLUG + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the plug can be disconnected again" + snap disconnect $SNAP_NAME:$PLUG ubuntu-core:$PLUG + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:$PLUG ubuntu-core:$PLUG + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to access the system logs" + response=$(log-observe-consumer) + echo "$response" | grep -Pqz "ok\n" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:$PLUG ubuntu-core:$PLUG + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't access the system logs" + if log-observe-consumer; then + echo "System log shouldn't be accessible" && exit 1 + fi + echo "============================================" diff --git a/tests/interfaces-network-bind/task.yaml b/tests/interfaces-network-bind/task.yaml new file mode 100644 index 0000000000..9cb054f09e --- /dev/null +++ b/tests/interfaces-network-bind/task.yaml @@ -0,0 +1,78 @@ +summary: Ensure that the network-bind interface works + +details: | + The network-bind interface allows a daemon to access the network as a server. + + A snap which defines the network-bind plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be accessible by a network client. + +environment: + SNAP_NAME: network-bind-consumer + SNAP_FILE: ./$[SNAP_NAME]_1.0_all.snap + PORT: 8081 + REQUEST_FILE: ./request.txt + +prepare: | + echo "Given a snap declaring the network-bind plug is installed" + snapbuild ../lib/snaps/$SNAP_NAME . + snap install $SNAP_FILE + + echo "Given the snap's service is listening" + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + + echo "Given we store a basic HTTP request" + cat > $REQUEST_FILE <<EOF + GET / HTTP/1.0 + + EOF + +restore: | + echo "FIXME: once the state is properly reset in the upper levels this remove can go away" + snap remove $SNAP_NAME + rm -f $SNAP_FILE $REQUEST_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :network-bind +$SNAP_NAME" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +$SNAP_NAME:network-bind" + + echo "Then the snap is listed as connected" + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network-bind ubuntu-core:network-bind + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the plug can be connected again" + snap connect $SNAP_NAME:network-bind ubuntu-core:network-bind + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:network-bind ubuntu-core:network-bind + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the service is accessible by a client" + response=$(nc -w 2 localhost "$PORT" < $REQUEST_FILE) + echo "$response" | grep -Pqz "ok\n" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network-bind ubuntu-core:network-bind + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the service is not accessible by a client" + response=$(nc -w 2 localhost "$PORT" < $REQUEST_FILE) + [ "$response" = "" ] || exit 1 + + echo "============================================" diff --git a/tests/interfaces-network/task.yaml b/tests/interfaces-network/task.yaml new file mode 100644 index 0000000000..2511839e7a --- /dev/null +++ b/tests/interfaces-network/task.yaml @@ -0,0 +1,76 @@ +summary: Ensure network interface works. + +details: | + The network interface allows a snap to access the network as a client. + + A snap which defines the network plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to access network services. + +environment: + SNAP_NAME: network-consumer + SNAP_FILE: "./$[SNAP_NAME]_1.0_all.snap" + PORT: 8081 + SERVICE_FILE: "./service.sh" + SERVICE_NAME: "test-service" + +prepare: | + echo "Given a snap declaring the network plug is installed" + snapbuild ../lib/snaps/$SNAP_NAME . + snap install $SNAP_FILE + + echo "And a service is listening" + echo "#!/bin/sh\nwhile true; do echo \"HTTP/1.1 200 OK\n\nok\n\" | nc -l -p $PORT -q 1; done" > $SERVICE_FILE + chmod a+x $SERVICE_FILE + systemd-run --unit $SERVICE_NAME $SERVICE_FILE + while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done + +restore: | + systemctl stop $SERVICE_NAME + rm -f $SNAP_FILE $SERVICE_FILE + +execute: | + CONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + :network +$SNAP_NAME" + DISCONNECTED_PATTERN="(?s)Slot +Plug\n\ + .*?\n\ + - +$SNAP_NAME:network" + + echo "Then the snap is listed as connected" + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network ubuntu-core:network + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the plug can be connected again" + snap connect $SNAP_NAME:network ubuntu-core:network + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "============================================" + + echo "When the plug is connected" + snap connect $SNAP_NAME:network ubuntu-core:network + echo "$(snap interfaces)" | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to access a network service" + response=$(network-consumer http://127.0.0.1:$PORT) + echo "$response" | grep -Pqz "ok\n" + + echo "============================================" + + echo "When the plug is disconnected" + snap disconnect $SNAP_NAME:network ubuntu-core:network + echo "$(snap interfaces)" | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then snap can't access a network service" + if network-consumer http://127.0.0.1:$PORT; then + echo "Network shouldn't be accessible" && exit 1 + fi + + echo "============================================" diff --git a/tests/lib/reset.sh b/tests/lib/reset.sh new file mode 100755 index 0000000000..e6298833d8 --- /dev/null +++ b/tests/lib/reset.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e -x + +systemctl stop snapd || true +mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')" +services="$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')" +for unit in $services $mounts; do + systemctl stop $unit || true +done + +rm -f /tmp/ubuntu-core* +rm -rf /snap/* +rm -rf /var/lib/snapd/* +rm -f /etc/systemd/system/snap[-.]*.{mount,service} +rm -f /etc/systemd/system/multi-user.target.wants/snap[-.]*.{mount,service} + +if [ "$1" = "--reuse-core" ]; then + $(cd / && tar xzf $SPREAD_PATH/snapd-state.tar.gz) + mounts="$(systemctl list-unit-files | grep '^snap[-.].*\.mount' | cut -f1 -d ' ')" + services="$(systemctl list-unit-files | grep '^snap[-.].*\.service' | cut -f1 -d ' ')" + systemctl daemon-reload # Workaround for http://paste.ubuntu.com/17735820/ + for unit in $mounts $services; do + systemctl start $unit + done +fi +systemctl start snapd diff --git a/tests/fixtures/snaps/basic-binaries/bin/block b/tests/lib/snaps/basic-binaries/bin/block index 2c68e4ef68..2c68e4ef68 100755 --- a/tests/fixtures/snaps/basic-binaries/bin/block +++ b/tests/lib/snaps/basic-binaries/bin/block diff --git a/tests/fixtures/snaps/basic-binaries/bin/cat b/tests/lib/snaps/basic-binaries/bin/cat index a5ce91c395..a5ce91c395 100755 --- a/tests/fixtures/snaps/basic-binaries/bin/cat +++ b/tests/lib/snaps/basic-binaries/bin/cat diff --git a/tests/fixtures/snaps/basic-binaries/bin/echo b/tests/lib/snaps/basic-binaries/bin/echo index c9ed1b045d..c9ed1b045d 100755 --- a/tests/fixtures/snaps/basic-binaries/bin/echo +++ b/tests/lib/snaps/basic-binaries/bin/echo diff --git a/tests/fixtures/snaps/basic-binaries/bin/fail b/tests/lib/snaps/basic-binaries/bin/fail index 2bb8d868bd..2bb8d868bd 100755 --- a/tests/fixtures/snaps/basic-binaries/bin/fail +++ b/tests/lib/snaps/basic-binaries/bin/fail diff --git a/tests/fixtures/snaps/basic-binaries/bin/success b/tests/lib/snaps/basic-binaries/bin/success index c52d3c26b3..c52d3c26b3 100755 --- a/tests/fixtures/snaps/basic-binaries/bin/success +++ b/tests/lib/snaps/basic-binaries/bin/success diff --git a/tests/fixtures/snaps/basic-binaries/meta/icon.png b/tests/lib/snaps/basic-binaries/meta/icon.png Binary files differindex 1ec92f1241..1ec92f1241 100644 --- a/tests/fixtures/snaps/basic-binaries/meta/icon.png +++ b/tests/lib/snaps/basic-binaries/meta/icon.png diff --git a/tests/fixtures/snaps/basic-binaries/meta/snap.yaml b/tests/lib/snaps/basic-binaries/meta/snap.yaml index 437d0b8479..437d0b8479 100644 --- a/tests/fixtures/snaps/basic-binaries/meta/snap.yaml +++ b/tests/lib/snaps/basic-binaries/meta/snap.yaml diff --git a/tests/fixtures/snaps/basic-desktop/bin/echo b/tests/lib/snaps/basic-desktop/bin/echo index ce4b3443f8..ce4b3443f8 100755 --- a/tests/fixtures/snaps/basic-desktop/bin/echo +++ b/tests/lib/snaps/basic-desktop/bin/echo diff --git a/tests/fixtures/snaps/basic-desktop/meta/gui/echo.desktop b/tests/lib/snaps/basic-desktop/meta/gui/echo.desktop index 932e2e5ddb..932e2e5ddb 100644 --- a/tests/fixtures/snaps/basic-desktop/meta/gui/echo.desktop +++ b/tests/lib/snaps/basic-desktop/meta/gui/echo.desktop diff --git a/tests/fixtures/snaps/basic-desktop/meta/gui/icon.png b/tests/lib/snaps/basic-desktop/meta/gui/icon.png Binary files differindex 1ec92f1241..1ec92f1241 100644 --- a/tests/fixtures/snaps/basic-desktop/meta/gui/icon.png +++ b/tests/lib/snaps/basic-desktop/meta/gui/icon.png diff --git a/tests/fixtures/snaps/basic-desktop/meta/snap.yaml b/tests/lib/snaps/basic-desktop/meta/snap.yaml index 0f9ebc962a..0f9ebc962a 100644 --- a/tests/fixtures/snaps/basic-desktop/meta/snap.yaml +++ b/tests/lib/snaps/basic-desktop/meta/snap.yaml diff --git a/tests/lib/snaps/basic-hooks/meta/hooks/install b/tests/lib/snaps/basic-hooks/meta/hooks/install new file mode 100755 index 0000000000..69b2f9c96b --- /dev/null +++ b/tests/lib/snaps/basic-hooks/meta/hooks/install @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "I'm the install hook" diff --git a/tests/lib/snaps/basic-hooks/meta/hooks/upgrade b/tests/lib/snaps/basic-hooks/meta/hooks/upgrade new file mode 100755 index 0000000000..d2ce0c6440 --- /dev/null +++ b/tests/lib/snaps/basic-hooks/meta/hooks/upgrade @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "I'm the upgrade hook" diff --git a/tests/fixtures/snaps/basic/meta/icon.png b/tests/lib/snaps/basic-hooks/meta/icon.png Binary files differindex 1ec92f1241..1ec92f1241 100644 --- a/tests/fixtures/snaps/basic/meta/icon.png +++ b/tests/lib/snaps/basic-hooks/meta/icon.png diff --git a/tests/lib/snaps/basic-hooks/meta/snap.yaml b/tests/lib/snaps/basic-hooks/meta/snap.yaml new file mode 100644 index 0000000000..0bb50350b5 --- /dev/null +++ b/tests/lib/snaps/basic-hooks/meta/snap.yaml @@ -0,0 +1,2 @@ +name: basic-hooks +version: 1.0 diff --git a/tests/lib/snaps/basic/meta/icon.png b/tests/lib/snaps/basic/meta/icon.png Binary files differnew file mode 100644 index 0000000000..1ec92f1241 --- /dev/null +++ b/tests/lib/snaps/basic/meta/icon.png diff --git a/tests/fixtures/snaps/basic/meta/snap.yaml b/tests/lib/snaps/basic/meta/snap.yaml index 7664ad2283..7664ad2283 100644 --- a/tests/fixtures/snaps/basic/meta/snap.yaml +++ b/tests/lib/snaps/basic/meta/snap.yaml diff --git a/integration-tests/data/snaps/log-observe-consumer/bin/consumer b/tests/lib/snaps/log-observe-consumer/bin/consumer index a9f42be9af..3bfc478f3c 100755 --- a/integration-tests/data/snaps/log-observe-consumer/bin/consumer +++ b/tests/lib/snaps/log-observe-consumer/bin/consumer @@ -9,6 +9,7 @@ def run(): print("ok") except Exception as e: print("error accessing log") + raise if __name__ == '__main__': sys.exit(run()) diff --git a/integration-tests/data/snaps/log-observe-consumer/meta/snap.yaml b/tests/lib/snaps/log-observe-consumer/meta/snap.yaml index 600ed778a6..600ed778a6 100644 --- a/integration-tests/data/snaps/log-observe-consumer/meta/snap.yaml +++ b/tests/lib/snaps/log-observe-consumer/meta/snap.yaml diff --git a/tests/lib/snaps/network-bind-consumer/bin/consumer b/tests/lib/snaps/network-bind-consumer/bin/consumer new file mode 100755 index 0000000000..b1e5b290e2 --- /dev/null +++ b/tests/lib/snaps/network-bind-consumer/bin/consumer @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import sys +from http.server import BaseHTTPRequestHandler, HTTPServer + +class testRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + + self.send_header('Content-type', 'text/html') + self.end_headers() + + message = b"<!doctype html>ok\n" + self.wfile.write(message) + +def run(): + server_address = ('localhost', 8081) + httpd = HTTPServer(server_address, testRequestHandler) + httpd.serve_forever() + +if __name__ == '__main__': + sys.exit(run()) diff --git a/tests/lib/snaps/network-bind-consumer/meta/snap.yaml b/tests/lib/snaps/network-bind-consumer/meta/snap.yaml new file mode 100644 index 0000000000..8019de827d --- /dev/null +++ b/tests/lib/snaps/network-bind-consumer/meta/snap.yaml @@ -0,0 +1,10 @@ +name: network-bind-consumer +version: 1.0 +summary: Basic network-bind consumer snap +description: A basic snap declaring a plug on network-bind + +apps: + network-consumer: + command: bin/consumer + daemon: simple + plugs: [network-bind] diff --git a/tests/lib/snaps/network-consumer/bin/consumer b/tests/lib/snaps/network-consumer/bin/consumer new file mode 100755 index 0000000000..b09be979ba --- /dev/null +++ b/tests/lib/snaps/network-consumer/bin/consumer @@ -0,0 +1,21 @@ +#! /usr/bin/env python3 + +import sys +from socket import timeout +import urllib.request + +if len(sys.argv) > 1: + url = sys.argv[1] +else: + url = 'http://www.ubuntu.com' + +try: + response = urllib.request.urlopen(url, timeout=3) + decoded_response = response.read().decode('utf-8') + print(decoded_response, end="") +except urllib.error.URLError as e: + print("Error, reason: ", e.reason) + sys.exit(1) +except timeout: + print("request timeout") + sys.exit(1) diff --git a/tests/lib/snaps/network-consumer/meta/snap.yaml b/tests/lib/snaps/network-consumer/meta/snap.yaml new file mode 100644 index 0000000000..de97ced217 --- /dev/null +++ b/tests/lib/snaps/network-consumer/meta/snap.yaml @@ -0,0 +1,9 @@ +name: network-consumer +version: 1.0 +summary: Basic network consumer snap +description: A basic snap declaring a plug on network + +apps: + network-consumer: + command: bin/consumer + plugs: [network] diff --git a/tests/searching/task.yaml b/tests/searching/task.yaml index 3f3edc2a18..2928c19ad9 100644 --- a/tests/searching/task.yaml +++ b/tests/searching/task.yaml @@ -5,13 +5,13 @@ execute: | .*?\ hello-world +.*? *\n\ .*?\ - ubuntu-clock-app +.*? *\n\ + xkcd-webserver +.*? *\n\ .*" actual=$(snap find) echo "$actual" | grep -Pzq "$expected" || exit 1 echo "Exact matches" - for snapName in hello-world ubuntu-clock-app + for snapName in hello-world xkcd-webserver do expected="(?s)Name +Version +Developer +Notes +Summary *\n\ .*?\n\ diff --git a/tests/security-profiles/task.yaml b/tests/security-profiles/task.yaml new file mode 100644 index 0000000000..373f8326e7 --- /dev/null +++ b/tests/security-profiles/task.yaml @@ -0,0 +1,33 @@ +summary: Check security profile generation for apps and hooks. +prepare: | + for snap in basic-binaries basic-hooks + do + snapbuild ../lib/snaps/$snap . + done +restore: | + for snap in basic-binaries basic-hooks + do + rm ${snap}_1.0_all.snap + done +execute: | + seccomp_profile_directory="/var/lib/snapd/seccomp/profiles" + + echo "Security profiles are generated and loaded for apps" + snap install basic-binaries_1.0_all.snap + loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) + + for profile in snap.basic-binaries.block snap.basic-binaries.cat snap.basic-binaries.echo snap.basic-binaries.fail snap.basic-binaries.success + do + echo "$loaded_profiles" | grep -zq "$profile (enforce)" || exit 1 + [ -f "$seccomp_profile_directory/$profile" ] || exit 1 + done + + echo "Security profiles are generated and loaded for hooks" + snap install basic-hooks_1.0_all.snap + loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) + + for profile in snap.basic-hooks.hook.install snap.basic-hooks.hook.upgrade + do + echo "$loaded_profiles" | grep -zq "$profile (enforce)" || exit 1 + [ -f "$seccomp_profile_directory/$profile" ] || exit 1 + done diff --git a/tests/server-snap/task.yaml b/tests/server-snap/task.yaml index 2eaca8a11a..99c61b7a68 100644 --- a/tests/server-snap/task.yaml +++ b/tests/server-snap/task.yaml @@ -8,9 +8,9 @@ environment: IP_VERSION/goServer: 6 PORT/goServer: 8081 TEXT/goServer: Hello World -kill-timeout: 1m +warn-timeout: 3m prepare: | - sudo snap install $SNAP_NAME + snap install $SNAP_NAME cat > request.txt <<EOF GET / HTTP/1.0 @@ -18,8 +18,7 @@ prepare: | echo "Wait for the service to be listening, limited to the task kill-timeout" while ! netstat -lnt | grep -Pq "tcp.*?:$PORT +.*?LISTEN\n*"; do sleep 0.5; done restore: | - sudo snap remove $SNAP_NAME - rm request.txt + rm -f request.txt execute: | response=$(nc -"$IP_VERSION" localhost "$PORT" < request.txt) diff --git a/tests/snap-run-symlink-error/task.yaml b/tests/snap-run-symlink-error/task.yaml index 1d007df8e8..b2d1d5138f 100644 --- a/tests/snap-run-symlink-error/task.yaml +++ b/tests/snap-run-symlink-error/task.yaml @@ -12,8 +12,8 @@ execute: | # FIXME: remove "SNAP_REEXEC" once we have `snap run` inside the os snap export SNAP_REEXEC=0 echo Setting up incorrect symlink for snap run - sudo mkdir -p /snap/bin - sudo ln -s /usr/bin/snap /snap/bin/xxx + mkdir -p /snap/bin + ln -s /usr/bin/snap /snap/bin/xxx echo Running unknown command expected='internal error, please report: running "xxx" failed: cannot find snap "xxx"' output="$(/snap/bin/xxx 2>&1 )" && exit 1 diff --git a/tests/snap-run-symlink/task.yaml b/tests/snap-run-symlink/task.yaml index ac815ea701..5aee969419 100644 --- a/tests/snap-run-symlink/task.yaml +++ b/tests/snap-run-symlink/task.yaml @@ -1,30 +1,25 @@ summary: Check that symlinks to /usr/bin/snap trigger `snap run` + prepare: | echo Ensure we have a os snap with snap run - sudo snap install --channel=beta ubuntu-core - sudo snap install hello-world -restore: | - echo Resetting snapd state... - systemctl stop snapd || true - umount /var/lib/snapd/snaps/*.snap 2>&1 || true - rm -rf /snap/* - rm -rf /var/lib/snapd/* - rm -f /etc/systemd/system/snap-*.{mount,service} - rm -f /etc/systemd/system/multi-user.target.wants/snap-*.mount - systemctl start snapd + $SPREAD_PATH/tests/lib/reset.sh + snap install --channel=beta ubuntu-core + snap install hello-world + environment: APP/helloworld: hello-world APP/helloworldecho: hello-world.echo + execute: | echo Testing that replacing the wrapper with a symlink works $APP $APP > orig.txt 2>&1 - sudo rm /snap/bin/$APP - sudo ln -s /usr/bin/snap /snap/bin/$APP + rm /snap/bin/$APP + ln -s /usr/bin/snap /snap/bin/$APP # FIXME: remove "SNAP_REEXEC" once we have `snap run` inside the os snap SNAP_REEXEC=0 $APP SNAP_REEXEC=0 $APP > new.txt 2>&1 - diff -u orig.txt new.txt \ No newline at end of file + diff -u orig.txt new.txt |
