diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2016-04-26 11:34:49 +0200 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2016-04-26 11:34:49 +0200 |
| commit | fedc4a36418be1e36be8dc44549c386d31750c6e (patch) | |
| tree | 4c1ffb80576edf5a02e7119030c5cf45ad6b6d98 | |
| parent | 443b3835a5bf217a9283e43993e526f7f96be8a9 (diff) | |
| parent | 52c9a256e18452b212151b9c379080ed04554954 (diff) | |
Merge remote-tracking branch 'upstream/master' into bugfix/man-fix-lp1570280bugfix/man-fix-lp1570280
31 files changed, 1053 insertions, 587 deletions
@@ -46,7 +46,7 @@ the `-d` flag. More details on the `go get` flags are available using go help get At this point you will have the git local repository of the `snappy` source at -`$GOPATH/github.com/ubuntu-core/snappy/snappy`. The source for any +`$GOPATH/github.com/ubuntu-core/snappy`. The source for any dependent packages will also be available inside `$GOPATH`. ### Dependencies handling @@ -69,9 +69,9 @@ If the dependencies need updating To build, once the sources are available and `GOPATH` is set, you can just run - go build -o /tmp/snappy github.com/ubuntu-core/snappy/cmd/snappy + go build -o /tmp/snap github.com/ubuntu-core/snappy/cmd/snap -to get the `snappy` binary in /tmp (or without -o to get it in the current +to get the `snap` binary in /tmp (or without -o to get it in the current working directory). Alternatively: go install github.com/ubuntu-core/snappy/... @@ -108,7 +108,7 @@ You can run individual test with: If a test hangs, you can enable verbose mode: - go test -v -check.vv + go test -v -check.vv (or -check.v for less verbose output). diff --git a/daemon/api.go b/daemon/api.go index 16ede8ff7e..dc4c8e862a 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -856,11 +856,25 @@ out: snap := tmpf.Name() + origPath := "" + if len(form.Value["snap-path"]) > 0 { + origPath = form.Value["snap-path"][0] + } + + info, err := readSnapInfo(snap) + if err != nil { + return InternalError("cannot read snap file: %v", err) + } + snapName := info.Name() + state := c.d.overlord.State() state.Lock() defer state.Unlock() - msg := fmt.Sprintf(i18n.G("Install %q snap file"), snap) + msg := fmt.Sprintf(i18n.G("Install %q snap from snap file"), snapName) + if origPath != "" { + msg = fmt.Sprintf(i18n.G("Install %q snap from snap file %q"), snapName, origPath) + } chg := state.NewChange("install-snap", msg) var userID int @@ -873,7 +887,7 @@ out: err = ensureUbuntuCore(chg, userID) if err == nil { - ts, err := snapstateInstallPath(state, snap, "", flags) + ts, err := snapstateInstallPath(state, snapName, snap, "", flags) if err == nil { chg.AddAll(ts) } @@ -892,6 +906,17 @@ out: return AsyncResponse(nil, &Meta{Change: chg.ID()}) } +func readSnapInfoImpl(snapPath string) (*snap.Info, error) { + // TODO Only open if in devmode or we have the assertion proving content right. + snapf, err := snap.Open(snapPath) + if err != nil { + return nil, err + } + return snapf.Info() +} + +var readSnapInfo = readSnapInfoImpl + func iconGet(st *state.State, name string) Response { info, _, err := localSnapInfo(st, name) if err != nil { diff --git a/daemon/api_test.go b/daemon/api_test.go index 942710e2fb..868b015e92 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -100,8 +100,6 @@ func (s *apiSuite) SetUpSuite(c *check.C) { func (s *apiSuite) TearDownSuite(c *check.C) { newRemoteRepo = nil muxVars = nil - snapstateInstall = snapstate.Install - snapstateGet = snapstate.Get } func (s *apiSuite) SetUpTest(c *check.C) { @@ -126,6 +124,10 @@ func (s *apiSuite) SetUpTest(c *check.C) { func (s *apiSuite) TearDownTest(c *check.C) { s.d = nil s.restoreBackends() + snapstateInstall = snapstate.Install + snapstateGet = snapstate.Get + snapstateInstallPath = snapstate.InstallPath + readSnapInfo = readSnapInfoImpl } func (s *apiSuite) daemon(c *check.C) *Daemon { @@ -244,12 +246,12 @@ func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { EditedDescription: "description", Developer: "bar", Size: 2, - IconURL: "meta/gui/icon.svg", Revision: 20, - Prices: map[string]float64{ - "GBP": 1.23, - "EUR": 2.34, - }, + }, + IconURL: "meta/gui/icon.svg", + Prices: map[string]float64{ + "GBP": 1.23, + "EUR": 2.34, }, }} s.suggestedCurrency = "GBP" @@ -440,6 +442,7 @@ func (s *apiSuite) TestListIncludesAll(c *check.C) { "snapstateUpdate", "snapstateInstallPath", "snapstateGet", + "readSnapInfo", } c.Check(found, check.Equals, len(api)+len(exceptions), check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list. If that is not the case, please add the exception to the "exceptions" list in this test.`)) @@ -1157,9 +1160,14 @@ func (s *apiSuite) TestSideloadSnap(c *check.C) { "Content-Disposition: form-data; name=\"x\"; filename=\"x\"\r\n" + "\r\n" + "xyzzy\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap-path\"\r\n" + + "\r\n" + + "a/b/local.snap\r\n" + "----hello--\r\n" head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} - s.sideloadCheck(c, body, head, 0) + chgSummary := s.sideloadCheck(c, body, head, 0) + c.Check(chgSummary, check.Equals, `Install "local" snap from snap file "a/b/local.snap"`) } func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) { @@ -1175,10 +1183,11 @@ func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) { "----hello--\r\n" head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} // try a multipart/form-data upload - s.sideloadCheck(c, body, head, snappy.DeveloperMode) + chgSummary := s.sideloadCheck(c, body, head, snappy.DeveloperMode) + c.Check(chgSummary, check.Equals, `Install "local" snap from snap file`) } -func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snappy.InstallFlags) { +func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedFlags snappy.InstallFlags) string { d := newTestDaemon(c) d.overlord.Loop() defer d.overlord.Stop() @@ -1192,6 +1201,10 @@ func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]str // setup done installQueue := []string{} + readSnapInfo = func(path string) (*snap.Info, error) { + return &snap.Info{SuggestedName: "local"}, nil + } + snapstateGet = func(s *state.State, name string, snapst *snapstate.SnapState) error { // pretend we do not have a state for ubuntu-core return state.ErrNoState @@ -1205,14 +1218,14 @@ func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]str return state.NewTaskSet(t), nil } - snapstateInstallPath = func(s *state.State, name, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { + snapstateInstallPath = func(s *state.State, name, path, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { c.Check(flags, check.Equals, expectedFlags) - bs, err := ioutil.ReadFile(name) + bs, err := ioutil.ReadFile(path) c.Check(err, check.IsNil) c.Check(string(bs), check.Equals, "xyzzy") - installQueue = append(installQueue, name) + installQueue = append(installQueue, name+"::"+path) t := s.NewTask("fake-install-snap", "Doing a fake install") return state.NewTaskSet(t), nil } @@ -1224,10 +1237,18 @@ func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]str } rsp := sideloadSnap(snapsCmd, req).(*resp) - c.Check(rsp.Type, check.Equals, ResponseTypeAsync) + c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) c.Assert(installQueue, check.HasLen, 2) c.Check(installQueue[0], check.Equals, "ubuntu-core") - c.Check(installQueue[1], check.Matches, ".*/snapd-sideload-pkg-.*") + c.Check(installQueue[1], check.Matches, "local::.*/snapd-sideload-pkg-.*") + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + chgs := st.Changes() + c.Check(chgs, check.HasLen, 1) + c.Check(chgs[0].Kind(), check.Equals, "install-snap") + return chgs[0].Summary() } func (s *apiSuite) TestAppIconGet(c *check.C) { diff --git a/debian/snapd.dirs b/debian/snapd.dirs index f994021abc..87a5ef80ca 100644 --- a/debian/snapd.dirs +++ b/debian/snapd.dirs @@ -1,5 +1,4 @@ snap -var/lib/snapd/apparmor/additional var/lib/snapd/snaps var/lib/snapd/lib/gl usr/lib/snapd diff --git a/debian/tests/integrationtests b/debian/tests/integrationtests index b6b608b45e..a45d876afa 100644 --- a/debian/tests/integrationtests +++ b/debian/tests/integrationtests @@ -26,4 +26,4 @@ cp debian/tests/testconfig.json $GOPATH/src/github.com/ubuntu-core/snappy/integr cd $GOPATH/src/github.com/ubuntu-core/snappy go test -c ./integration-tests/tests -./tests.test -check.vv -check.f 'snapHelloWorldExampleSuite|searchSuite|installAppSuite' +./tests.test -check.vv -check.f 'snapHelloWorldExampleSuite|searchSuite|installAppSuite|networkInterfaceSuite' diff --git a/etc/profile.d/apps-bin-path.sh b/etc/profile.d/apps-bin-path.sh index 4bedd126bc..1c3743b75a 100644 --- a/etc/profile.d/apps-bin-path.sh +++ b/etc/profile.d/apps-bin-path.sh @@ -1,3 +1,3 @@ -# Expand the $PATH to include /snaps/bin which is what snappy applications +# Expand the $PATH to include /snap/bin which is what snappy applications # use PATH=$PATH:/snap/bin diff --git a/integration-tests/data/snaps/network-consumer/bin/consumer b/integration-tests/data/snaps/network-consumer/bin/consumer new file mode 100755 index 0000000000..73436c6f52 --- /dev/null +++ b/integration-tests/data/snaps/network-consumer/bin/consumer @@ -0,0 +1,10 @@ +#! /usr/bin/env python3 + +import urllib.request + +try: + response = urllib.request.urlopen('http://www.ubuntu.com') + if '<!doctype html>' in response.read().decode('utf-8'): + print('ok') +except urllib.error.URLError as e: + print("Error, reason: ", e.reason) diff --git a/integration-tests/data/snaps/network-consumer/meta/snap.yaml b/integration-tests/data/snaps/network-consumer/meta/snap.yaml new file mode 100644 index 0000000000..de97ced217 --- /dev/null +++ b/integration-tests/data/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/integration-tests/main.go b/integration-tests/main.go index 657de5ab39..5b9fccc5e3 100644 --- a/integration-tests/main.go +++ b/integration-tests/main.go @@ -35,7 +35,7 @@ import ( const ( defaultOutputDir = "/tmp/snappy-test" - defaultRelease = "rolling" + defaultRelease = "16" defaultChannel = "edge" defaultSSHPort = 22 dataOutputDir = "integration-tests/data/output/" diff --git a/integration-tests/tests/base_test.go b/integration-tests/tests/base_test.go index e639725d57..ba638dec94 100644 --- a/integration-tests/tests/base_test.go +++ b/integration-tests/tests/base_test.go @@ -143,10 +143,17 @@ func writeEnvConfig(extraEnv string) error { if err != nil { return err } + httpProxy := os.Getenv("http_proxy") + httpsProxy := os.Getenv("https_proxy") + noProxy := os.Getenv("no_proxy") cfgContent := []byte(fmt.Sprintf(`[Service] Environment="SNAPPY_TRUSTED_ACCOUNT_KEY=%s" "%s" -`, trustedKey, extraEnv)) +Environment="http_proxy=%s" +Environment="https_proxy=%s" +Environment="no_proxy=%s" +`, trustedKey, extraEnv, httpProxy, httpsProxy, noProxy)) + if err = ioutil.WriteFile("/tmp/snapd.env.conf", cfgContent, os.ModeExclusive); err != nil { return err } diff --git a/integration-tests/tests/interfaces_test.go b/integration-tests/tests/interfaces_test.go new file mode 100644 index 0000000000..40d4e787b3 --- /dev/null +++ b/integration-tests/tests/interfaces_test.go @@ -0,0 +1,115 @@ +// -*- 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 ( + "os" + + "github.com/ubuntu-core/snappy/integration-tests/testutils/build" + "github.com/ubuntu-core/snappy/integration-tests/testutils/cli" + "github.com/ubuntu-core/snappy/integration-tests/testutils/common" + "github.com/ubuntu-core/snappy/integration-tests/testutils/data" + + "gopkg.in/check.v1" +) + +var _ = check.Suite(&networkInterfaceSuite{}) + +const ( + connectedPattern = "(?ms)" + + "Slot +Plug\n" + + ".*" + + "^:network +network-consumer\n" + + ".*" + disconnectedPattern = "(?ms)" + + "Slot +Plug\n" + + ".*" + + "^:network +-\n" + + ".*" + + "^- +network-consumer:network\n" + + ".*" + networkAccessibleOutput = "ok\n" +) + +type networkInterfaceSuite struct { + common.SnappySuite + snapPath string +} + +func (s *networkInterfaceSuite) SetUpTest(c *check.C) { + s.SnappySuite.SetUpTest(c) + + var err error + s.snapPath, err = build.LocalSnap(c, data.NetworkConsumerSnapName) + c.Assert(err, check.IsNil) + + common.InstallSnap(c, s.snapPath) +} + +func (s *networkInterfaceSuite) TearDownTest(c *check.C) { + s.SnappySuite.TearDownTest(c) + + os.Remove(s.snapPath) + common.RemoveSnap(c, data.NetworkConsumerSnapName) +} + +func (s *networkInterfaceSuite) TestPlugIsAutoconnected(c *check.C) { + output, err := cli.ExecCommandErr("snap", "interfaces") + c.Assert(err, check.IsNil) + + c.Assert(output, check.Matches, connectedPattern) +} + +func (s *networkInterfaceSuite) TestPlugCanBeReconnected(c *check.C) { + _, err := cli.ExecCommandErr("sudo", "snap", "disconnect", + "network-consumer:network", "ubuntu-core:network") + c.Assert(err, check.IsNil) + + output, err := cli.ExecCommandErr("snap", "interfaces") + c.Assert(err, check.IsNil) + c.Assert(output, check.Matches, disconnectedPattern) + + output, err = cli.ExecCommandErr("sudo", "snap", "connect", + "network-consumer:network", "ubuntu-core:network") + c.Assert(err, check.IsNil) + + output, err = cli.ExecCommandErr("snap", "interfaces") + c.Assert(err, check.IsNil) + c.Assert(output, check.Matches, connectedPattern) +} + +func (s *networkInterfaceSuite) TestPlugDisconnectionDisablesFunctionality(c *check.C) { + output, err := cli.ExecCommandErr("network-consumer") + c.Assert(err, check.IsNil) + c.Assert(output, check.Equals, networkAccessibleOutput) + + _, err = cli.ExecCommandErr("sudo", "snap", "disconnect", + "network-consumer:network", "ubuntu-core:network") + c.Assert(err, check.IsNil) + + output, err = cli.ExecCommandErr("snap", "interfaces") + c.Assert(err, check.IsNil) + c.Assert(output, check.Matches, disconnectedPattern) + + output, err = cli.ExecCommandErr("network-consumer") + c.Assert(err, check.IsNil) + c.Assert(output == networkAccessibleOutput, check.Equals, false) +} diff --git a/integration-tests/tests/snap_example_test.go b/integration-tests/tests/snap_example_test.go index f73bc32425..a4c85bbef1 100644 --- a/integration-tests/tests/snap_example_test.go +++ b/integration-tests/tests/snap_example_test.go @@ -42,9 +42,7 @@ func installSnap(c *check.C, packageName string) string { cli.ExecCommand(c, "sudo", "snap", "install", "--channel", "edge", packageName) // FIXME: should `snap install` shold show a list afterards? // like `snappy install`? - // right now "snap list" on freshly booted is empty - // because u-d-f installed aren't in state - out, _ := cli.ExecCommandErr("snap", "list") + out := cli.ExecCommand(c, "snap", "list") return out } @@ -52,9 +50,7 @@ func removeSnap(c *check.C, packageName string) string { cli.ExecCommand(c, "sudo", "snap", "remove", packageName) // FIXME: should `snap remove` shold show a list afterards? // like `snappy install`? - // right now "snap list" on freshly booted is empty - // because u-d-f installed aren't in state - out, _ := cli.ExecCommandErr("snap", "list") + out := cli.ExecCommand(c, "snap", "list") return out } diff --git a/integration-tests/tests/snap_find_test.go b/integration-tests/tests/snap_find_test.go index f69551ce75..a05f2aa96c 100644 --- a/integration-tests/tests/snap_find_test.go +++ b/integration-tests/tests/snap_find_test.go @@ -34,8 +34,6 @@ type searchSuite struct { } func (s *searchSuite) TestSearchMustPrintMatch(c *check.C) { - searchOutput := cli.ExecCommand(c, "snap", "find", "hello-world") - // XXX: Summary is empty atm, waiting for store support expected := "(?ms)" + "Name +Version +Summary *\n" + @@ -43,5 +41,9 @@ func (s *searchSuite) TestSearchMustPrintMatch(c *check.C) { "^hello-world +.* *\n" + ".*" - c.Assert(searchOutput, check.Matches, expected) + for _, searchTerm := range []string{"hello-", "hello-world"} { + searchOutput := cli.ExecCommand(c, "snap", "find", searchTerm) + + c.Check(searchOutput, check.Matches, expected) + } } diff --git a/integration-tests/tests/snap_persists_test.go b/integration-tests/tests/snap_persists_test.go new file mode 100644 index 0000000000..0a92220c3f --- /dev/null +++ b/integration-tests/tests/snap_persists_test.go @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !excludeintegration,!excludereboots + +/* + * 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 ( + "os" + + "github.com/ubuntu-core/snappy/integration-tests/testutils/build" + "github.com/ubuntu-core/snappy/integration-tests/testutils/cli" + "github.com/ubuntu-core/snappy/integration-tests/testutils/common" + "github.com/ubuntu-core/snappy/integration-tests/testutils/data" + + "gopkg.in/check.v1" +) + +var _ = check.Suite(&snapPersistsSuite{}) + +type snapPersistsSuite struct { + common.SnappySuite +} + +func (s *snapPersistsSuite) TestSnapPersistAfterReboot(c *check.C) { + if common.BeforeReboot() { + snapPath, err := build.LocalSnap(c, data.BasicSnapName) + defer os.Remove(snapPath) + c.Assert(err, check.IsNil, check.Commentf("Error building local snap: %s", err)) + common.InstallSnap(c, snapPath) + + common.Reboot(c) + } else if common.AfterReboot(c) { + common.RemoveRebootMark(c) + defer common.RemoveSnap(c, data.BasicSnapName) + + output, err := cli.ExecCommandErr("snap", "list") + c.Assert(err, check.IsNil) + + expected := "(?ms)Name .*\n" + + ".*" + + "^basic +.* *\n" + + ".*" + c.Assert(output, check.Matches, expected) + } +} diff --git a/integration-tests/testutils/common/common.go b/integration-tests/testutils/common/common.go index b5b6df8c11..691d7ebff2 100644 --- a/integration-tests/testutils/common/common.go +++ b/integration-tests/testutils/common/common.go @@ -123,12 +123,7 @@ func (s *SnappySuite) SetUpTest(c *check.C) { // GetCurrentVersion returns the version of the installed and active package. func GetCurrentVersion(c *check.C, packageName string) string { - output, err := cli.ExecCommandErr("snap", "list") - if err != nil { - // XXX: right now "snap list" on freshly booted is empty - // because u-d-f installed aren't in state - return "verUnknown" - } + output := cli.ExecCommand(c, "snap", "list") pattern := "(?mU)^" + packageName + " +(.*)$" re := regexp.MustCompile(pattern) match := re.FindStringSubmatch(string(output)) @@ -220,17 +215,13 @@ func getVersionFile() string { // InstallSnap executes the required command to install the specified snap func InstallSnap(c *check.C, packageName string) string { cli.ExecCommand(c, "sudo", "snap", "install", packageName) - // XXX: right now "snap list" on freshly booted is empty - // because u-d-f installed aren't in state - out, _ := cli.ExecCommandErr("snap", "list") + out := cli.ExecCommand(c, "snap", "list") return out } // RemoveSnap executes the required command to remove the specified snap func RemoveSnap(c *check.C, packageName string) string { cli.ExecCommand(c, "sudo", "snap", "remove", packageName) - // XXX: right now "snap list" on freshly booted is empty - // because u-d-f installed aren't in state - out, _ := cli.ExecCommandErr("snap", "list") + out := cli.ExecCommand(c, "snap", "list") return out } diff --git a/integration-tests/testutils/data/data.go b/integration-tests/testutils/data/data.go index 8c9cbd104e..dc29ba282c 100644 --- a/integration-tests/testutils/data/data.go +++ b/integration-tests/testutils/data/data.go @@ -2,7 +2,7 @@ // +build !excludeintegration /* - * Copyright (C) 2015 Canonical Ltd + * 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 @@ -35,6 +35,8 @@ const ( BasicServiceSnapName = "basic-service" // BasicDesktopSnapName is the name of the snap with a desktop file BasicDesktopSnapName = "basic-desktop" + // NetworkConsumerSnapName is the name of the snap with network plug + NetworkConsumerSnapName = "network-consumer" // WrongYamlSnapName is the name of a snap with an invalid meta yaml WrongYamlSnapName = "wrong-yaml" ) diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go new file mode 100644 index 0000000000..e9e6ba856d --- /dev/null +++ b/overlord/ifacestate/handlers.go @@ -0,0 +1,311 @@ +// -*- 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 ifacestate + +import ( + "fmt" + + "gopkg.in/tomb.v2" + + "github.com/ubuntu-core/snappy/interfaces" + "github.com/ubuntu-core/snappy/logger" + "github.com/ubuntu-core/snappy/overlord/snapstate" + "github.com/ubuntu-core/snappy/overlord/state" + "github.com/ubuntu-core/snappy/snap" +) + +func (m *InterfaceManager) doSetupProfiles(task *state.Task, _ *tomb.Tomb) error { + task.State().Lock() + defer task.State().Unlock() + + // Get snap.Info from bits handed by the snap manager. + ss, err := snapstate.TaskSnapSetup(task) + if err != nil { + return err + } + snapInfo, err := snapstate.Info(task.State(), ss.Name, ss.Revision) + if err != nil { + return err + } + snap.AddImplicitSlots(snapInfo) + snapName := snapInfo.Name() + var snapState snapstate.SnapState + if err := snapstate.Get(task.State(), snapName, &snapState); err != nil { + task.Errorf("cannot get state of snap %q: %s", snapName, err) + return err + } + + // Set DevMode flag if SnapSetup.Flags indicates it should be done + // but remember the old value in the task in case we undo. + task.Set("old-devmode", snapState.DevMode()) + if ss.DevMode() { + snapState.Flags |= snapstate.DevMode + } else { + snapState.Flags &= ^snapstate.DevMode + } + snapstate.Set(task.State(), snapName, &snapState) + + // The snap may have been updated so perform the following operation to + // ensure that we are always working on the correct state: + // + // - disconnect all connections to/from the given snap + // - remembering the snaps that were affected by this operation + // - remove the (old) snap from the interfaces repository + // - add the (new) snap to the interfaces repository + // - restore connections based on what is kept in the state + // - if a connection cannot be restored then remove it from the state + // - setup the security of all the affected snaps + blacklist := m.repo.AutoConnectBlacklist(snapName) + affectedSnaps, err := m.repo.DisconnectSnap(snapName) + if err != nil { + return err + } + // XXX: what about snap renames? We should remove the old name (or switch + // to IDs in the interfaces repository) + if err := m.repo.RemoveSnap(snapName); err != nil { + return err + } + if err := m.repo.AddSnap(snapInfo); err != nil { + if _, ok := err.(*interfaces.BadInterfacesError); ok { + logger.Noticef("%s", err) + } else { + return err + } + } + if err := m.reloadConnections(snapName); err != nil { + return err + } + if err := m.autoConnect(task, snapName, blacklist); err != nil { + return err + } + if len(affectedSnaps) == 0 { + affectedSnaps = append(affectedSnaps, snapInfo) + } + for _, snapInfo := range affectedSnaps { + if err := setupSnapSecurity(task, snapInfo, m.repo); err != nil { + return state.Retry + } + } + return nil +} + +func (m *InterfaceManager) doRemoveProfiles(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + // Get SnapSetup for this snap. This is gives us the name of the snap. + snapSetup, err := snapstate.TaskSnapSetup(task) + if err != nil { + return err + } + snapName := snapSetup.Name + + // Get SnapState for this snap + var snapState snapstate.SnapState + err = snapstate.Get(st, snapName, &snapState) + if err != nil && err != state.ErrNoState { + return err + } + + // Get the old-devmode flag from the task. + // This flag is set by setup-profiles in case we have to undo. + var oldDevMode bool + err = task.Get("old-devmode", &oldDevMode) + if err != nil && err != state.ErrNoState { + return err + } + // Restore the state of DevMode flag if old-devmode was saved in the task. + if err == nil { + if oldDevMode { + snapState.Flags |= snapstate.DevMode + } else { + snapState.Flags &= ^snapstate.DevMode + } + snapstate.Set(st, snapName, &snapState) + } + + // Disconnect the snap entirely. + // This is required to remove the snap from the interface repository. + // The returned list of affected snaps will need to have its security setup + // to reflect the change. + affectedSnaps, err := m.repo.DisconnectSnap(snapName) + if err != nil { + return err + } + + // Setup security of the affected snaps. + for _, snapInfo := range affectedSnaps { + if snapInfo.Name() == snapName { + // Skip setup for the snap being removed as this is handled below. + continue + } + if err := setupSnapSecurity(task, snapInfo, m.repo); err != nil { + return state.Retry + } + } + + // Remove the snap from the interface repository. + // This discards all the plugs and slots belonging to that snap. + if err := m.repo.RemoveSnap(snapName); err != nil { + return err + } + + // Remove security artefacts of the snap. + if err := removeSnapSecurity(task, snapName); err != nil { + return state.Retry + } + + return nil +} + +func (m *InterfaceManager) doDiscardConns(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + snapSetup, err := snapstate.TaskSnapSetup(task) + if err != nil { + return err + } + + snapName := snapSetup.Name + + var snapState snapstate.SnapState + err = snapstate.Get(st, snapName, &snapState) + if err != nil && err != state.ErrNoState { + return err + } + + if err == nil && len(snapState.Sequence) != 0 { + return fmt.Errorf("cannot discard connections for snap %q while it is present", snapName) + } + conns, err := getConns(st) + if err != nil { + return err + } + removed := make(map[string]connState) + for id := range conns { + plugRef, slotRef, err := parseConnID(id) + if err != nil { + return err + } + if plugRef.Snap == snapName || slotRef.Snap == snapName { + removed[id] = conns[id] + delete(conns, id) + } + } + task.Set("removed", removed) + setConns(st, conns) + return nil +} + +func (m *InterfaceManager) undoDiscardConns(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + var removed map[string]connState + err := task.Get("removed", &removed) + if err != nil && err != state.ErrNoState { + return err + } + + conns, err := getConns(st) + if err != nil { + return err + } + + for id, connState := range removed { + conns[id] = connState + } + setConns(st, conns) + task.Set("removed", nil) + return nil +} + +func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + + conns, err := getConns(st) + if err != nil { + return err + } + + err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + return err + } + + plug := m.repo.Plug(plugRef.Snap, plugRef.Name) + slot := m.repo.Slot(slotRef.Snap, slotRef.Name) + if err := setupSnapSecurity(task, plug.Snap, m.repo); err != nil { + return state.Retry + } + if err := setupSnapSecurity(task, slot.Snap, m.repo); err != nil { + return state.Retry + } + + conns[connID(plugRef, slotRef)] = connState{Interface: plug.Interface} + setConns(st, conns) + + return nil +} + +func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error { + st := task.State() + st.Lock() + defer st.Unlock() + + plugRef, slotRef, err := getPlugAndSlotRefs(task) + if err != nil { + return err + } + + conns, err := getConns(st) + if err != nil { + return err + } + + err = m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + return err + } + + plug := m.repo.Plug(plugRef.Snap, plugRef.Name) + slot := m.repo.Slot(slotRef.Snap, slotRef.Name) + if err := setupSnapSecurity(task, plug.Snap, m.repo); err != nil { + return state.Retry + } + if err := setupSnapSecurity(task, slot.Snap, m.repo); err != nil { + return state.Retry + } + + delete(conns, connID(plugRef, slotRef)) + setConns(st, conns) + return nil +} diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go new file mode 100644 index 0000000000..949c61c419 --- /dev/null +++ b/overlord/ifacestate/helpers.go @@ -0,0 +1,225 @@ +// -*- 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 ifacestate + +import ( + "fmt" + "strings" + + "github.com/ubuntu-core/snappy/interfaces" + "github.com/ubuntu-core/snappy/interfaces/apparmor" + "github.com/ubuntu-core/snappy/interfaces/builtin" + "github.com/ubuntu-core/snappy/interfaces/dbus" + "github.com/ubuntu-core/snappy/interfaces/seccomp" + "github.com/ubuntu-core/snappy/interfaces/udev" + "github.com/ubuntu-core/snappy/logger" + "github.com/ubuntu-core/snappy/overlord/snapstate" + "github.com/ubuntu-core/snappy/overlord/state" + "github.com/ubuntu-core/snappy/snap" +) + +func (m *InterfaceManager) initialize(extra []interfaces.Interface) error { + m.state.Lock() + defer m.state.Unlock() + + if err := m.addInterfaces(extra); err != nil { + return err + } + if err := m.addSnaps(); err != nil { + return err + } + if err := m.reloadConnections(""); err != nil { + return err + } + return nil +} + +func (m *InterfaceManager) addInterfaces(extra []interfaces.Interface) error { + for _, iface := range builtin.Interfaces() { + if err := m.repo.AddInterface(iface); err != nil { + return err + } + } + for _, iface := range extra { + if err := m.repo.AddInterface(iface); err != nil { + return err + } + } + return nil +} + +func (m *InterfaceManager) addSnaps() error { + snaps, err := snapstate.ActiveInfos(m.state) + if err != nil { + return err + } + for _, snapInfo := range snaps { + snap.AddImplicitSlots(snapInfo) + if err := m.repo.AddSnap(snapInfo); err != nil { + logger.Noticef("%s", err) + } + } + return nil +} + +// reloadConnections reloads connections stored in the state in the repository. +// Using non-empty snapName the operation can be scoped to connections +// affecting a given snap. +func (m *InterfaceManager) reloadConnections(snapName string) error { + conns, err := getConns(m.state) + if err != nil { + return err + } + for id := range conns { + plugRef, slotRef, err := parseConnID(id) + if err != nil { + return err + } + if snapName != "" && plugRef.Snap != snapName && slotRef.Snap != snapName { + continue + } + err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + logger.Noticef("%s", err) + } + } + return nil +} + +func setupSnapSecurity(task *state.Task, snapInfo *snap.Info, repo *interfaces.Repository) error { + st := task.State() + var snapState snapstate.SnapState + snapName := snapInfo.Name() + if err := snapstate.Get(st, snapName, &snapState); err != nil { + task.Errorf("cannot get state of snap %q: %s", snapName, err) + return err + } + for _, backend := range securityBackends { + st.Unlock() + err := backend.Setup(snapInfo, snapState.DevMode(), repo) + st.Lock() + if err != nil { + task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err) + return err + } + } + return nil +} + +func removeSnapSecurity(task *state.Task, snapName string) error { + st := task.State() + for _, backend := range securityBackends { + st.Unlock() + err := backend.Remove(snapName) + st.Lock() + if err != nil { + task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err) + return err + } + } + return nil +} + +type connState struct { + Auto bool `json:"auto,omitempty"` + Interface string `json:"interface,omitempty"` +} + +func connID(plug *interfaces.PlugRef, slot *interfaces.SlotRef) string { + return fmt.Sprintf("%s:%s %s:%s", plug.Snap, plug.Name, slot.Snap, slot.Name) +} + +func parseConnID(conn string) (*interfaces.PlugRef, *interfaces.SlotRef, error) { + parts := strings.SplitN(conn, " ", 2) + if len(parts) != 2 { + return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) + } + plugParts := strings.SplitN(parts[0], ":", 2) + slotParts := strings.SplitN(parts[1], ":", 2) + if len(plugParts) != 2 || len(slotParts) != 2 { + return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) + } + plugRef := &interfaces.PlugRef{Snap: plugParts[0], Name: plugParts[1]} + slotRef := &interfaces.SlotRef{Snap: slotParts[0], Name: slotParts[1]} + return plugRef, slotRef, nil +} + +func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blacklist map[string]bool) error { + var conns map[string]connState + err := task.State().Get("conns", &conns) + if err != nil && err != state.ErrNoState { + return err + } + if conns == nil { + conns = make(map[string]connState) + } + // XXX: quick hack, auto-connect everything + for _, plug := range m.repo.Plugs(snapName) { + if blacklist[plug.Name] { + continue + } + candidates := m.repo.AutoConnectCandidates(snapName, plug.Name) + if len(candidates) != 1 { + continue + } + slot := candidates[0] + if err := m.repo.Connect(snapName, plug.Name, slot.Snap.Name(), slot.Name); err != nil { + task.Logf("cannot auto connect %s:%s to %s:%s: %s", + snapName, plug.Name, slot.Snap.Name(), slot.Name, err) + } + key := fmt.Sprintf("%s:%s %s:%s", snapName, plug.Name, slot.Snap.Name(), slot.Name) + conns[key] = connState{Interface: plug.Interface, Auto: true} + } + task.State().Set("conns", conns) + return nil +} + +func getPlugAndSlotRefs(task *state.Task) (*interfaces.PlugRef, *interfaces.SlotRef, error) { + var plugRef interfaces.PlugRef + var slotRef interfaces.SlotRef + if err := task.Get("plug", &plugRef); err != nil { + return nil, nil, err + } + if err := task.Get("slot", &slotRef); err != nil { + return nil, nil, err + } + return &plugRef, &slotRef, nil +} + +func getConns(st *state.State) (map[string]connState, error) { + // Get information about connections from the state + var conns map[string]connState + err := st.Get("conns", &conns) + if err != nil && err != state.ErrNoState { + return nil, fmt.Errorf("cannot obtain data about existing connections: %s", err) + } + if conns == nil { + conns = make(map[string]connState) + } + return conns, nil +} + +func setConns(st *state.State, conns map[string]connState) { + st.Set("conns", conns) +} + +var securityBackends = []interfaces.SecurityBackend{ + &apparmor.Backend{}, &seccomp.Backend{}, &dbus.Backend{}, &udev.Backend{}, +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 609b1873a2..deb6c70d0d 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -23,21 +23,10 @@ package ifacestate import ( "fmt" - "strings" - - "gopkg.in/tomb.v2" "github.com/ubuntu-core/snappy/i18n" "github.com/ubuntu-core/snappy/interfaces" - "github.com/ubuntu-core/snappy/interfaces/apparmor" - "github.com/ubuntu-core/snappy/interfaces/builtin" - "github.com/ubuntu-core/snappy/interfaces/dbus" - "github.com/ubuntu-core/snappy/interfaces/seccomp" - "github.com/ubuntu-core/snappy/interfaces/udev" - "github.com/ubuntu-core/snappy/logger" - "github.com/ubuntu-core/snappy/overlord/snapstate" "github.com/ubuntu-core/snappy/overlord/state" - "github.com/ubuntu-core/snappy/snap" ) // InterfaceManager is responsible for the maintenance of interfaces in @@ -69,372 +58,6 @@ func Manager(s *state.State, extra []interfaces.Interface) (*InterfaceManager, e return m, nil } -func (m *InterfaceManager) initialize(extra []interfaces.Interface) error { - m.state.Lock() - defer m.state.Unlock() - - if err := m.addInterfaces(extra); err != nil { - return err - } - if err := m.addSnaps(); err != nil { - return err - } - if err := m.reloadConnections(""); err != nil { - return err - } - return nil -} - -func (m *InterfaceManager) addInterfaces(extra []interfaces.Interface) error { - for _, iface := range builtin.Interfaces() { - if err := m.repo.AddInterface(iface); err != nil { - return err - } - } - for _, iface := range extra { - if err := m.repo.AddInterface(iface); err != nil { - return err - } - } - return nil -} - -func (m *InterfaceManager) addSnaps() error { - snaps, err := snapstate.ActiveInfos(m.state) - if err != nil { - return err - } - for _, snapInfo := range snaps { - snap.AddImplicitSlots(snapInfo) - if err := m.repo.AddSnap(snapInfo); err != nil { - logger.Noticef("%s", err) - } - } - return nil -} - -// reloadConnections reloads connections stored in the state in the repository. -// Using non-empty snapName the operation can be scoped to connections -// affecting a given snap. -func (m *InterfaceManager) reloadConnections(snapName string) error { - conns, err := getConns(m.state) - if err != nil { - return err - } - for id := range conns { - plugRef, slotRef, err := parseConnID(id) - if err != nil { - return err - } - if snapName != "" && plugRef.Snap != snapName && slotRef.Snap != snapName { - continue - } - err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) - if err != nil { - logger.Noticef("%s", err) - } - } - return nil -} - -func setupSnapSecurity(task *state.Task, snapInfo *snap.Info, repo *interfaces.Repository) error { - st := task.State() - var snapState snapstate.SnapState - snapName := snapInfo.Name() - if err := snapstate.Get(st, snapName, &snapState); err != nil { - task.Errorf("cannot get state of snap %q: %s", snapName, err) - return err - } - for _, backend := range securityBackends { - st.Unlock() - err := backend.Setup(snapInfo, snapState.DevMode(), repo) - st.Lock() - if err != nil { - task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err) - return err - } - } - return nil -} - -func removeSnapSecurity(task *state.Task, snapName string) error { - st := task.State() - for _, backend := range securityBackends { - st.Unlock() - err := backend.Remove(snapName) - st.Lock() - if err != nil { - task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), snapName, err) - return err - } - } - return nil -} - -func (m *InterfaceManager) doSetupProfiles(task *state.Task, _ *tomb.Tomb) error { - task.State().Lock() - defer task.State().Unlock() - - // Get snap.Info from bits handed by the snap manager. - ss, err := snapstate.TaskSnapSetup(task) - if err != nil { - return err - } - snapInfo, err := snapstate.Info(task.State(), ss.Name, ss.Revision) - if err != nil { - return err - } - snap.AddImplicitSlots(snapInfo) - snapName := snapInfo.Name() - var snapState snapstate.SnapState - if err := snapstate.Get(task.State(), snapName, &snapState); err != nil { - task.Errorf("cannot get state of snap %q: %s", snapName, err) - return err - } - - // Set DevMode flag if SnapSetup.Flags indicates it should be done - // but remember the old value in the task in case we undo. - task.Set("old-devmode", snapState.DevMode()) - if ss.DevMode() { - snapState.Flags |= snapstate.DevMode - } else { - snapState.Flags &= ^snapstate.DevMode - } - snapstate.Set(task.State(), snapName, &snapState) - - // The snap may have been updated so perform the following operation to - // ensure that we are always working on the correct state: - // - // - disconnect all connections to/from the given snap - // - remembering the snaps that were affected by this operation - // - remove the (old) snap from the interfaces repository - // - add the (new) snap to the interfaces repository - // - restore connections based on what is kept in the state - // - if a connection cannot be restored then remove it from the state - // - setup the security of all the affected snaps - blacklist := m.repo.AutoConnectBlacklist(snapName) - affectedSnaps, err := m.repo.DisconnectSnap(snapName) - if err != nil { - return err - } - // XXX: what about snap renames? We should remove the old name (or switch - // to IDs in the interfaces repository) - if err := m.repo.RemoveSnap(snapName); err != nil { - return err - } - if err := m.repo.AddSnap(snapInfo); err != nil { - if _, ok := err.(*interfaces.BadInterfacesError); ok { - logger.Noticef("%s", err) - } else { - return err - } - } - if err := m.reloadConnections(snapName); err != nil { - return err - } - if err := m.autoConnect(task, snapName, blacklist); err != nil { - return err - } - if len(affectedSnaps) == 0 { - affectedSnaps = append(affectedSnaps, snapInfo) - } - for _, snapInfo := range affectedSnaps { - if err := setupSnapSecurity(task, snapInfo, m.repo); err != nil { - return state.Retry - } - } - return nil -} - -type connState struct { - Auto bool `json:"auto,omitempty"` - Interface string `json:"interface,omitempty"` -} - -func connID(plug *interfaces.PlugRef, slot *interfaces.SlotRef) string { - return fmt.Sprintf("%s:%s %s:%s", plug.Snap, plug.Name, slot.Snap, slot.Name) -} - -func parseConnID(conn string) (*interfaces.PlugRef, *interfaces.SlotRef, error) { - parts := strings.SplitN(conn, " ", 2) - if len(parts) != 2 { - return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) - } - plugParts := strings.SplitN(parts[0], ":", 2) - slotParts := strings.SplitN(parts[1], ":", 2) - if len(plugParts) != 2 || len(slotParts) != 2 { - return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) - } - plugRef := &interfaces.PlugRef{Snap: plugParts[0], Name: plugParts[1]} - slotRef := &interfaces.SlotRef{Snap: slotParts[0], Name: slotParts[1]} - return plugRef, slotRef, nil -} - -func (m *InterfaceManager) autoConnect(task *state.Task, snapName string, blacklist map[string]bool) error { - var conns map[string]connState - err := task.State().Get("conns", &conns) - if err != nil && err != state.ErrNoState { - return err - } - if conns == nil { - conns = make(map[string]connState) - } - // XXX: quick hack, auto-connect everything - for _, plug := range m.repo.Plugs(snapName) { - if blacklist[plug.Name] { - continue - } - candidates := m.repo.AutoConnectCandidates(snapName, plug.Name) - if len(candidates) != 1 { - continue - } - slot := candidates[0] - if err := m.repo.Connect(snapName, plug.Name, slot.Snap.Name(), slot.Name); err != nil { - task.Logf("cannot auto connect %s:%s to %s:%s: %s", - snapName, plug.Name, slot.Snap.Name(), slot.Name, err) - } - key := fmt.Sprintf("%s:%s %s:%s", snapName, plug.Name, slot.Snap.Name(), slot.Name) - conns[key] = connState{Interface: plug.Interface, Auto: true} - } - task.State().Set("conns", conns) - return nil -} - -func (m *InterfaceManager) doRemoveProfiles(task *state.Task, _ *tomb.Tomb) error { - st := task.State() - st.Lock() - defer st.Unlock() - - // Get SnapSetup for this snap. This is gives us the name of the snap. - snapSetup, err := snapstate.TaskSnapSetup(task) - if err != nil { - return err - } - snapName := snapSetup.Name - - // Get SnapState for this snap - var snapState snapstate.SnapState - err = snapstate.Get(st, snapName, &snapState) - if err != nil && err != state.ErrNoState { - return err - } - - // Get the old-devmode flag from the task. - // This flag is set by setup-profiles in case we have to undo. - var oldDevMode bool - err = task.Get("old-devmode", &oldDevMode) - if err != nil && err != state.ErrNoState { - return err - } - // Restore the state of DevMode flag if old-devmode was saved in the task. - if err == nil { - if oldDevMode { - snapState.Flags |= snapstate.DevMode - } else { - snapState.Flags &= ^snapstate.DevMode - } - snapstate.Set(st, snapName, &snapState) - } - - // Disconnect the snap entirely. - // This is required to remove the snap from the interface repository. - // The returned list of affected snaps will need to have its security setup - // to reflect the change. - affectedSnaps, err := m.repo.DisconnectSnap(snapName) - if err != nil { - return err - } - - // Setup security of the affected snaps. - for _, snapInfo := range affectedSnaps { - if snapInfo.Name() == snapName { - // Skip setup for the snap being removed as this is handled below. - continue - } - if err := setupSnapSecurity(task, snapInfo, m.repo); err != nil { - return state.Retry - } - } - - // Remove the snap from the interface repository. - // This discards all the plugs and slots belonging to that snap. - if err := m.repo.RemoveSnap(snapName); err != nil { - return err - } - - // Remove security artefacts of the snap. - if err := removeSnapSecurity(task, snapName); err != nil { - return state.Retry - } - - return nil -} - -func (m *InterfaceManager) doDiscardConns(task *state.Task, _ *tomb.Tomb) error { - st := task.State() - st.Lock() - defer st.Unlock() - - snapSetup, err := snapstate.TaskSnapSetup(task) - if err != nil { - return err - } - - snapName := snapSetup.Name - - var snapState snapstate.SnapState - err = snapstate.Get(st, snapName, &snapState) - if err != nil && err != state.ErrNoState { - return err - } - - if err == nil && len(snapState.Sequence) != 0 { - return fmt.Errorf("cannot discard connections for snap %q while it is present", snapName) - } - conns, err := getConns(st) - if err != nil { - return err - } - removed := make(map[string]connState) - for id := range conns { - plugRef, slotRef, err := parseConnID(id) - if err != nil { - return err - } - if plugRef.Snap == snapName || slotRef.Snap == snapName { - removed[id] = conns[id] - delete(conns, id) - } - } - task.Set("removed", removed) - setConns(st, conns) - return nil -} - -func (m *InterfaceManager) undoDiscardConns(task *state.Task, _ *tomb.Tomb) error { - st := task.State() - st.Lock() - defer st.Unlock() - - var removed map[string]connState - err := task.Get("removed", &removed) - if err != nil && err != state.ErrNoState { - return err - } - - conns, err := getConns(st) - if err != nil { - return err - } - - for id, connState := range removed { - conns[id] = connState - } - setConns(st, conns) - task.Set("removed", nil) - return nil -} - // Connect returns a set of tasks for connecting an interface. // func Connect(s *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { @@ -461,104 +84,6 @@ func Disconnect(s *state.State, plugSnap, plugName, slotSnap, slotName string) ( return state.NewTaskSet(task), nil } -func getPlugAndSlotRefs(task *state.Task) (*interfaces.PlugRef, *interfaces.SlotRef, error) { - var plugRef interfaces.PlugRef - var slotRef interfaces.SlotRef - if err := task.Get("plug", &plugRef); err != nil { - return nil, nil, err - } - if err := task.Get("slot", &slotRef); err != nil { - return nil, nil, err - } - return &plugRef, &slotRef, nil -} - -func getConns(st *state.State) (map[string]connState, error) { - // Get information about connections from the state - var conns map[string]connState - err := st.Get("conns", &conns) - if err != nil && err != state.ErrNoState { - return nil, fmt.Errorf("cannot obtain data about existing connections: %s", err) - } - if conns == nil { - conns = make(map[string]connState) - } - return conns, nil -} - -func setConns(st *state.State, conns map[string]connState) { - st.Set("conns", conns) -} - -func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error { - st := task.State() - st.Lock() - defer st.Unlock() - - plugRef, slotRef, err := getPlugAndSlotRefs(task) - if err != nil { - return err - } - - conns, err := getConns(st) - if err != nil { - return err - } - - err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) - if err != nil { - return err - } - - plug := m.repo.Plug(plugRef.Snap, plugRef.Name) - slot := m.repo.Slot(slotRef.Snap, slotRef.Name) - if err := setupSnapSecurity(task, plug.Snap, m.repo); err != nil { - return state.Retry - } - if err := setupSnapSecurity(task, slot.Snap, m.repo); err != nil { - return state.Retry - } - - conns[connID(plugRef, slotRef)] = connState{Interface: plug.Interface} - setConns(st, conns) - - return nil -} - -func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error { - st := task.State() - st.Lock() - defer st.Unlock() - - plugRef, slotRef, err := getPlugAndSlotRefs(task) - if err != nil { - return err - } - - conns, err := getConns(st) - if err != nil { - return err - } - - err = m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) - if err != nil { - return err - } - - plug := m.repo.Plug(plugRef.Snap, plugRef.Name) - slot := m.repo.Slot(slotRef.Snap, slotRef.Name) - if err := setupSnapSecurity(task, plug.Snap, m.repo); err != nil { - return state.Retry - } - if err := setupSnapSecurity(task, slot.Snap, m.repo); err != nil { - return state.Retry - } - - delete(conns, connID(plugRef, slotRef)) - setConns(st, conns) - return nil -} - // Ensure implements StateManager.Ensure. func (m *InterfaceManager) Ensure() error { m.runner.Ensure() @@ -596,7 +121,3 @@ func MockSecurityBackends(backends []interfaces.SecurityBackend) func() { securityBackends = backends return func() { securityBackends = old } } - -var securityBackends = []interfaces.SecurityBackend{ - &apparmor.Backend{}, &seccomp.Backend{}, &dbus.Backend{}, &udev.Backend{}, -} diff --git a/overlord/ifacestate/ifacemgr_test.go b/overlord/ifacestate/ifacemgr_test.go index c756631783..030822bc20 100644 --- a/overlord/ifacestate/ifacemgr_test.go +++ b/overlord/ifacestate/ifacemgr_test.go @@ -225,12 +225,11 @@ func (s *interfaceManagerSuite) mockSnap(c *C, yamlText string) *snap.Info { return snapInfo } -func (s *interfaceManagerSuite) addSetupSnapSecurityChange(c *C, snapName string, flags snappy.InstallFlags) *state.Change { +func (s *interfaceManagerSuite) addSetupSnapSecurityChange(c *C, ss *snapstate.SnapSetup) *state.Change { s.state.Lock() defer s.state.Unlock() task := s.state.NewTask("setup-profiles", "") - ss := snapstate.SnapSetup{Name: snapName, Flags: int(flags)} task.Set("snap-setup", ss) taskset := state.NewTaskSet(task) change := s.state.NewChange("test", "") @@ -309,7 +308,8 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecurityHonorsDisconnect(c *C) { mgr := s.manager(c) // Run the setup-snap-security task and let it finish. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), snappy.InstallFlags(0)) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Revision: snapInfo.Revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -345,7 +345,8 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyAutoConnects(c *C) { snapInfo := s.mockSnap(c, sampleSnapYaml) // Run the setup-snap-security task and let it finish. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), snappy.InstallFlags(0)) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Revision: snapInfo.Revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -395,7 +396,8 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionSt s.state.Unlock() // Run the setup-snap-security task and let it finish. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), snappy.InstallFlags(0)) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Revision: snapInfo.Revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -431,7 +433,8 @@ func (s *interfaceManagerSuite) TestDoSetupProfilesAddsImplicitSlots(c *C) { snapInfo := s.mockSnap(c, osSnapYaml) // Run the setup-profiles task and let it finish. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), snappy.InstallFlags(0)) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Revision: snapInfo.Revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -449,17 +452,19 @@ func (s *interfaceManagerSuite) TestDoSetupProfilesAddsImplicitSlots(c *C) { } func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOnPlugSide(c *C) { - s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, "consumer") + snapInfo := s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, snapInfo.Name(), snapInfo.Revision) } func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOnSlotSide(c *C) { - s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, "producer") + s.mockSnap(c, consumerYaml) + snapInfo := s.mockSnap(c, producerYaml) + s.testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c, snapInfo.Name(), snapInfo.Revision) } -func (s *interfaceManagerSuite) testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c *C, snapName string) { +func (s *interfaceManagerSuite) testDoSetupSnapSecuirtyReloadsConnectionsWhenInvokedOn(c *C, snapName string, revision int) { s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) - s.mockSnap(c, consumerYaml) - s.mockSnap(c, producerYaml) s.state.Lock() s.state.Set("conns", map[string]interface{}{ @@ -470,7 +475,7 @@ func (s *interfaceManagerSuite) testDoSetupSnapSecuirtyReloadsConnectionsWhenInv mgr := s.manager(c) // Run the setup-profiles task - change := s.addSetupSnapSecurityChange(c, snapName, snappy.InstallFlags(0)) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{Name: snapName, Revision: revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -504,7 +509,8 @@ func (s *interfaceManagerSuite) TestSetupProfilesHonorsDevMode(c *C) { // Run the setup-profiles task and let it finish. // Note that the task will see SnapSetup.Flags equal to DeveloperMode. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), snappy.DeveloperMode) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Flags: int(snappy.DeveloperMode), Revision: snapInfo.Revision}) mgr.Ensure() mgr.Wait() mgr.Stop() @@ -562,7 +568,8 @@ func (s *interfaceManagerSuite) undoDevModeCheck(c *C, flags snappy.InstallFlags mgr := s.manager(c) // Run the setup-profiles task in UndoMode and let it finish. - change := s.addSetupSnapSecurityChange(c, snapInfo.Name(), flags) + change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + Name: snapInfo.Name(), Flags: int(flags), Revision: snapInfo.Revision}) s.state.Lock() task := change.Tasks()[0] // Inject the old value of DevMode flag for the task handler to restore diff --git a/overlord/snapstate/snapmgr_test.go b/overlord/snapstate/snapmgr_test.go index f521484445..a630f90729 100644 --- a/overlord/snapstate/snapmgr_test.go +++ b/overlord/snapstate/snapmgr_test.go @@ -95,7 +95,7 @@ func verifyInstallUpdateTasks(c *C, curActive bool, ts *state.TaskSet, st *state } c.Assert(ts.Tasks(), HasLen, n) // all tasks are accounted - c.Assert(st.Tasks(), HasLen, n) + c.Assert(st.NumTask(), Equals, n) c.Assert(ts.Tasks()[i].Kind(), Equals, "download-snap") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "mount-snap") @@ -138,8 +138,11 @@ func (s *snapmgrTestSuite) TestInstallConflict(c *C) { s.state.Lock() defer s.state.Unlock() - _, err := snapstate.Install(s.state, "some-snap", "some-channel", 0, 0) + ts, err := snapstate.Install(s.state, "some-snap", "some-channel", 0, 0) c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("install", "...").AddAll(ts) + _, err = snapstate.Install(s.state, "some-snap", "some-channel", 0, 0) c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) } @@ -148,11 +151,13 @@ func (s *snapmgrTestSuite) TestInstallPathConflict(c *C) { s.state.Lock() defer s.state.Unlock() - _, err := snapstate.Install(s.state, "some-snap", "some-channel", 0, 0) + ts, err := snapstate.Install(s.state, "some-snap", "some-channel", 0, 0) c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("install", "...").AddAll(ts) mockSnap := makeTestSnap(c, "name: some-snap\nversion: 1.0") - _, err = snapstate.InstallPath(s.state, mockSnap, "", 0) + _, err = snapstate.InstallPath(s.state, "some-snap", mockSnap, "", 0) c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) } @@ -206,8 +211,11 @@ func (s *snapmgrTestSuite) TestUpdateConflict(c *C) { Sequence: []*snap.SideInfo{{OfficialName: "some-snap"}}, }) - _, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0) + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0) c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("refresh", "...").AddAll(ts) + _, err = snapstate.Update(s.state, "some-snap", "some-channel", s.user.ID, 0) c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) } @@ -230,7 +238,7 @@ func (s *snapmgrTestSuite) TestRemoveTasks(c *C) { i := 0 c.Assert(ts.Tasks(), HasLen, 4) // all tasks are accounted - c.Assert(s.state.Tasks(), HasLen, 4) + c.Assert(s.state.NumTask(), Equals, 4) c.Assert(ts.Tasks()[i].Kind(), Equals, "unlink-snap") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "remove-profiles") @@ -255,7 +263,7 @@ func (s *snapmgrTestSuite) TestRemoveLast(c *C) { i := 0 c.Assert(ts.Tasks(), HasLen, 5) // all tasks are accounted - c.Assert(s.state.Tasks(), HasLen, 5) + c.Assert(s.state.NumTask(), Equals, 5) c.Assert(ts.Tasks()[i].Kind(), Equals, "unlink-snap") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "remove-profiles") @@ -276,8 +284,11 @@ func (s *snapmgrTestSuite) TestRemoveConflict(c *C) { Sequence: []*snap.SideInfo{{OfficialName: "some-snap"}}, }) - _, err := snapstate.Remove(s.state, "some-snap", 0) + ts, err := snapstate.Remove(s.state, "some-snap", 0) c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("remove", "...").AddAll(ts) + _, err = snapstate.Remove(s.state, "some-snap", 0) c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`) } @@ -772,7 +783,7 @@ func (s *snapmgrTestSuite) TestInstallFirstLocalIntegration(c *C) { mockSnap := makeTestSnap(c, `name: mock version: 1.0`) chg := s.state.NewChange("install", "install a local snap") - ts, err := snapstate.InstallPath(s.state, mockSnap, "", 0) + ts, err := snapstate.InstallPath(s.state, "mock", mockSnap, "", 0) c.Assert(err, IsNil) chg.AddAll(ts) @@ -830,7 +841,7 @@ func (s *snapmgrTestSuite) TestInstallSubequentLocalIntegration(c *C) { mockSnap := makeTestSnap(c, `name: mock version: 1.0`) chg := s.state.NewChange("install", "install a local snap") - ts, err := snapstate.InstallPath(s.state, mockSnap, "", 0) + ts, err := snapstate.InstallPath(s.state, "mock", mockSnap, "", 0) c.Assert(err, IsNil) chg.AddAll(ts) diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 8824d5b322..0468ce3c34 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -96,15 +96,6 @@ func doInstall(s *state.State, curActive bool, snapName, snapPath, channel strin return state.NewTaskSet(tasks...), nil } -func readSnapInfo(snapPath string) (*snap.Info, error) { - // TODO Only open if in devmode or we have the assertion proving content right. - snapf, err := snap.Open(snapPath) - if err != nil { - return nil, err - } - return snapf.Info() -} - func checkChangeConflict(s *state.State, snapName string) error { for _, task := range s.Tasks() { k := task.Kind() @@ -139,20 +130,14 @@ func Install(s *state.State, name, channel string, userID int, flags snappy.Inst // InstallPath returns a set of tasks for installing snap from a file path. // Note that the state must be locked by the caller. -func InstallPath(s *state.State, path, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { - info, err := readSnapInfo(path) - if err != nil { - return nil, err - } - snapName := info.Name() - +func InstallPath(s *state.State, name, path, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { var snapst SnapState - err = Get(s, snapName, &snapst) + err := Get(s, name, &snapst) if err != nil && err != state.ErrNoState { return nil, err } - return doInstall(s, snapst.Active, snapName, path, channel, 0, flags) + return doInstall(s, snapst.Active, name, path, channel, 0, flags) } // Update initiates a change updating a snap. diff --git a/overlord/state/export_test.go b/overlord/state/export_test.go index a62d6b15f1..2812b7cb4c 100644 --- a/overlord/state/export_test.go +++ b/overlord/state/export_test.go @@ -39,3 +39,8 @@ func MockChangeTimes(chg *Change, spawnTime, readyTime time.Time) { chg.spawnTime = spawnTime chg.readyTime = readyTime } + +func MockTaskTimes(t *Task, spawnTime, readyTime time.Time) { + t.spawnTime = spawnTime + t.readyTime = readyTime +} diff --git a/overlord/state/state.go b/overlord/state/state.go index e979f1b709..23323f9907 100644 --- a/overlord/state/state.go +++ b/overlord/state/state.go @@ -294,20 +294,33 @@ func (s *State) NewTask(kind, summary string) *Task { return t } -// Tasks returns all tasks currently known to the state. +// Tasks returns all tasks currently known to the state and linked to changes. func (s *State) Tasks() []*Task { s.reading() res := make([]*Task, 0, len(s.tasks)) for _, t := range s.tasks { + if t.Change() == nil { // skip unlinked tasks + continue + } res = append(res, t) } return res } -// Task returns the task for the given ID. +// Task returns the task for the given ID if the task has been linked to a change. func (s *State) Task(id string) *Task { s.reading() - return s.tasks[id] + t := s.tasks[id] + if t == nil || t.Change() == nil { + return nil + } + return t +} + +// NumTask returns the number of tasks that currently exist in the state (both linked or not yet linked to changes), useful for sanity checking. +func (s *State) NumTask() int { + s.reading() + return len(s.tasks) } func (s *State) tasksIn(tids []string) []*Task { @@ -320,6 +333,7 @@ func (s *State) tasksIn(tids []string) []*Task { // Prune removes changes that became ready for more than pruneWait // and aborts tasks spawned for more than abortWait. +// It also removes tasks unlinked to changes after pruneWait. func (s *State) Prune(pruneWait, abortWait time.Duration) { now := time.Now() pruneLimit := now.Add(-pruneWait) @@ -341,6 +355,13 @@ func (s *State) Prune(pruneWait, abortWait time.Duration) { delete(s.changes, chg.ID()) } } + for tid, t := range s.tasks { + // TODO: this could be done more aggressively + if t.Change() == nil && t.SpawnTime().Before(pruneLimit) { + s.writing() + delete(s.tasks, tid) + } + } } // ReadState returns the state deserialized from r. diff --git a/overlord/state/state_test.go b/overlord/state/state_test.go index c7490b133d..df2c39902a 100644 --- a/overlord/state/state_test.go +++ b/overlord/state/state_test.go @@ -393,9 +393,6 @@ func (ss *stateSuite) TestNewTaskAndCheckpoint(c *C) { t2ID := t2.ID() t2.WaitFor(t1) - t3 := st.NewTask("three", "3...") - t3ID := t3.ID() - // implicit checkpoint st.Unlock() @@ -445,8 +442,7 @@ func (ss *stateSuite) TestNewTaskAndCheckpoint(c *C) { for _, t := range st2.Tasks() { tasks2[t.ID()] = t } - c.Assert(tasks2, HasLen, 3) - c.Check(tasks2[t3ID].Kind(), Equals, "three") + c.Assert(tasks2, HasLen, 2) } func (ss *stateSuite) TestEnsureBefore(c *C) { @@ -498,6 +494,7 @@ func (ss *stateSuite) TestNewTaskAndTasks(c *C) { chg2 := st.NewChange("remove", "...") t21 := st.NewTask("check", "...") t22 := st.NewTask("rm", "...") + chg2.AddTask(t21) chg2.AddTask(t22) tasks := st.Tasks() @@ -515,6 +512,27 @@ func (ss *stateSuite) TestNewTaskAndTasks(c *C) { } } +func (ss *stateSuite) TestTaskNoTask(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + c.Check(st.Task("1"), IsNil) +} + +func (ss *stateSuite) TestNewTaskHiddenUntilLinked(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + t1 := st.NewTask("check", "...") + + tasks := st.Tasks() + c.Check(tasks, HasLen, 0) + + c.Check(st.Task(t1.ID()), IsNil) +} + func (ss *stateSuite) TestMethodEntrance(c *C) { st := state.New(&fakeStateBackend{}) @@ -539,6 +557,7 @@ func (ss *stateSuite) TestMethodEntrance(c *C) { func() { st.Task("foo") }, func() { st.MarshalJSON() }, func() { st.Prune(time.Hour, time.Hour) }, + func() { st.NumTask() }, } for i, f := range reads { @@ -591,6 +610,11 @@ func (ss *stateSuite) TestPrune(c *C) { chg4.AddTask(t4) state.MockChangeTimes(chg4, now.Add(-pruneWait/2), unset) + // unlinked task + t5 := st.NewTask("unliked", "...") + c.Check(st.Task(t5.ID()), IsNil) + state.MockTaskTimes(t5, now.Add(-pruneWait), now.Add(-pruneWait)) + st.Prune(pruneWait, abortWait) c.Assert(st.Change(chg1.ID()), Equals, chg1) @@ -610,4 +634,6 @@ func (ss *stateSuite) TestPrune(c *C) { c.Assert(t1.Status(), Equals, state.HoldStatus) c.Assert(t3.Status(), Equals, state.DoStatus) c.Assert(t4.Status(), Equals, state.DoStatus) + + c.Check(st.NumTask(), Equals, 3) } diff --git a/overlord/state/task.go b/overlord/state/task.go index f1e60d2a1f..26bfd3d997 100644 --- a/overlord/state/task.go +++ b/overlord/state/task.go @@ -346,6 +346,31 @@ func (ts TaskSet) WaitFor(another *Task) { } } +// WaitAll registers all the tasks in the argument set as requirements for ts +// the target set to make progress. +func (ts *TaskSet) WaitAll(anotherTs *TaskSet) { + for _, req := range anotherTs.tasks { + ts.WaitFor(req) + } +} + +// AddTask adds the the task to the task set. +func (ts *TaskSet) AddTask(task *Task) { + for _, t := range ts.tasks { + if t == task { + return + } + } + ts.tasks = append(ts.tasks, task) +} + +// AddAll adds all the tasks in the argument set to the target set ts. +func (ts *TaskSet) AddAll(anotherTs *TaskSet) { + for _, t := range anotherTs.tasks { + ts.AddTask(t) + } +} + // Tasks returns the tasks in the task set. func (ts TaskSet) Tasks() []*Task { // Return something mutable, just like every other Tasks method. diff --git a/overlord/state/task_test.go b/overlord/state/task_test.go index e1da85f19f..c72461e5f0 100644 --- a/overlord/state/task_test.go +++ b/overlord/state/task_test.go @@ -348,3 +348,44 @@ func (ts *taskSuite) TestTaskSetWaitFor(c *C) { c.Assert(t3.WaitTasks(), DeepEquals, []*state.Task{t1}) c.Assert(t1.HaltTasks(), HasLen, 2) } + +func (ts *taskSuite) TestTaskSetWaitAll(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + t1 := st.NewTask("download", "1...") + t2 := st.NewTask("check", "2...") + t3 := st.NewTask("setup", "3...") + t4 := st.NewTask("link", "4...") + ts12 := state.NewTaskSet(t1, t2) + ts34 := state.NewTaskSet(t3, t4) + ts34.WaitAll(ts12) + + c.Assert(t3.WaitTasks(), DeepEquals, []*state.Task{t1, t2}) + c.Assert(t4.WaitTasks(), DeepEquals, []*state.Task{t1, t2}) + c.Assert(t1.HaltTasks(), HasLen, 2) + c.Assert(t2.HaltTasks(), HasLen, 2) +} + +func (ts *taskSuite) TestTaskSetAddTaskAndAddAll(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + t1 := st.NewTask("download", "1...") + t2 := st.NewTask("check", "2...") + t3 := st.NewTask("setup", "3...") + t4 := st.NewTask("link", "4...") + + ts0 := state.NewTaskSet(t1) + + ts0.AddTask(t2) + ts0.AddAll(state.NewTaskSet(t3, t4)) + + // these do nothing + ts0.AddTask(t2) + ts0.AddAll(state.NewTaskSet(t3, t4)) + + c.Check(ts0.Tasks(), DeepEquals, []*state.Task{t1, t2, t3, t4}) +} diff --git a/po/snappy.pot b/po/snappy.pot index e5a2670a0b..11f25a0983 100644 --- a/po/snappy.pot +++ b/po/snappy.pot @@ -7,7 +7,7 @@ msgid "" msgstr "Project-Id-Version: snappy\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" - "POT-Creation-Date: 2016-04-18 17:13+0200\n" + "POT-Creation-Date: 2016-04-21 17:53+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -98,11 +98,15 @@ msgid "Install %q snap" msgstr "" #, c-format -msgid "Install %q snap file" +msgid "Install %q snap from %q channel" msgstr "" #, c-format -msgid "Install %q snap from %q channel" +msgid "Install %q snap from snap file" +msgstr "" + +#, c-format +msgid "Install %q snap from snap file %q" msgstr "" msgid "Install a snap to the system" diff --git a/snap/info.go b/snap/info.go index a1f139e44a..977efcc2eb 100644 --- a/snap/info.go +++ b/snap/info.go @@ -59,8 +59,18 @@ func MountDir(name string, revision int) string { return filepath.Join(dirs.SnapSnapsDir, name, strconv.Itoa(revision)) } -// SideInfo holds snap metadata that is not included in snap.yaml or for which the store is the canonical source. -// It can be marshalled both as JSON and YAML. +// 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 +// source overriding snap.yaml content. +// +// It can be marshalled and will be stored in the system state for +// each currently installed snap revision so it needs to be evolved +// carefully. +// +// Information that can be taken directly from snap.yaml or that comes +// from the store but is not required for working offline should not +// end up in SideInfo. type SideInfo struct { OfficialName string `yaml:"name,omitempty" json:"name,omitempty"` SnapID string `yaml:"snap-id" json:"snap-id"` @@ -71,9 +81,6 @@ type SideInfo struct { EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"` Size int64 `yaml:"size,omitempty" json:"size,omitempty"` Sha512 string `yaml:"sha512,omitempty" json:"sha512,omitempty"` - IconURL string `yaml:"icon-url,omitempty" json:"icon-url,omitempty"` - - Prices map[string]float64 `yaml:"prices,omitempty" json:"prices,omitempty"` } // Info provides information about snaps. @@ -96,11 +103,15 @@ type Info struct { // legacy fields collected Legacy *LegacyYaml - // The information in these fields is not present inside the snap blob itself. + // The information in all the remaining fields is not sourced from the snap blob itself. SideInfo + // The information in these fields is ephemeral, available only from the store. AnonDownloadURL string DownloadURL string + + IconURL string + Prices map[string]float64 `yaml:"prices,omitempty" json:"prices,omitempty"` } // Name returns the blessed name for the snap. diff --git a/store/snap_remote_repo.go b/store/store.go index 210321a389..67e714d04d 100644 --- a/store/snap_remote_repo.go +++ b/store/store.go @@ -262,7 +262,12 @@ func (s *SnapUbuntuStoreRepository) Snap(name, channel string, auther Authentica case resp.StatusCode == 404: return nil, ErrSnapNotFound case resp.StatusCode != 200: - return nil, fmt.Errorf("SnapUbuntuStoreRepository: unexpected HTTP status code %d while looking for snap %q/%q", resp.StatusCode, name, channel) + tpl := "Ubuntu CPI service returned unexpected HTTP status code %d while looking for snap %q in channel %q" + if oops := resp.Header.Get("X-Oops-Id"); oops != "" { + tpl += " [%s]" + return nil, fmt.Errorf(tpl, resp.StatusCode, name, channel, oops) + } + return nil, fmt.Errorf(tpl, resp.StatusCode, name, channel) } // and decode json diff --git a/store/snap_remote_repo_test.go b/store/store_test.go index 300d1c8ec3..4eb2f4eade 100644 --- a/store/snap_remote_repo_test.go +++ b/store/store_test.go @@ -304,6 +304,36 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsSetsAuth(c *C) { c.Assert(err, IsNil) } +func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, Equals, "/search") + + 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") + + w.Header().Set("X-Oops-Id", "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f") + w.WriteHeader(http.StatusInternalServerError) + + io.WriteString(w, `{"oops": "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f"}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + searchURI, err := url.Parse(mockServer.URL + "/search") + c.Assert(err, IsNil) + cfg := SnapUbuntuStoreConfig{ + SearchURI: searchURI, + } + repo := NewUbuntuStoreSnapRepository(&cfg, "") + c.Assert(repo, NotNil) + + // the actual test + _, err = repo.Snap("hello-world", "edge", 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]*\]`) +} + /* acquired via curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: rolling-core" -H "X-Ubuntu-Device-Channel: edge" 'https://search.apps.ubuntu.com/api/v1/search?q=package_name:""&fields=channel,publisher,package_name,origin,description,summary,title,icon_url,prices,content,ratings_average,version,anon_download_url,download_url,download_sha512,last_updated,binary_filesize,support_url,revision' | python -m json.tool |
