diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2019-01-18 13:09:44 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2019-01-18 13:09:44 +0100 |
| commit | 6acb7a50fc87be9d494bbcf7fba8a836cc6e3c30 (patch) | |
| tree | 569b7088ec019b6fe0742ca166815aac67b711a8 | |
| parent | 39a1f82217efc1724421ec34d609aa50f357662d (diff) | |
many: add remodel API, `snap remodel` CLI and spead testremodel-v0-spread
| -rw-r--r-- | client/model.go | 45 | ||||
| -rw-r--r-- | client/model_test.go | 55 | ||||
| -rw-r--r-- | cmd/snap/cmd_debug_get_model.go | 54 | ||||
| -rw-r--r-- | cmd/snap/cmd_debug_get_model_test.go | 55 | ||||
| -rw-r--r-- | cmd/snap/cmd_remodel.go | 75 | ||||
| -rw-r--r-- | daemon/api.go | 54 | ||||
| -rw-r--r-- | daemon/api_test.go | 48 | ||||
| -rw-r--r-- | tests/lib/assertions/developer1-pc-revno2.model | 24 | ||||
| -rw-r--r-- | tests/main/remodel/task.yaml | 71 |
9 files changed, 481 insertions, 0 deletions
diff --git a/client/model.go b/client/model.go new file mode 100644 index 0000000000..fae6c41d8b --- /dev/null +++ b/client/model.go @@ -0,0 +1,45 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" +) + +type remodelData struct { + NewModel string `json:"new-model"` +} + +// Remodel tries to remodel the system with the given assertion data +func (client *Client) Remodel(b []byte) (changeID string, err error) { + data, err := json.Marshal(&remodelData{ + NewModel: string(b), + }) + if err != nil { + return "", fmt.Errorf("cannot marshal remodel data: %v", err) + } + headers := map[string]string{ + "Content-Type": "application/json", + } + + return client.doAsync("POST", "/v2/model", nil, headers, bytes.NewReader(data)) +} diff --git a/client/model_test.go b/client/model_test.go new file mode 100644 index 0000000000..847ef3e137 --- /dev/null +++ b/client/model_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package client_test + +import ( + "encoding/json" + "io/ioutil" + + . "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientRemodelEndpoint(c *C) { + cs.cli.Remodel([]byte(`{"new-model": "some-model"}`)) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/model") +} + +func (cs *clientSuite) TestClientRemodel(c *C) { + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": {}, + "change": "d728" + }` + remodelJsonData := []byte(`{"new-model": "some-model"}`) + id, err := cs.cli.Remodel(remodelJsonData) + c.Assert(err, IsNil) + c.Check(id, Equals, "d728") + c.Assert(cs.req.Header.Get("Content-Type"), Equals, "application/json") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + jsonBody := make(map[string]string) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, IsNil) + c.Check(jsonBody, HasLen, 1) + c.Check(jsonBody["new-model"], Equals, string(remodelJsonData)) +} diff --git a/cmd/snap/cmd_debug_get_model.go b/cmd/snap/cmd_debug_get_model.go new file mode 100644 index 0000000000..a0a1803353 --- /dev/null +++ b/cmd/snap/cmd_debug_get_model.go @@ -0,0 +1,54 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +import ( + "fmt" + + "github.com/jessevdk/go-flags" +) + +type cmdGetModel struct { + clientMixin +} + +func init() { + cmd := addDebugCommand("get-model", + "(internal) obtain the active model assertion", + "(internal) obtain the active model assertion", + func() flags.Commander { + return &cmdGetModel{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdGetModel) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + var resp struct { + Model string `json:"model"` + } + if err := x.client.Debug("get-model", nil, &resp); err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", resp.Model) + return nil +} diff --git a/cmd/snap/cmd_debug_get_model_test.go b/cmd/snap/cmd_debug_get_model_test.go new file mode 100644 index 0000000000..da71bc4a5b --- /dev/null +++ b/cmd/snap/cmd_debug_get_model_test.go @@ -0,0 +1,55 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestGetModel(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/debug") + c.Check(r.URL.RawQuery, check.Equals, "") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.DeepEquals, []byte(`{"action":"get-model"}`)) + fmt.Fprintln(w, `{"type": "sync", "result": {"model": "some-model-json"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "get-model"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "some-model-json\n") + c.Check(s.Stderr(), check.Equals, "") +} diff --git a/cmd/snap/cmd_remodel.go b/cmd/snap/cmd_remodel.go new file mode 100644 index 0000000000..8567de22bc --- /dev/null +++ b/cmd/snap/cmd_remodel.go @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdRemodel struct { + waitMixin + RemodelOptions struct { + NewModelFile flags.Filename + } `positional-args:"true" required:"true"` +} + +func init() { + cmd := addCommand("remodel", + "Remodel the given device", + "Remodel the given device", + func() flags.Commander { + return &cmdRemodel{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G("<new model file>"), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("New model file"), + }}) + cmd.hidden = true +} + +func (x *cmdRemodel) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + newModelFile := x.RemodelOptions.NewModelFile + modelData, err := ioutil.ReadFile(string(newModelFile)) + if err != nil { + return err + } + changeID, err := x.client.Remodel(modelData) + if err != nil { + return fmt.Errorf("cannot remodel: %v", err) + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + fmt.Fprintf(Stdout, i18n.G("New model %s set"), newModelFile) + return nil +} diff --git a/daemon/api.go b/daemon/api.go index b871f0ee33..a6469d6ca8 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -101,6 +101,7 @@ var api = []*Command{ warningsCmd, debugCmd, snapshotCmd, + modelCmd, } var ( @@ -263,6 +264,12 @@ var ( POST: ackWarnings, } + modelCmd = &Command{ + Path: "/v2/model", + POST: postModel, + // FIXME: provide getModel here instead of via debug? + } + buildID = "unknown" ) @@ -944,6 +951,8 @@ var ( snapstateRevert = snapstate.Revert snapstateRevertToRevision = snapstate.RevertToRevision + devicestateRemodel = devicestate.Remodel + snapshotList = snapshotstate.List snapshotCheck = snapshotstate.Check snapshotForget = snapshotstate.Forget @@ -2539,6 +2548,14 @@ func postDebug(c *Command, r *http.Request, user *auth.UserState) Response { return SyncResponse(map[string]interface{}{ "base-declaration": string(asserts.Encode(bd)), }, nil) + case "get-model": + model, err := devicestate.Model(st) + if err != nil { + return InternalError("cannot get model: %v", err) + } + return SyncResponse(map[string]interface{}{ + "model": string(asserts.Encode(model)), + }, nil) case "can-manage-refreshes": return SyncResponse(devicestate.CanManageRefreshes(st), nil) case "connectivity": @@ -2914,6 +2931,43 @@ var ( statePendingWarnings = (*state.State).PendingWarnings ) +type postModelData struct { + NewModel string `json:"new-model"` +} + +func postModel(c *Command, r *http.Request, _ *auth.UserState) Response { + defer r.Body.Close() + var data postModelData + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&data); err != nil { + return BadRequest("cannot decode request body into remodel operation: %v", err) + } + rawNewModel, err := asserts.Decode([]byte(data.NewModel)) + if err != nil { + return BadRequest("cannot decode request new model assertion: %v", err) + } + newModel, ok := rawNewModel.(*asserts.Model) + if !ok { + return BadRequest("new model is not a model assertion: %v", newModel.Type()) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + tss, err := devicestateRemodel(st, newModel) + if err != nil { + return BadRequest("cannot remodel device: %v", err) + } + msg := fmt.Sprintf(i18n.G("Remodel device to %v (%v)"), newModel.Model(), newModel.Revision()) + chg := newChange(st, "remodel", msg, tss, nil) + + ensureStateSoon(st) + + return AsyncResponse(nil, &Meta{Change: chg.ID()}) + +} + func ackWarnings(c *Command, r *http.Request, _ *auth.UserState) Response { defer r.Body.Close() var op struct { diff --git a/daemon/api_test.go b/daemon/api_test.go index dac4607526..26bff5af0b 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -296,6 +296,8 @@ func (s *apiBaseSuite) SetUpTest(c *check.C) { snapstateTryPath = nil snapstateUpdate = nil snapstateUpdateMany = nil + + devicestateRemodel = nil } func (s *apiBaseSuite) TearDownTest(c *check.C) { @@ -7551,3 +7553,49 @@ func (s *apiSuite) TestErrToResponse(c *check.C) { c.Check(rsp, check.DeepEquals, t.expectedRsp, com) } } + +func (s *apiSuite) TestPostRemodelUnhappy(c *check.C) { + data, err := json.Marshal(postModelData{NewModel: "invalid model"}) + c.Check(err, check.IsNil) + + req, err := http.NewRequest("POST", "/v2/model", bytes.NewBuffer(data)) + c.Assert(err, check.IsNil) + rsp := postModel(appsCmd, req, nil).(*resp) + c.Check(rsp.Type, check.Equals, ResponseTypeError) + c.Assert(rsp.Status, check.Equals, 400) + c.Check(rsp.Result.(*errorResult).Message, check.Matches, "cannot decode request new model assertion: .*") +} + +func (s *apiSuite) TestPostRemodel(c *check.C) { + s.daemonWithOverlordMock(c) + + var devicestateRemodelGotModel *asserts.Model + devicestateRemodel = func(st *state.State, nm *asserts.Model) ([]*state.TaskSet, error) { + devicestateRemodelGotModel = nm + return nil, nil + } + + // create a valid model assertion + mockModel, err := s.storeSigning.RootSigning.Sign(asserts.ModelType, map[string]interface{}{ + "series": "16", + "authority-id": "my-brand", + "brand-id": "my-brand", + "model": "my-model", + "architecture": "amd64", + "gadget": "pc", + "kernel": "pc-kernel", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + mockModelEncoded := string(asserts.Encode(mockModel)) + data, err := json.Marshal(postModelData{NewModel: mockModelEncoded}) + c.Check(err, check.IsNil) + + // set it and validate that this is what we was passed to + // devicestateRemodel + req, err := http.NewRequest("POST", "/v2/model", bytes.NewBuffer(data)) + c.Assert(err, check.IsNil) + rsp := postModel(appsCmd, req, nil).(*resp) + c.Assert(rsp.Status, check.Equals, 202) + c.Check(mockModel, check.DeepEquals, devicestateRemodelGotModel) +} diff --git a/tests/lib/assertions/developer1-pc-revno2.model b/tests/lib/assertions/developer1-pc-revno2.model new file mode 100644 index 0000000000..d950805cff --- /dev/null +++ b/tests/lib/assertions/developer1-pc-revno2.model @@ -0,0 +1,24 @@ +type: model +authority-id: developer1 +revision: 2 +series: 16 +brand-id: developer1 +model: my-model +architecture: amd64 +gadget: pc +kernel: pc-kernel +required-snaps: + - test-snapd-tools +timestamp: 2019-01-18T08:00:00+00:00 +sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu + +AcLBUgQAAQoABgUCXEGouAAAycMQACN+CgOcrx4y8F9LgnkCI4Z7x5enN40/ubd0JCgyx+pAQRYc +5yqDXDMQ6No8zz/NX2U8BPQExUvLhvI+iTbnMhxg8FJsaionupKEap57CaL/OaTDn9S1WdKMhdQI +dUP6ZvM8z5FYRmOw9vzcMGy1FQ8C+VyX0Q93gj0QdgaiIZt0v9Dc3cqB+fJR3W2/WP2WjqTU4Yfh +f+gJYlFIGQGhsfzWAcvBwQi9d4+WO5hEWvvsIjKI3/4EDy5C4PBl6zRJLzCcflDG25If0Ax/Ptli +pjjo7IexscnybJEar/qY9lSBECtZpeQ70D1sDplBsuEBxV4mP04N2GT6+ZDN4lIUDOPg/3esWzO4 +YrbPcW/4fBDXxX7PomL6ttwgwqGS0kIw8pMaJWfvO1VASEFWkdEB8I6haVwQ/Ya1Y5GNz7WVAz2M +9ULXRiT201Een9XSF0iZbxvu2sBgfdLqEIXEpUCd4MmUz9zE3uyT/x+mdBhoj51q8RZp4v+Ve4ws +SY+febHLAA5RLH6VvkgdA7JFgJsIZsuI+3ICYmU1CgIWq8iRwiJ7ggaBiprm+66QxxI7A06l3H3e +VYZkbILnTU2XSBb8mFUjIRDQ464h1QSi1MfdFzehUSKk1YlL4elnDXhvJAGvGDRBf8F4jF+IPTzB +5abmlPCoPbhdgn+ZSLdnoI7EZ0PR diff --git a/tests/main/remodel/task.yaml b/tests/main/remodel/task.yaml new file mode 100644 index 0000000000..885cfb4c8b --- /dev/null +++ b/tests/main/remodel/task.yaml @@ -0,0 +1,71 @@ +summary: | + Test remodel + +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + mv /var/lib/snapd/seed/assertions/model model.bak + cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions + # kick first boot again + systemctl start snapd.service snapd.socket + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + + rm -f /var/lib/snapd/seed/assertions/developer1.account + rm -f /var/lib/snapd/seed/assertions/developer1.account-key + rm -f /var/lib/snapd/seed/assertions/developer1-pc.model + rm -f /var/lib/snapd/seed/assertions/testrootorg-store.account-key + rm -f /var/lib/snapd/seed/assertions/test-snapd-with-configure_*.assert + cp model.bak /var/lib/snapd/seed/assertions/model + rm -f ./*.bak + # kick first boot again + systemctl start snapd.service snapd.socket + # wait for first boot to be done + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # sanity check + ! snap list test-snapd-tools + + echo "Wait for first boot to be done" + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + echo "We have the right model assertion" + snap debug get-model|MATCH "model: my-model" + + echo "Now we remodel" + snap remodel "$TESTSLIB"/assertions/developer1-pc-revno2.model + + echo "and we got the new required snap" + snap list test-snapd-tools + + # FIXME: check that this is the active model assertion + echo "and we got the new model assertion" + snap debug get-model|MATCH "revision: 2" |
