summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2019-01-18 13:09:44 +0100
committerMichael Vogt <mvo@ubuntu.com>2019-01-18 13:09:44 +0100
commit6acb7a50fc87be9d494bbcf7fba8a836cc6e3c30 (patch)
tree569b7088ec019b6fe0742ca166815aac67b711a8
parent39a1f82217efc1724421ec34d609aa50f357662d (diff)
many: add remodel API, `snap remodel` CLI and spead testremodel-v0-spread
-rw-r--r--client/model.go45
-rw-r--r--client/model_test.go55
-rw-r--r--cmd/snap/cmd_debug_get_model.go54
-rw-r--r--cmd/snap/cmd_debug_get_model_test.go55
-rw-r--r--cmd/snap/cmd_remodel.go75
-rw-r--r--daemon/api.go54
-rw-r--r--daemon/api_test.go48
-rw-r--r--tests/lib/assertions/developer1-pc-revno2.model24
-rw-r--r--tests/main/remodel/task.yaml71
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"