diff -Nru snapd-1.9.1.1/client/client.go snapd-1.9.2/client/client.go --- snapd-1.9.1.1/client/client.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/client/client.go 2016-04-13 15:30:28.000000000 +0000 @@ -176,13 +176,13 @@ Type string `json:"type"` } -// errorResult is the real value of response.Result when an error occurs. -// Note that only the 'message' field is unmarshaled from JSON representation. -type errorResult struct { +// Error is the real value of response.Result when an error occurs. +type Error struct { + Kind string `json:"kind"` Message string `json:"message"` } -func (e *errorResult) Error() string { +func (e *Error) Error() string { return e.Message } @@ -199,7 +199,7 @@ if rsp.Type != "error" { return nil } - var resultErr errorResult + var resultErr Error err := json.Unmarshal(rsp.Result, &resultErr) if err != nil || resultErr.Message == "" { return fmt.Errorf("server error: %q", rsp.Status) diff -Nru snapd-1.9.1.1/client/op.go snapd-1.9.2/client/op.go --- snapd-1.9.1.1/client/op.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/client/op.go 2016-04-13 15:30:28.000000000 +0000 @@ -48,7 +48,7 @@ return nil } - var res errorResult + var res Error if json.Unmarshal(op.Output, &res) != nil { return fmt.Errorf("unexpected error format: %q", op.Output) } diff -Nru snapd-1.9.1.1/cmd/snap/cmd_booted.go snapd-1.9.2/cmd/snap/cmd_booted.go --- snapd-1.9.1.1/cmd/snap/cmd_booted.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/cmd/snap/cmd_booted.go 2016-04-13 15:30:28.000000000 +0000 @@ -35,12 +35,12 @@ "internal", "internal", func() flags.Commander { - return &cmdFind{} + return &cmdBooted{} }) cmd.hidden = true } -func (x *cmdBooted) doBooted() error { +func (x *cmdBooted) Execute(args []string) error { bootloader, err := partition.FindBootloader() if err != nil { return fmt.Errorf("can not mark boot successful: %s", err) diff -Nru snapd-1.9.1.1/cmd/snap/cmd_login.go snapd-1.9.2/cmd/snap/cmd_login.go --- snapd-1.9.1.1/cmd/snap/cmd_login.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/cmd/snap/cmd_login.go 2016-04-13 15:30:28.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" + "github.com/ubuntu-core/snappy/client" "github.com/ubuntu-core/snappy/i18n" "github.com/ubuntu-core/snappy/store" ) @@ -52,28 +53,28 @@ }) } -func requestStoreTokenWith2faRetry(username, password, tokenName string) (*store.StoreToken, error) { +func requestLoginWith2faRetry(username, password string) error { + cli := Client() // first try without otp - token, err := store.RequestStoreToken(username, password, tokenName, "") + _, err := cli.Login(username, password, "") // check if we need 2fa - if err == store.ErrAuthenticationNeeds2fa { - fmt.Print(i18n.G("2fa code: ")) + if e := err.(*client.Error); e != nil && e.Kind == store.TwoFactorErrKind { + fmt.Print(i18n.G("Two-factor code: ")) reader := bufio.NewReader(os.Stdin) // the browser shows it as well (and Sergio wants to see it ;) otp, _, err := reader.ReadLine() if err != nil { - return nil, err + return err } - return store.RequestStoreToken(username, password, tokenName, string(otp)) + _, err = cli.Login(username, password, string(otp)) + return err } - return token, err + return err } func (x *cmdLogin) Execute(args []string) error { - const tokenName = "snappy login token" - username := x.Positional.UserName fmt.Print(i18n.G("Password: ")) password, err := terminal.ReadPassword(0) @@ -82,11 +83,11 @@ return err } - token, err := requestStoreTokenWith2faRetry(username, string(password), tokenName) + err = requestLoginWith2faRetry(username, string(password)) if err != nil { return err } fmt.Println(i18n.G("Login successful")) - return store.WriteStoreToken(*token) + return nil } diff -Nru snapd-1.9.1.1/cmd/snap/cmd_snap_op.go snapd-1.9.2/cmd/snap/cmd_snap_op.go --- snapd-1.9.1.1/cmd/snap/cmd_snap_op.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/cmd/snap/cmd_snap_op.go 2016-04-13 15:30:28.000000000 +0000 @@ -118,10 +118,11 @@ var err error cli := Client() - if strings.Contains(x.Positional.Snap, "/") { - uuid, err = cli.InstallSnapFile(x.Positional.Snap) + name := x.Positional.Snap + if strings.Contains(name, "/") || strings.HasSuffix(name, ".snap") || strings.Contains(name, ".snap.") { + uuid, err = cli.InstallSnapFile(name) } else { - uuid, err = cli.InstallSnap(x.Positional.Snap, x.Channel) + uuid, err = cli.InstallSnap(name, x.Channel) } if err != nil { return err diff -Nru snapd-1.9.1.1/daemon/api.go snapd-1.9.2/daemon/api.go --- snapd-1.9.1.1/daemon/api.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/daemon/api.go 2016-04-13 15:30:28.000000000 +0000 @@ -39,6 +39,7 @@ "github.com/ubuntu-core/snappy/interfaces" "github.com/ubuntu-core/snappy/lockfile" "github.com/ubuntu-core/snappy/overlord" + "github.com/ubuntu-core/snappy/overlord/ifacestate" "github.com/ubuntu-core/snappy/overlord/snapstate" "github.com/ubuntu-core/snappy/overlord/state" "github.com/ubuntu-core/snappy/progress" @@ -66,6 +67,7 @@ assertsCmd, assertsFindManyCmd, eventsCmd, + stateChangeCmd, stateChangesCmd, } @@ -144,6 +146,12 @@ GET: getEvents, } + stateChangeCmd = &Command{ + Path: "/v2/changes/{id}", + UserOK: true, + GET: getChange, + } + stateChangesCmd = &Command{ Path: "/v2/changes", UserOK: true, @@ -202,7 +210,7 @@ macaroon, err := store.RequestPackageAccessMacaroon() if err != nil { - return InternalError("cannot get package access macaroon") + return InternalError(err.Error()) } discharge, err := store.DischargeAuthCaveat(loginData.Username, loginData.Password, macaroon, loginData.Otp) @@ -211,14 +219,14 @@ Type: ResponseTypeError, Result: &errorResult{ Kind: errorKindTwoFactorRequired, - Message: "two factor authentication required", + Message: store.ErrAuthenticationNeeds2fa.Error(), }, Status: http.StatusUnauthorized, } return SyncResponse(twofactorRequiredResponse) } if err != nil { - return Unauthorized("cannot get discharge authorization") + return Unauthorized(err.Error()) } authenticatedUser := userAuthState{ @@ -890,7 +898,8 @@ // getInterfaces returns all plugs and slots. func getInterfaces(c *Command, r *http.Request) Response { - return SyncResponse(c.d.interfaces.Interfaces()) + repo := c.d.overlord.InterfaceManager().Repository() + return SyncResponse(repo.Interfaces()) } // plugJSON aids in marshaling Plug into JSON. @@ -941,12 +950,30 @@ if len(a.Plugs) > 1 || len(a.Slots) > 1 { return NotImplemented("many-to-many operations are not implemented") } + + state := c.d.overlord.State() + switch a.Action { case "connect": if len(a.Plugs) == 0 || len(a.Slots) == 0 { return BadRequest("at least one plug and slot is required") } - err := c.d.interfaces.Connect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + summary := fmt.Sprintf("Connect %s:%s to %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + state.Lock() + change := state.NewChange("connect-snap", summary) + taskset, err := ifacestate.Connect( + state, a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + if err == nil { + change.AddAll(taskset) + } + state.Unlock() + if err != nil { + return BadRequest("%v", err) + } + + state.EnsureBefore(0) + err = waitChange(change) + if err != nil { return BadRequest("%v", err) } @@ -955,7 +982,22 @@ if len(a.Plugs) == 0 || len(a.Slots) == 0 { return BadRequest("at least one plug and slot is required") } - err := c.d.interfaces.Disconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + summary := fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + state.Lock() + change := state.NewChange("disconnect-snap", summary) + taskset, err := ifacestate.Disconnect(state, + a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) + if err == nil { + change.AddAll(taskset) + } + state.Unlock() + if err != nil { + return BadRequest("%v", err) + } + + state.EnsureBefore(0) + err = waitChange(change) + if err != nil { return BadRequest("%v", err) } @@ -1012,6 +1054,7 @@ } type changeInfo struct { + ID string `json:"id"` Kind string `json:"kind"` Summary string `json:"summary"` Status string `json:"status"` @@ -1019,10 +1062,49 @@ } type taskInfo struct { - Kind string `json:"kind"` - Summary string `json:"summary"` - Status string `json:"status"` - Log []string `json:"log,omitempty"` + Kind string `json:"kind"` + Summary string `json:"summary"` + Status string `json:"status"` + Log []string `json:"log,omitempty"` + Progress [2]int `json:"progress"` +} + +func change2changeInfo(chg *state.Change) *changeInfo { + chgInfo := &changeInfo{ + ID: chg.ID(), + Kind: chg.Kind(), + Summary: chg.Summary(), + Status: chg.Status().String(), + } + tasks := chg.Tasks() + taskInfos := make([]*taskInfo, len(tasks)) + for j, t := range tasks { + cur, tot := t.Progress() + taskInfo := &taskInfo{ + Kind: t.Kind(), + Summary: t.Summary(), + Status: t.Status().String(), + Log: t.Log(), + Progress: [2]int{cur, tot}, + } + taskInfos[j] = taskInfo + } + chgInfo.Tasks = taskInfos + + return chgInfo +} + +func getChange(c *Command, r *http.Request) Response { + chID := muxVars(r)["id"] + state := c.d.overlord.State() + state.Lock() + defer state.Unlock() + chg := state.Change(chID) + if chg == nil { + return NotFound("unable to find change with id %q", chID) + } + + return SyncResponse(change2changeInfo(chg)) } func getChanges(c *Command, r *http.Request) Response { @@ -1052,24 +1134,8 @@ if !filter(chg) { continue } - chgInfo := &changeInfo{ - Kind: chg.Kind(), - Summary: chg.Summary(), - Status: chg.Status().String(), - } - tasks := chg.Tasks() - taskInfos := make([]*taskInfo, len(tasks)) - for j, t := range tasks { - taskInfo := &taskInfo{ - Kind: t.Kind(), - Summary: t.Summary(), - Status: t.Status().String(), - Log: t.Log(), - } - taskInfos[j] = taskInfo - } - chgInfo.Tasks = taskInfos - chgInfos = append(chgInfos, chgInfo) + chgInfos = append(chgInfos, change2changeInfo(chg)) } + return SyncResponse(chgInfos) } diff -Nru snapd-1.9.1.1/daemon/api_test.go snapd-1.9.2/daemon/api_test.go --- snapd-1.9.1.1/daemon/api_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/daemon/api_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -534,7 +534,7 @@ c.Check(rsp.Type, check.Equals, ResponseTypeError) c.Check(rsp.Status, check.Equals, http.StatusInternalServerError) - c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot get package access macaroon") + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot get package access macaroon") } func (s *apiSuite) TestLoginUserTwoFactorRequiredError(c *check.C) { @@ -576,7 +576,7 @@ c.Check(rsp.Type, check.Equals, ResponseTypeError) c.Check(rsp.Status, check.Equals, http.StatusUnauthorized) - c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot get discharge authorization") + c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "cannot get discharge macaroon") } func (s *apiSuite) TestSnapsInfoOnePerIntegration(c *check.C) { @@ -1485,11 +1485,12 @@ } func (s *apiSuite) TestGetPlugs(c *check.C) { - d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) - d.interfaces.AddSlot(makeSlot("interface")) - d.interfaces.Connect("producer", "plug", "consumer", "slot") + repo := s.daemon(c).overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + repo.AddSlot(makeSlot("interface")) + repo.Connect("producer", "plug", "consumer", "slot") + req, err := http.NewRequest("GET", "/v2/interfaces", nil) c.Assert(err, check.IsNil) rec := httptest.NewRecorder() @@ -1537,9 +1538,14 @@ func (s *apiSuite) TestConnectPlugSuccess(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) - d.interfaces.AddSlot(makeSlot("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + repo.AddSlot(makeSlot("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "connect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1562,7 +1568,7 @@ "status-code": 200.0, "type": "sync", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makeConnectedPlug()}, Slots: []*interfaces.Slot{makeConnectedSlot()}, }) @@ -1570,10 +1576,15 @@ func (s *apiSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "other-interface"}) - d.interfaces.AddPlug(makePlug("interface")) - d.interfaces.AddSlot(makeSlot("other-interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "other-interface"}) + repo.AddPlug(makePlug("interface")) + repo.AddSlot(makeSlot("other-interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "connect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1592,13 +1603,14 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot connect plug "producer:plug" (interface "interface") to "consumer:slot" (interface "other-interface")`, + "message": `cannot perform the following tasks: +- Connect producer:plug to consumer:slot (cannot connect plug "producer:plug" (interface "interface") to "consumer:slot" (interface "other-interface"))`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makePlug("interface")}, Slots: []*interfaces.Slot{makeSlot("other-interface")}, }) @@ -1606,8 +1618,13 @@ func (s *apiSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddSlot(makeSlot("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddSlot(makeSlot("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "connect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1626,21 +1643,27 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot connect plug "plug" from snap "producer", no such plug`, + "message": `cannot perform the following tasks: +- Connect producer:plug to consumer:slot (cannot connect plug "plug" from snap "producer", no such plug)`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Slots: []*interfaces.Slot{makeSlot("interface")}, }) } func (s *apiSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "connect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1659,23 +1682,29 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot connect plug to slot "slot" from snap "consumer", no such slot`, + "message": `cannot perform the following tasks: +- Connect producer:plug to consumer:slot (cannot connect plug to slot "slot" from snap "consumer", no such slot)`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makePlug("interface")}, }) } func (s *apiSuite) TestDisconnectPlugSuccess(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) - d.interfaces.AddSlot(makeSlot("interface")) - d.interfaces.Connect("producer", "plug", "consumer", "slot") + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + repo.AddSlot(makeSlot("interface")) + repo.Connect("producer", "plug", "consumer", "slot") + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "disconnect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1698,7 +1727,7 @@ "status-code": 200.0, "type": "sync", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makePlug("interface")}, Slots: []*interfaces.Slot{makeSlot("interface")}, }) @@ -1706,8 +1735,13 @@ func (s *apiSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddSlot(makeSlot("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddSlot(makeSlot("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "disconnect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1726,21 +1760,27 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot disconnect plug "plug" from snap "producer", no such plug`, + "message": `cannot perform the following tasks: +- Disconnect producer:plug from consumer:slot (cannot disconnect plug "plug" from snap "producer", no such plug)`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Slots: []*interfaces.Slot{makeSlot("interface")}, }) } func (s *apiSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "disconnect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1759,22 +1799,28 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot disconnect plug from slot "slot" from snap "consumer", no such slot`, + "message": `cannot perform the following tasks: +- Disconnect producer:plug from consumer:slot (cannot disconnect plug from slot "slot" from snap "consumer", no such slot)`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makePlug("interface")}, }) } func (s *apiSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { d := s.daemon(c) - d.interfaces.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) - d.interfaces.AddPlug(makePlug("interface")) - d.interfaces.AddSlot(makeSlot("interface")) + repo := d.overlord.InterfaceManager().Repository() + repo.AddInterface(&interfaces.TestInterface{InterfaceName: "interface"}) + repo.AddPlug(makePlug("interface")) + repo.AddSlot(makeSlot("interface")) + + d.overlord.Loop() + defer d.overlord.Stop() + action := &interfaceAction{ Action: "disconnect", Plugs: []plugJSON{{Snap: "producer", Name: "plug"}}, @@ -1793,13 +1839,14 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "result": map[string]interface{}{ - "message": `cannot disconnect plug "plug" from snap "producer" from slot "slot" from snap "consumer", it is not connected`, + "message": `cannot perform the following tasks: +- Disconnect producer:plug from consumer:slot (cannot disconnect plug "plug" from snap "producer" from slot "slot" from snap "consumer", it is not connected)`, }, "status": "Bad Request", "status-code": 400.0, "type": "error", }) - c.Assert(d.interfaces.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ + c.Assert(repo.Interfaces(), check.DeepEquals, &interfaces.Interfaces{ Plugs: []*interfaces.Plug{makePlug("interface")}, Slots: []*interfaces.Slot{makeSlot("interface")}, }) @@ -2091,7 +2138,7 @@ c.Assert(d.hub.SubscriberCount(), check.Equals, 1) } -func setupChanges(st *state.State) { +func setupChanges(st *state.State) []string { chg1 := st.NewChange("install", "install...") t1 := st.NewTask("download", "1...") t2 := st.NewTask("activate", "2...") @@ -2103,6 +2150,8 @@ chg2.AddTask(t3) t3.SetStatus(state.ErrorStatus) t3.Errorf("rm failed") + + return []string{chg1.ID(), chg2.ID()} } func (s *apiSuite) TestStateChangesDefaultToInProgress(c *check.C) { @@ -2126,7 +2175,7 @@ res, err := rsp.MarshalJSON() c.Assert(err, check.IsNil) - c.Check(string(res), testutil.Contains, `{"kind":"install","summary":"install...","status":"Do","tasks":[{"kind":"download","summary":"1...","status":"Do","log":["INFO: l11","INFO: l12"]}`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"kind":"download","summary":"1...","status":"Do","log":\["INFO: l11","INFO: l12"],"progress":\[0,1]}.*`) } func (s *apiSuite) TestStateChangesInProgress(c *check.C) { @@ -2150,7 +2199,7 @@ res, err := rsp.MarshalJSON() c.Assert(err, check.IsNil) - c.Check(string(res), testutil.Contains, `{"kind":"install","summary":"install...","status":"Do","tasks":[{"kind":"download","summary":"1...","status":"Do","log":["INFO: l11","INFO: l12"]}`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"kind":"download","summary":"1...","status":"Do","log":\["INFO: l11","INFO: l12"],"progress":\[0,1]}.*`) } func (s *apiSuite) TestStateChangesAll(c *check.C) { @@ -2173,8 +2222,8 @@ res, err := rsp.MarshalJSON() c.Assert(err, check.IsNil) - c.Check(string(res), testutil.Contains, `{"kind":"install","summary":"install...","status":"Do","tasks":[{"kind":"download","summary":"1...","status":"Do","log":["INFO: l11","INFO: l12"]}`) - c.Check(string(res), testutil.Contains, `{"kind":"remove","summary":"remove..","status":"Error","tasks":[{"kind":"unlink","summary":"1...","status":"Error","log":["ERROR: rm failed"]}]}`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"kind":"download","summary":"1...","status":"Do","log":\["INFO: l11","INFO: l12"],"progress":\[0,1]}.*`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"kind":"unlink","summary":"1...","status":"Error","log":\["ERROR: rm failed"],"progress":\[1,1]}]}.*`) } func (s *apiSuite) TestStateChangesReady(c *check.C) { @@ -2197,5 +2246,53 @@ res, err := rsp.MarshalJSON() c.Assert(err, check.IsNil) - c.Check(string(res), testutil.Contains, `{"kind":"remove","summary":"remove..","status":"Error","tasks":[{"kind":"unlink","summary":"1...","status":"Error","log":["ERROR: rm failed"]}]}`) + c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"kind":"unlink","summary":"1...","status":"Error","log":\["ERROR: rm failed"],"progress":\[1,1]}]}.*`) +} + +func (s *apiSuite) TestStateChange(c *check.C) { + // Setup + d := newTestDaemon(c) + st := d.overlord.State() + st.Lock() + ids := setupChanges(st) + st.Unlock() + s.vars = map[string]string{"id": ids[0]} + + // Execute + req, err := http.NewRequest("POST", "/v2/change/"+ids[0], nil) + c.Assert(err, check.IsNil) + rsp := getChange(stateChangeCmd, req).(*resp) + rec := httptest.NewRecorder() + rsp.ServeHTTP(rec, req) + + // Verify + c.Check(rec.Code, check.Equals, 200) + c.Check(rsp.Status, check.Equals, http.StatusOK) + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.NotNil) + + var body map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &body) + c.Check(err, check.IsNil) + c.Check(body["result"], check.DeepEquals, map[string]interface{}{ + "id": ids[0], + "kind": "install", + "summary": "install...", + "status": "Do", + "tasks": []interface{}{ + map[string]interface{}{ + "kind": "download", + "summary": "1...", + "status": "Do", + "log": []interface{}{"INFO: l11", "INFO: l12"}, + "progress": []interface{}{0., 1.}, + }, + map[string]interface{}{ + "kind": "activate", + "summary": "2...", + "status": "Do", + "progress": []interface{}{0., 1.}, + }, + }, + }) } diff -Nru snapd-1.9.1.1/daemon/daemon.go snapd-1.9.2/daemon/daemon.go --- snapd-1.9.1.1/daemon/daemon.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/daemon/daemon.go 2016-04-13 15:30:28.000000000 +0000 @@ -31,8 +31,6 @@ "github.com/gorilla/mux" "gopkg.in/tomb.v2" - "github.com/ubuntu-core/snappy/interfaces" - "github.com/ubuntu-core/snappy/interfaces/builtin" "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/notifications" "github.com/ubuntu-core/snappy/overlord" @@ -47,7 +45,6 @@ tomb tomb.Tomb router *mux.Router hub *notifications.Hub - interfaces *interfaces.Repository // enableInternalInterfaceActions controls if adding and removing slots and plugs is allowed. enableInternalInterfaceActions bool } @@ -258,17 +255,10 @@ if err != nil { return nil, err } - interfacesRepo := interfaces.NewRepository() - for _, iface := range builtin.Interfaces() { - if err := interfacesRepo.AddInterface(iface); err != nil { - return nil, err - } - } return &Daemon{ - overlord: ovld, - tasks: make(map[string]*Task), - hub: notifications.NewHub(), - interfaces: interfacesRepo, + overlord: ovld, + tasks: make(map[string]*Task), + hub: notifications.NewHub(), // TODO: Decide when this should be disabled by default. enableInternalInterfaceActions: true, }, nil diff -Nru snapd-1.9.1.1/daemon/response.go snapd-1.9.2/daemon/response.go --- snapd-1.9.1.1/daemon/response.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/daemon/response.go 2016-04-13 15:30:28.000000000 +0000 @@ -32,6 +32,7 @@ "github.com/ubuntu-core/snappy/asserts" "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/notifications" + "github.com/ubuntu-core/snappy/store" ) // ResponseType is the response type @@ -100,7 +101,7 @@ const ( errorKindLicenseRequired = errorKind("license-required") - errorKindTwoFactorRequired = errorKind("two-factor-required") + errorKindTwoFactorRequired = errorKind(store.TwoFactorErrKind) ) type errorValue interface{} diff -Nru snapd-1.9.1.1/debian/changelog snapd-1.9.2/debian/changelog --- snapd-1.9.1.1/debian/changelog 2016-04-12 15:19:45.000000000 +0000 +++ snapd-1.9.2/debian/changelog 2016-04-13 15:30:28.000000000 +0000 @@ -1,3 +1,32 @@ +snapd (1.9.2) xenial; urgency=medium + + * New upstream release: + - cmd/snap,daemon,store: rework login command to use daemon login + API + - store: cache suggested currency from the store + - overlord/ifacestate: modularize and extend tests + - integration-tests: reenable failure tests + - daemon: include progress in rest changes + - daemon, overlord/state: expose individual changes + - overlord/ifacestate: drop duplicate package comment + - overlord/ifacestate: allow tests to override security backends + - cmd/snap: install *.snap and *.snap.* as files too + - interfaces/apparmor: replace /var/lib/snap with /var/snap + - daemon,overlord/ifacestate: connect REST API to interfaces in the + overlord + - debian: remove unneeded dependencies from snapd + - overlord/state: checkpoint on final progress only + - osutil: introduce IsUIDInAny + - overlord/snapstate: rename GetSnapState to Get, SetSnapState to + Set + - daemon: add id to changes json + - overlord/snapstate: SetSnapState() needs locks + - overlord: fix broken tests + - overlord/snapstate,overlord/ifacestate: reimplement SnapInfo (as + Info) actually using the state + + -- Michael Vogt Wed, 13 Apr 2016 17:27:00 +0200 + snapd (1.9.1.1) xenial; urgency=medium * debian/tests/control: diff -Nru snapd-1.9.1.1/debian/control snapd-1.9.2/debian/control --- snapd-1.9.1.1/debian/control 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/debian/control 2016-04-13 15:30:28.000000000 +0000 @@ -44,8 +44,6 @@ Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, adduser, lsb-release, squashfs-tools, ubuntu-core-launcher (>= 1.0.23), - ubuntu-core-security-seccomp, ubuntu-core-security-apparmor, - ubuntu-core-security-utils, Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9) Conflicts: snappy, snap (<< 2013-11-29-1ubuntu1) diff -Nru snapd-1.9.1.1/docs/interfaces.md snapd-1.9.2/docs/interfaces.md --- snapd-1.9.1.1/docs/interfaces.md 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/docs/interfaces.md 2016-04-13 15:30:28.000000000 +0000 @@ -19,12 +19,14 @@ Can access the network as a client. Usage: common +Auto-Connect: yes ### network-bind Can access the network as a server. Usage: common +Auto-Connect: yes ### unity7 @@ -33,13 +35,15 @@ apps interfering with one another. Usage: reserved +Auto-Connect: yes -### x +### x11 Can access the X server. Restricted because X does not prevent eavesdropping or apps interfering with one another. Usage: reserved +Auto-Connect: yes ### home @@ -92,7 +96,7 @@ Usage: reserved -### snap-control +### snapd-control Can manage snaps via snapd. diff -Nru snapd-1.9.1.1/integration-tests/tests/failover_rclocal_crash_test.go snapd-1.9.2/integration-tests/tests/failover_rclocal_crash_test.go --- snapd-1.9.1.1/integration-tests/tests/failover_rclocal_crash_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/integration-tests/tests/failover_rclocal_crash_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -29,8 +29,6 @@ ) func (s *failoverSuite) TestRCLocalCrash(c *check.C) { - c.Skip("port to snapd") - breakSnap := func(snapPath string) error { targetFile := filepath.Join(snapPath, "etc", "rc.local") cli.ExecCommand(c, "sudo", "chmod", "a+xw", targetFile) diff -Nru snapd-1.9.1.1/integration-tests/tests/failover_systemd_loop_test.go snapd-1.9.2/integration-tests/tests/failover_systemd_loop_test.go --- snapd-1.9.1.1/integration-tests/tests/failover_systemd_loop_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/integration-tests/tests/failover_systemd_loop_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -77,8 +77,6 @@ } func (s *failoverSuite) TestSystemdDependencyLoop(c *check.C) { - c.Skip("port to snapd") - breakSnap := func(snapPath string) error { servicesPath := filepath.Join(snapPath, "lib", "systemd", "system") installService(c, "deadlock", deadlockService, servicesPath) diff -Nru snapd-1.9.1.1/integration-tests/tests/failover_test.go snapd-1.9.2/integration-tests/tests/failover_test.go --- snapd-1.9.1.1/integration-tests/tests/failover_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/integration-tests/tests/failover_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -25,6 +25,7 @@ "gopkg.in/check.v1" + "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/updates" ) @@ -38,10 +39,12 @@ // This is the logic common to all the failover tests. Each of them has to call this method // with the snap that will be updated and the function that changes it to fail. func (s *failoverSuite) testUpdateToBrokenVersion(c *check.C, snap string, changeFunc updates.ChangeFakeUpdateSnap) { - c.Skip("port to snapd") - snapName := strings.Split(snap, ".")[0] + // FIXME: remove once the OS snap is fixed and has a working + // "snap booted" again + cli.ExecCommand(c, "sudo", "snap", "booted") + if common.BeforeReboot() { currentVersion := common.GetCurrentVersion(c, snapName) diff -Nru snapd-1.9.1.1/integration-tests/tests/failover_zero_size_file_test.go snapd-1.9.2/integration-tests/tests/failover_zero_size_file_test.go --- snapd-1.9.1.1/integration-tests/tests/failover_zero_size_file_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/integration-tests/tests/failover_zero_size_file_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -64,8 +64,6 @@ */ func (s *failoverSuite) TestZeroSizeInitrd(c *check.C) { - c.Skip("port to snapd") - breakSnap := func(snapPath string) error { fullPath, error := filepath.EvalSymlinks(filepath.Join(snapPath, "initrd.img")) if error != nil { @@ -80,8 +78,6 @@ } func (s *failoverSuite) TestZeroSizeSystemd(c *check.C) { - c.Skip("port to snapd") - breakSnap := func(snapPath string) error { fullPath := filepath.Join(snapPath, "lib", "systemd", "systemd") replaceWithZeroSizeFile(c, fullPath) diff -Nru snapd-1.9.1.1/integration-tests/tests/login_test.go snapd-1.9.2/integration-tests/tests/login_test.go --- snapd-1.9.1.1/integration-tests/tests/login_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/integration-tests/tests/login_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -58,20 +58,28 @@ } func (s *loginSuite) TestEmptyLoginNameError(c *check.C) { - output, err := cli.ExecCommandErr("snap", "login") + output, err := cli.ExecCommandErr("sudo", "snap", "login") c.Assert(err, check.NotNil, check.Commentf("expecting empty login error")) c.Assert(output, check.Equals, "error: the required argument `userid` was not provided\n") } -func (s *loginSuite) TestInvalidCredentialsError(c *check.C) { +func (s *loginSuite) TestInvalidLoginError(c *check.C) { err := s.writeCredentials(invalidLoginName) c.Assert(err, check.IsNil, check.Commentf("error writting credentials")) - expectedMsg := "invalid credentials" + expectedMsg := "Invalid request data" err = wait.ForFunction(c, expectedMsg, func() (string, error) { return s.stdout.String(), err }) - c.Assert(err, check.IsNil, check.Commentf("didn't get expected invalid credentials error")) + c.Assert(err, check.IsNil, check.Commentf("didn't get expected invalid data error: %v", err)) +} +func (s *loginSuite) TestInvalidCredentialsError(c *check.C) { + err := s.writeCredentials(validLoginName) + c.Assert(err, check.IsNil, check.Commentf("error writting credentials")) + + expectedMsg := "Provided email/password is not correct" + err = wait.ForFunction(c, expectedMsg, func() (string, error) { return s.stdout.String(), err }) + c.Assert(err, check.IsNil, check.Commentf("didn't get expected invalid credentials error: %v", err)) } func (s *loginSuite) TestFakeServerIsDetected(c *check.C) { @@ -83,9 +91,9 @@ err := s.writeCredentials(validLoginName) c.Assert(err, check.IsNil, check.Commentf("error writting credentials")) - expectedMsg := fmt.Sprintf("Post https://%s/api/v2/tokens/oauth: x509: certificate is valid for example.com, not %s", loginHost, loginHost) + expectedMsg := fmt.Sprintf("Post https://%s/api/v2/tokens/discharge: x509: certificate is valid for example.com, not %s", loginHost, loginHost) err = wait.ForFunction(c, expectedMsg, func() (string, error) { return s.stdout.String(), err }) - c.Assert(err, check.IsNil, check.Commentf("didn't get expected fake server error")) + c.Assert(err, check.IsNil, check.Commentf("didn't get expected fake server error: %v", err)) } func (s *loginSuite) handler(w http.ResponseWriter, r *http.Request) { @@ -132,7 +140,7 @@ } func (s *loginSuite) writeCredentials(loginName string) error { - cmds, _ := cli.AddOptionsToCommand([]string{"snap", "login", loginName}) + cmds, _ := cli.AddOptionsToCommand([]string{"sudo", "snap", "login", loginName}) cmd := exec.Command(cmds[0], cmds[1:]...) f, err := pty.Start(cmd) if err != nil { diff -Nru snapd-1.9.1.1/interfaces/apparmor/template.go snapd-1.9.2/interfaces/apparmor/template.go --- snapd-1.9.1.1/interfaces/apparmor/template.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/interfaces/apparmor/template.go 2016-04-13 15:30:28.000000000 +0000 @@ -232,11 +232,11 @@ owner @{HOME}/snap/@{APP_PKGNAME}/@{APP_VERSION}/** wl, # Read-only system area for other versions - /var/lib/snap/@{APP_PKGNAME}/ r, - /var/lib/snap/@{APP_PKGNAME}/** mrkix, + /var/snap/@{APP_PKGNAME}/ r, + /var/snap/@{APP_PKGNAME}/** mrkix, # Writable system area only for this version - /var/lib/snap/@{APP_PKGNAME}/@{APP_VERSION}/** wl, + /var/snap/@{APP_PKGNAME}/@{APP_VERSION}/** wl, # The ubuntu-core-launcher creates an app-specific private restricted /tmp # and will fail to launch the app if something goes wrong. As such, we can diff -Nru snapd-1.9.1.1/osutil/group.go snapd-1.9.2/osutil/group.go --- snapd-1.9.1.1/osutil/group.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/osutil/group.go 2016-04-13 15:30:28.000000000 +0000 @@ -19,6 +19,11 @@ package osutil +import ( + "os/user" + "strconv" +) + // Group implements the grp.h struct group type Group struct { Name string @@ -31,3 +36,37 @@ func Getgrnam(name string) (result Group, err error) { return getgrnam(name) } + +// IsUIDInAny checks whether the given user belongs to any of the +// given groups +func IsUIDInAny(uid uint32, groups ...string) bool { + usr, err := user.LookupId(strconv.FormatUint(uint64(uid), 10)) + if err != nil { + return false + } + + gid, err := strconv.ParseUint(usr.Gid, 10, 32) + if err != nil { + return false + } + + // XXX cache the Getgrnam calls for a second or so? + for _, groupname := range groups { + group, err := Getgrnam(groupname) + if err != nil { + continue + } + + if group.Gid == uint(gid) { + return true + } + + for _, member := range group.Mem { + if member == usr.Username { + return true + } + } + } + + return false +} diff -Nru snapd-1.9.1.1/osutil/group_linux.go snapd-1.9.2/osutil/group_linux.go --- snapd-1.9.1.1/osutil/group_linux.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/osutil/group_linux.go 2016-04-13 15:30:28.000000000 +0000 @@ -31,7 +31,6 @@ import ( "fmt" - "syscall" "unsafe" ) @@ -50,9 +49,8 @@ defer C.free(buf) // getgrnam_r is harder to use (from cgo), but it is thread safe - rv := C.getgrnam_r(nameC, &cgrp, (*C.char)(buf), C.size_t(bufSize), &result) - if rv != 0 { - return grp, fmt.Errorf("getgrnam_r failed for %s: %s", name, syscall.Errno(rv)) + if _, err := C.getgrnam_r(nameC, &cgrp, (*C.char)(buf), C.size_t(bufSize), &result); err != nil { + return grp, fmt.Errorf("getgrnam_r failed for %q: %v", name, err) } if result == nil { return grp, fmt.Errorf("group %q not found", name) diff -Nru snapd-1.9.1.1/osutil/group_linux_test.go snapd-1.9.2/osutil/group_linux_test.go --- snapd-1.9.1.1/osutil/group_linux_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/osutil/group_linux_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -82,3 +82,15 @@ c.Assert(err, IsNil) c.Assert(groups, DeepEquals, expected) } + +func (s *groupTestSuite) TestIsUIDInAnyEmpty(c *C) { + c.Check(IsUIDInAny(0), Equals, false) +} + +func (s *groupTestSuite) TestIsUIDInAnyBad(c *C) { + c.Check(IsUIDInAny(0, "no-such-group-really-no-no"), Equals, false) +} + +func (s *groupTestSuite) TestIsUIDInAnySelf(c *C) { + c.Check(IsUIDInAny(0, "root"), Equals, true) +} diff -Nru snapd-1.9.1.1/overlord/ifacestate/export_test.go snapd-1.9.2/overlord/ifacestate/export_test.go --- snapd-1.9.1.1/overlord/ifacestate/export_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/ifacestate/export_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -21,8 +21,10 @@ import ( "github.com/ubuntu-core/snappy/interfaces" + "github.com/ubuntu-core/snappy/snap" ) -func (m *InterfaceManager) Repository() *interfaces.Repository { - return m.repo +func MockSecurityBackendsForSnap(fn func(snapInfo *snap.Info) []interfaces.SecurityBackend) func() { + securityBackendsForSnap = fn + return func() { securityBackendsForSnap = securityBackendsForSnapImpl } } diff -Nru snapd-1.9.1.1/overlord/ifacestate/ifacemgr.go snapd-1.9.2/overlord/ifacestate/ifacemgr.go --- snapd-1.9.1.1/overlord/ifacestate/ifacemgr.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/ifacestate/ifacemgr.go 2016-04-13 15:30:28.000000000 +0000 @@ -108,7 +108,7 @@ if err != nil { return err } - snapInfo, err := snapstate.SnapInfo(task.State(), ss.Name, ss.Revision) + snapInfo, err := snapstate.Info(task.State(), ss.Name, ss.Revision) if err != nil { return err } @@ -200,7 +200,7 @@ if err != nil { return err } - snapInfo, err := snapstate.SnapInfo(task.State(), ss.Name, ss.Revision) + snapInfo, err := snapstate.Info(task.State(), ss.Name, ss.Revision) if err != nil { return err } @@ -227,7 +227,7 @@ return nil } -func securityBackendsForSnap(snapInfo *snap.Info) []interfaces.SecurityBackend { +func securityBackendsForSnapImpl(snapInfo *snap.Info) []interfaces.SecurityBackend { aaBackend := &apparmor.Backend{} // TODO: Implement special provisions for apparmor and old-security when // old-security becomes a real interface. When that happens we nee to call @@ -237,6 +237,8 @@ aaBackend, &seccomp.Backend{}, &dbus.Backend{}, &udev.Backend{}} } +var securityBackendsForSnap = securityBackendsForSnapImpl + // Connect returns a set of tasks for connecting an interface. // func Connect(s *state.State, plugSnap, plugName, slotSnap, slotName string) (*state.TaskSet, error) { @@ -313,3 +315,15 @@ m.runner.Stop() } + +// Repository returns the interface repository used internally by the manager. +// +// This method has two use-cases: +// - it is needed for setting up state in daemon tests +// - it is needed to return the set of known interfaces in the daemon api +// +// In the second case it is only informational and repository has internal +// locks to ensure consistency. +func (m *InterfaceManager) Repository() *interfaces.Repository { + return m.repo +} diff -Nru snapd-1.9.1.1/overlord/ifacestate/ifacemgr_test.go snapd-1.9.2/overlord/ifacestate/ifacemgr_test.go --- snapd-1.9.1.1/overlord/ifacestate/ifacemgr_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/ifacestate/ifacemgr_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -23,6 +23,7 @@ "io/ioutil" "os" "path/filepath" + "strconv" "testing" . "gopkg.in/check.v1" @@ -33,14 +34,14 @@ "github.com/ubuntu-core/snappy/overlord/snapstate" "github.com/ubuntu-core/snappy/overlord/state" "github.com/ubuntu-core/snappy/snap" - "github.com/ubuntu-core/snappy/testutil" ) func TestInterfaceManager(t *testing.T) { TestingT(t) } type interfaceManagerSuite struct { - state *state.State - mgr *ifacestate.InterfaceManager + state *state.State + mgr *ifacestate.InterfaceManager + restoreBackends func() } var _ = Suite(&interfaceManagerSuite{}) @@ -52,11 +53,15 @@ c.Assert(err, IsNil) s.state = state s.mgr = mgr + s.restoreBackends = ifacestate.MockSecurityBackendsForSnap( + func(snapInfo *snap.Info) []interfaces.SecurityBackend { return nil }, + ) } func (s *interfaceManagerSuite) TearDownTest(c *C) { s.mgr.Stop() dirs.SetRootDir("") + s.restoreBackends() } func (s *interfaceManagerSuite) TestSmoke(c *C) { @@ -185,23 +190,55 @@ c.Assert(err, IsNil) } -func (s *interfaceManagerSuite) TestDoSetupSnapSecuirty(c *C) { - parserCmd := testutil.MockCommand(c, "apparmor_parser", "") - defer parserCmd.Restore() +func (s *interfaceManagerSuite) mockSnap(c *C, yamlText string) *snap.Info { + s.state.Lock() + defer s.state.Unlock() - osSnap := &snap.Info{ - Type: snap.TypeOS, - SuggestedName: "ubuntu-core", - Slots: make(map[string]*snap.SlotInfo), - } - snap.AddImplicitSlots(osSnap) - err := s.mgr.Repository().AddSnap(osSnap) + // Parse the yaml + snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) c.Assert(err, IsNil) + snap.AddImplicitSlots(snapInfo) - dname := filepath.Join(dirs.SnapSnapsDir, "snap", "0", "meta") + // Create on-disk yaml file (it is read by snapstate) + dname := filepath.Join(dirs.SnapSnapsDir, snapInfo.Name(), + strconv.Itoa(snapInfo.Revision), "meta") fname := filepath.Join(dname, "snap.yaml") + err = os.MkdirAll(dname, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(fname, []byte(yamlText), 0644) + c.Assert(err, IsNil) + + // Put a side info into the state + snapstate.Set(s.state, snapInfo.Name(), &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{Revision: snapInfo.Revision}}, + }) - data := []byte(` + // Add it to the repository + s.mgr.Repository().AddSnap(snapInfo) + + return snapInfo +} + +func (s *interfaceManagerSuite) addSetupSnapSecurityChange(c *C, snapName string) *state.Change { + s.state.Lock() + defer s.state.Unlock() + + task := s.state.NewTask("setup-snap-security", "") + ss := snapstate.SnapSetup{Name: "snap"} + task.Set("snap-setup", ss) + taskset := state.NewTaskSet(task) + change := s.state.NewChange("test", "") + change.AddAll(taskset) + return change +} + +var osSnapYaml = ` +name: ubuntu-core +version: 1 +type: os +` + +var sampleSnapYaml = ` name: snap version: 1 apps: @@ -210,22 +247,48 @@ plugs: network: interface: network -`) - err = os.MkdirAll(dname, 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(fname, data, 0644) +` + +func (s *interfaceManagerSuite) TestDoSetupSnapSecuirty(c *C) { + s.mockSnap(c, osSnapYaml) + snapInfo := s.mockSnap(c, sampleSnapYaml) + + // Run the setup-snap-security task + change := s.addSetupSnapSecurityChange(c, snapInfo.Name()) + s.mgr.Ensure() + s.mgr.Wait() + s.mgr.Stop() + + s.state.Lock() + defer s.state.Unlock() + + c.Check(change.Status(), Equals, state.DoneStatus) + var conns map[string]interface{} + err := s.state.Get("conns", &conns) c.Assert(err, IsNil) + // Auto-connection data was saved into the state + c.Check(conns, DeepEquals, map[string]interface{}{ + "snap:network ubuntu-core:network": map[string]interface{}{ + "interface": "network", "auto": true, + }, + }) +} + +func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionState(c *C) { + s.mockSnap(c, osSnapYaml) + snapInfo := s.mockSnap(c, sampleSnapYaml) + // Put information about connections for another snap into the state s.state.Lock() - task := s.state.NewTask("setup-snap-security", "") - ss := snapstate.SnapSetup{Name: "snap"} - task.Set("snap-setup-task", task.ID()) - task.Set("snap-setup", ss) - taskset := state.NewTaskSet(task) - change := s.state.NewChange("test", "") - change.AddAll(taskset) + s.state.Set("conns", map[string]interface{}{ + "other-snap:network ubuntu-core:network": map[string]interface{}{ + "interface": "network", + }, + }) s.state.Unlock() + // Run the setup-snap-security task + change := s.addSetupSnapSecurityChange(c, snapInfo.Name()) s.mgr.Ensure() s.mgr.Wait() s.mgr.Stop() @@ -233,16 +296,17 @@ s.state.Lock() defer s.state.Unlock() - c.Check(task.Status(), Equals, state.DoneStatus) c.Check(change.Status(), Equals, state.DoneStatus) - var conns map[string]interface{} - err = task.State().Get("conns", &conns) + err := s.state.Get("conns", &conns) c.Assert(err, IsNil) + // Information from other snaps is not damaged c.Check(conns, DeepEquals, map[string]interface{}{ - "snap:network ubuntu-core:network": map[string]interface{}{ + "other-snap:network ubuntu-core:network": map[string]interface{}{ "interface": "network", - "auto": true, + }, + "snap:network ubuntu-core:network": map[string]interface{}{ + "interface": "network", "auto": true, }, }) } diff -Nru snapd-1.9.1.1/overlord/snapstate/export_test.go snapd-1.9.2/overlord/snapstate/export_test.go --- snapd-1.9.1.1/overlord/snapstate/export_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/snapstate/export_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -25,8 +25,6 @@ "github.com/ubuntu-core/snappy/overlord/state" ) -type SnapStateForTests snapState - type ManagerBackend managerBackend func SetSnapManagerBackend(s *SnapManager, b ManagerBackend) { diff -Nru snapd-1.9.1.1/overlord/snapstate/snapmgr.go snapd-1.9.2/overlord/snapstate/snapmgr.go --- snapd-1.9.1.1/overlord/snapstate/snapmgr.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/snapstate/snapmgr.go 2016-04-13 15:30:28.000000000 +0000 @@ -21,17 +21,12 @@ package snapstate import ( - "encoding/json" "fmt" - "path/filepath" - "strconv" "gopkg.in/tomb.v2" - "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/overlord/state" "github.com/ubuntu-core/snappy/snap" - "github.com/ubuntu-core/snappy/snappy" ) // SnapManager is responsible for the installation and removal of snaps. @@ -57,8 +52,8 @@ SnapPath string `json:"snap-path,omitempty"` } -// snapState holds the state for a snap installed in the system. -type snapState struct { +// SnapState holds the state for a snap installed in the system. +type SnapState struct { Sequence []*snap.SideInfo `json:"sequence"` // Last is current Candidate *snap.SideInfo `josn:"candidate,omitempty"` Active bool `json:"active,omitempty"` @@ -71,7 +66,7 @@ } func (ss *SnapSetup) MountDir() string { - return ss.placeInfo().MountDir() + return snap.MountDir(ss.Name, ss.Revision) } // Manager returns a new snap manager. @@ -147,7 +142,7 @@ st.Lock() t.Set("snap-setup", ss) snapst.Candidate = &snap.SideInfo{} - setSnapState(st, ss.Name, snapst) + Set(st, ss.Name, snapst) st.Unlock() return nil } @@ -155,13 +150,14 @@ func (m *SnapManager) undoPrepareSnap(t *state.Task, _ *tomb.Tomb) error { st := t.State() st.Lock() + defer st.Unlock() + ss, snapst, err := snapSetupAndState(t) - st.Unlock() if err != nil { return err } snapst.Candidate = nil - setSnapState(st, ss.Name, snapst) + Set(st, ss.Name, snapst) return nil } @@ -191,7 +187,7 @@ st.Lock() t.Set("snap-setup", ss) snapst.Candidate = &storeInfo.SideInfo - setSnapState(st, ss.Name, snapst) + Set(st, ss.Name, snapst) st.Unlock() return nil @@ -317,55 +313,19 @@ return &ss, nil } -func snapSetupAndState(t *state.Task) (*SnapSetup, *snapState, error) { +func snapSetupAndState(t *state.Task) (*SnapSetup, *SnapState, error) { ss, err := TaskSnapSetup(t) if err != nil { return nil, nil, err } - var snapst snapState - err = getSnapState(t.State(), ss.Name, &snapst) + var snapst SnapState + err = Get(t.State(), ss.Name, &snapst) if err != nil && err != state.ErrNoState { return nil, nil, err } return ss, &snapst, nil } -func getSnapState(s *state.State, name string, snapst *snapState) error { - var snaps map[string]*json.RawMessage - err := s.Get("snaps", &snaps) - if err != nil { - return err - } - raw, ok := snaps[name] - if !ok { - return state.ErrNoState - } - err = json.Unmarshal([]byte(*raw), &snapst) - if err != nil { - return fmt.Errorf("cannot unmarshal snap state: %v", err) - } - return nil -} - -func setSnapState(s *state.State, name string, snapst *snapState) { - var snaps map[string]*json.RawMessage - err := s.Get("snaps", &snaps) - if err == state.ErrNoState { - s.Set("snaps", map[string]*snapState{name: snapst}) - return - } - if err != nil { - panic("internal error: cannot unmarshal snaps state: " + err.Error()) - } - data, err := json.Marshal(snapst) - if err != nil { - panic("internal error: cannot marshal snap state: " + err.Error()) - } - raw := json.RawMessage(data) - snaps[name] = &raw - s.Set("snaps", snaps) -} - func (m *SnapManager) undoMountSnap(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() ss, err := TaskSnapSetup(t) @@ -440,7 +400,7 @@ } // Do at the end so we only preserve the new state if it worked. - setSnapState(st, ss.Name, snapst) + Set(st, ss.Name, snapst) return nil } @@ -458,7 +418,7 @@ oldDir := "" if len(snapst.Sequence) > 0 { latest := snapst.Sequence[len(snapst.Sequence)-1] - oldDir = snap.MinimalPlaceInfo(ss.Name, latest.Revision).MountDir() + oldDir = snap.MountDir(ss.Name, latest.Revision) } return m.backend.UndoLinkSnap(oldDir, newDir) } @@ -474,18 +434,3 @@ pb := &TaskProgressAdapter{task: t} return m.backend.GarbageCollect(ss.Name, ss.Flags, pb) } - -// SnapInfo returns the snap.Info for a snap in the system. -// -// Today this function is looking at data directly from the mounted snap, but soon it will -// be changed so it looks first at the state for the snap details (Revision, Developer, etc), -// and then complements it with information from the snap itself. -func SnapInfo(state *state.State, name string, revision int) (*snap.Info, error) { - fname := filepath.Join(dirs.SnapSnapsDir, name, strconv.Itoa(revision), "meta", "snap.yaml") - // XXX: This hacky and should not be needed. - sn, err := snappy.NewInstalledSnap(fname) - if err != nil { - return nil, err - } - return sn.Info(), nil -} diff -Nru snapd-1.9.1.1/overlord/snapstate/snapmgr_test.go snapd-1.9.2/overlord/snapstate/snapmgr_test.go --- snapd-1.9.1.1/overlord/snapstate/snapmgr_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/snapstate/snapmgr_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -196,7 +196,7 @@ }) // verify snaps in the system state - var snaps map[string]*snapstate.SnapStateForTests + var snaps map[string]*snapstate.SnapState err = s.state.Get("snaps", &snaps) c.Assert(err, IsNil) @@ -451,7 +451,7 @@ c.Assert(s.fakeBackend.ops[0].active, Equals, false) } -func (s *snapmgrTestSuite) TestSnapInfo(c *C) { +func (s *snapmgrTestSuite) TestInfo(c *C) { s.state.Lock() defer s.state.Unlock() @@ -459,25 +459,30 @@ defer dirs.SetRootDir("") // Write a snap.yaml with fake name - dname := filepath.Join(dirs.SnapSnapsDir, "name", "11", "meta") + dname := filepath.Join(dirs.SnapSnapsDir, "name1", "11", "meta") err := os.MkdirAll(dname, 0775) c.Assert(err, IsNil) fname := filepath.Join(dname, "snap.yaml") err = ioutil.WriteFile(fname, []byte(` -name: ignored +name: name0 version: 1.2 description: | Lots of text`), 0644) c.Assert(err, IsNil) - snapInfo, err := snapstate.SnapInfo(s.state, "name", 11) + snapstate.Set(s.state, "name1", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {OfficialName: "name1", Revision: 11, EditedSummary: "s11"}, + {OfficialName: "name1", Revision: 12, EditedSummary: "s12"}, + }, + }) + + info, err := snapstate.Info(s.state, "name1", 11) c.Assert(err, IsNil) - // TODO: This test is not faking the manifest so SideInfo is not present. - // The test and the actual implementation need to be improved so that this - // is not so hacky and that the manifest can go away. - c.Check(snapInfo.Name(), Equals, "ignored") - // Check that other values are read from YAML - c.Check(snapInfo.Description(), Equals, "Lots of text") - c.Check(snapInfo.Version, Equals, "1.2") + c.Check(info.Name(), Equals, "name1") + c.Check(info.Revision, Equals, 11) + c.Check(info.Summary(), Equals, "s11") + c.Check(info.Version, Equals, "1.2") + c.Check(info.Description(), Equals, "Lots of text") } diff -Nru snapd-1.9.1.1/overlord/snapstate/snapstate.go snapd-1.9.2/overlord/snapstate/snapstate.go --- snapd-1.9.1.1/overlord/snapstate/snapstate.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/snapstate/snapstate.go 2016-04-13 15:30:28.000000000 +0000 @@ -21,12 +21,17 @@ package snapstate import ( + "encoding/json" "fmt" + "io/ioutil" + "os" + "path/filepath" "strings" "github.com/ubuntu-core/snappy/i18n" "github.com/ubuntu-core/snappy/osutil" "github.com/ubuntu-core/snappy/overlord/state" + "github.com/ubuntu-core/snappy/snap" "github.com/ubuntu-core/snappy/snappy" ) @@ -194,3 +199,89 @@ return state.NewTaskSet(t), nil } + +// Retrieval functions + +func retrieveInfo(name string, si *snap.SideInfo) (*snap.Info, error) { + // XXX: move some of this in snap as helper? + snapYamlFn := filepath.Join(snap.MountDir(name, si.Revision), "meta", "snap.yaml") + meta, err := ioutil.ReadFile(snapYamlFn) + if os.IsNotExist(err) { + return nil, fmt.Errorf("cannot find mounted snap %q at revision %d", name, si.Revision) + } + if err != nil { + return nil, err + } + + info, err := snap.InfoFromSnapYaml(meta) + if err != nil { + return nil, err + } + + info.SideInfo = *si + + return info, nil +} + +// Info returns the information about the snap with given name and revision. +// Works also for a mounted candidate snap in the process of being installed. +func Info(s *state.State, name string, revision int) (*snap.Info, error) { + var snapst SnapState + err := Get(s, name, &snapst) + if err == state.ErrNoState { + return nil, fmt.Errorf("cannot find snap %q", name) + } + if err != nil { + return nil, err + } + + for i := len(snapst.Sequence) - 1; i >= 0; i-- { + if si := snapst.Sequence[i]; si.Revision == revision { + return retrieveInfo(name, si) + } + } + + if snapst.Candidate != nil && snapst.Candidate.Revision == revision { + return retrieveInfo(name, snapst.Candidate) + } + + return nil, fmt.Errorf("cannot find snap %q at revision %d", name, revision) +} + +// Get retrieves the SnapState of the given snap. +func Get(s *state.State, name string, snapst *SnapState) error { + var snaps map[string]*json.RawMessage + err := s.Get("snaps", &snaps) + if err != nil { + return err + } + raw, ok := snaps[name] + if !ok { + return state.ErrNoState + } + err = json.Unmarshal([]byte(*raw), &snapst) + if err != nil { + return fmt.Errorf("cannot unmarshal snap state: %v", err) + } + return nil +} + +// Set sets the SnapState of the given snap, overwriting any earlier state. +func Set(s *state.State, name string, snapst *SnapState) { + var snaps map[string]*json.RawMessage + err := s.Get("snaps", &snaps) + if err == state.ErrNoState { + s.Set("snaps", map[string]*SnapState{name: snapst}) + return + } + if err != nil { + panic("internal error: cannot unmarshal snaps state: " + err.Error()) + } + data, err := json.Marshal(snapst) + if err != nil { + panic("internal error: cannot marshal snap state: " + err.Error()) + } + raw := json.RawMessage(data) + snaps[name] = &raw + s.Set("snaps", snaps) +} diff -Nru snapd-1.9.1.1/overlord/state/state.go snapd-1.9.2/overlord/state/state.go --- snapd-1.9.1.1/overlord/state/state.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/state/state.go 2016-04-13 15:30:28.000000000 +0000 @@ -277,6 +277,12 @@ return res } +// Change returns the change for the given ID. +func (s *State) Change(id string) *Change { + s.reading() + return s.changes[id] +} + // NewTask creates a new task. // It usually will be registered with a Change using AddTask or // through a TaskSet. diff -Nru snapd-1.9.1.1/overlord/state/state_test.go snapd-1.9.2/overlord/state/state_test.go --- snapd-1.9.1.1/overlord/state/state_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/state/state_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -275,7 +275,10 @@ for _, chg := range chgs { c.Check(chg, Equals, expected[chg.ID()]) + c.Check(st.Change(chg.ID()), Equals, chg) } + + c.Check(st.Change("no-such-id"), IsNil) } func (ss *stateSuite) TestNewChangeAndCheckpoint(c *C) { @@ -499,7 +502,9 @@ func() { st.Cached("foo") }, func() { st.Cache("foo", 1) }, func() { st.Changes() }, + func() { st.Change("foo") }, func() { st.Tasks() }, + func() { st.Task("foo") }, func() { st.MarshalJSON() }, } diff -Nru snapd-1.9.1.1/overlord/state/task.go snapd-1.9.2/overlord/state/task.go --- snapd-1.9.1.1/overlord/state/task.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/state/task.go 2016-04-13 15:30:28.000000000 +0000 @@ -172,7 +172,12 @@ // SetProgress sets the task progress to cur out of total steps. func (t *Task) SetProgress(cur, total int) { - t.state.writing() + // Only mark state for checkpointing if progress is final. + if total > 0 && cur == total { + t.state.writing() + } else { + t.state.reading() + } if total <= 0 || cur > total { // Doing math wrong is easy. Be conservative. t.progress = nil diff -Nru snapd-1.9.1.1/overlord/state/task_test.go snapd-1.9.2/overlord/state/task_test.go --- snapd-1.9.1.1/overlord/state/task_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/overlord/state/task_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -116,6 +116,11 @@ c.Check(cur, Equals, 0) c.Check(tot, Equals, 1) c.Check(jsonStr(t), Not(testutil.Contains), "progress") + + t.SetProgress(42, 42) + cur, tot = t.Progress() + c.Check(cur, Equals, 42) + c.Check(tot, Equals, 42) } func (ts *taskSuite) TestProgressDefaults(c *C) { @@ -244,6 +249,7 @@ func() { t1.Logf("") }, func() { t1.Errorf("") }, func() { t1.UnmarshalJSON(nil) }, + func() { t1.SetProgress(1, 1) }, } reads := []func(){ @@ -254,6 +260,8 @@ func() { t1.Progress() }, func() { t1.Log() }, func() { t1.MarshalJSON() }, + func() { t1.Progress() }, + func() { t1.SetProgress(0, 1) }, } for i, f := range reads { diff -Nru snapd-1.9.1.1/po/snappy.pot snapd-1.9.2/po/snappy.pot --- snapd-1.9.1.1/po/snappy.pot 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/po/snappy.pot 2016-04-13 15:30:28.000000000 +0000 @@ -19,9 +19,6 @@ msgid "(deprecated) please use \"list\"" msgstr "" -msgid "2fa code: " -msgstr "" - msgid "A concise summary of key attributes of the snappy system, such as the release and channel.\n" "\n" "The verbose output includes the specific version information for the factory image, the running image and the image that will be run on reboot, together with a list of the available channels for this image.\n" @@ -385,6 +382,9 @@ msgid "This command logs the given username into the store" msgstr "" +msgid "Two-factor code: " +msgstr "" + #, c-format msgid "Update %q snap" msgstr "" diff -Nru snapd-1.9.1.1/snap/info.go snapd-1.9.2/snap/info.go --- snapd-1.9.1.1/snap/info.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/snap/info.go 2016-04-13 15:30:28.000000000 +0000 @@ -34,7 +34,7 @@ // Name returns the name of the snap. Name() string - //MountDir returns the base directory of the snap. + // MountDir returns the base directory of the snap. MountDir() string // MountFile returns the path where the snap file that is mounted is installed. @@ -52,6 +52,11 @@ return &Info{SideInfo: SideInfo{OfficialName: name, Revision: revision}} } +// MountDir returns the base directory where it gets mounted of the snap with the given name and revision. +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. type SideInfo struct { @@ -123,7 +128,7 @@ // MountDir returns the base directory of the snap where it gets mounted. func (s *Info) MountDir() string { - return filepath.Join(dirs.SnapSnapsDir, s.Name(), s.strRevno()) + return MountDir(s.Name(), s.Revision) } // MountFile returns the path where the snap file that is mounted is installed. diff -Nru snapd-1.9.1.1/store/errors.go snapd-1.9.2/store/errors.go --- snapd-1.9.1.1/store/errors.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/store/errors.go 2016-04-13 15:30:28.000000000 +0000 @@ -25,6 +25,11 @@ "net/url" ) +const ( + // TwoFactorErrKind is the error kind to be returned if 2FA is needed + TwoFactorErrKind = "two-factor-required" +) + var ( // ErrSnapNotFound is returned when a snap can not be found ErrSnapNotFound = errors.New("snap not found") @@ -32,9 +37,8 @@ // ErrAssertionNotFound is returned when an assertion can not be found ErrAssertionNotFound = errors.New("assertion not found") - // ErrAuthenticationNeeds2fa is returned if the authentication - // needs 2factor - ErrAuthenticationNeeds2fa = errors.New("authentication needs second factor") + // ErrAuthenticationNeeds2fa is returned if the authentication needs 2factor + ErrAuthenticationNeeds2fa = errors.New("two factor authentication required") // ErrInvalidCredentials is returned on login error ErrInvalidCredentials = errors.New("invalid credentials") diff -Nru snapd-1.9.1.1/store/snap_remote_repo.go snapd-1.9.2/store/snap_remote_repo.go --- snapd-1.9.1.1/store/snap_remote_repo.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/store/snap_remote_repo.go 2016-04-13 15:30:28.000000000 +0000 @@ -32,6 +32,7 @@ "path" "reflect" "strings" + "sync" "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/asserts" @@ -85,6 +86,9 @@ assertionsURI *url.URL // reused http client client *http.Client + + mu sync.Mutex + suggestedCurrency string } func getStructFields(s interface{}) []string { @@ -241,6 +245,17 @@ s.applyUbuntuStoreHeaders(req, accept) } +// read all the available metadata from the store response and cache +func (s *SnapUbuntuStoreRepository) checkStoreResponse(resp *http.Response) { + suggestedCurrency := resp.Header.Get("X-Suggested-Currency") + + if suggestedCurrency != "" { + s.mu.Lock() + s.suggestedCurrency = suggestedCurrency + s.mu.Unlock() + } +} + // Snap returns the snap.Info for the store hosted snap with the given name or an error. func (s *SnapUbuntuStoreRepository) Snap(name, channel string) (*snap.Info, error) { @@ -278,6 +293,8 @@ return nil, err } + s.checkStoreResponse(resp) + return infoFromRemote(detailsData), nil } @@ -323,6 +340,8 @@ snaps[i] = infoFromRemote(pkg) } + s.checkStoreResponse(resp) + return snaps, nil } @@ -359,6 +378,8 @@ res[i] = infoFromRemote(rsnap) } + s.checkStoreResponse(resp) + return res, nil } @@ -474,3 +495,14 @@ dec := asserts.NewDecoder(resp.Body) return dec.Decode() } + +// SuggestedCurrency retrieves the cached value for the store's suggested currency +func (s *SnapUbuntuStoreRepository) SuggestedCurrency() string { + s.mu.Lock() + defer s.mu.Unlock() + + if s.suggestedCurrency == "" { + return "USD" + } + return s.suggestedCurrency +} diff -Nru snapd-1.9.1.1/store/snap_remote_repo_test.go snapd-1.9.2/store/snap_remote_repo_test.go --- snapd-1.9.1.1/store/snap_remote_repo_test.go 2016-04-12 12:40:10.000000000 +0000 +++ snapd-1.9.2/store/snap_remote_repo_test.go 2016-04-13 15:30:28.000000000 +0000 @@ -573,3 +573,43 @@ _, err = repo.Assertion(asserts.SnapDeclarationType, "16", "snapidfoo") c.Check(err, Equals, ErrAssertionNotFound) } + +func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { + suggestedCurrency := "GBP" + + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Suggested-Currency", suggestedCurrency) + w.WriteHeader(http.StatusOK) + + io.WriteString(w, MockDetailsJSON) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + var err error + detailsURI, err := url.Parse(mockServer.URL + "/details/") + c.Assert(err, IsNil) + cfg := SnapUbuntuStoreConfig{ + DetailsURI: detailsURI, + } + repo := NewUbuntuStoreSnapRepository(&cfg, "") + c.Assert(repo, NotNil) + + // the store doesn't know the currency until after the first search, so fall back to dollars + c.Check(repo.SuggestedCurrency(), Equals, "USD") + + // we should soon have a suggested currency + result, err := repo.Snap(funkyAppName+"."+funkyAppDeveloper, "edge") + c.Assert(err, IsNil) + c.Assert(result, NotNil) + c.Check(repo.SuggestedCurrency(), Equals, "GBP") + + suggestedCurrency = "EUR" + + // checking the currency updates + result, err = repo.Snap(funkyAppName+"."+funkyAppDeveloper, "edge") + c.Assert(err, IsNil) + c.Assert(result, NotNil) + c.Check(repo.SuggestedCurrency(), Equals, "EUR") +}