summaryrefslogtreecommitdiff
diff options
authorPaweł Stołowski <stolowski@gmail.com>2020-10-29 09:59:48 +0100
committerPaweł Stołowski <stolowski@gmail.com>2020-10-29 09:59:48 +0100
commit8a082408fddb2e791aca737347b13d72ba6a1e8c (patch)
tree7db3083e5ae4c685cc62d80147995c6da531281a
parenteee9569170b4a5d5075a19a64220bc77cdb8cb41 (diff)
parentafab8fb88b821a54f73c3a349720fe21b9c9e488 (diff)
Merge branch 'master' into preseeding-interface-hookspreseeding-interface-hooks
-rw-r--r--boot/seal.go14
-rw-r--r--client/client.go58
-rw-r--r--client/client_test.go58
-rw-r--r--client/console_conf.go44
-rw-r--r--client/console_conf_test.go63
-rw-r--r--cmd/snap/cmd_routine_console_conf.go144
-rw-r--r--cmd/snap/cmd_routine_console_conf_test.go355
-rw-r--r--cmd/snap/export_test.go16
-rw-r--r--daemon/api.go1
-rw-r--r--daemon/api_console_conf.go96
-rw-r--r--daemon/api_console_conf_test.go116
-rw-r--r--daemon/api_systems_test.go2
-rw-r--r--daemon/daemon.go94
-rw-r--r--daemon/daemon_test.go101
-rw-r--r--daemon/response.go25
-rw-r--r--dirs/dirs.go3
-rw-r--r--interfaces/apparmor/template.go3
-rw-r--r--interfaces/builtin/x11.go9
-rw-r--r--osutil/disks/mockdisk.go18
-rw-r--r--osutil/disks/mockdisk_test.go25
-rw-r--r--overlord/snapstate/autorefresh.go25
-rw-r--r--overlord/snapstate/autorefresh_test.go92
-rw-r--r--overlord/snapstate/export_test.go4
-rw-r--r--overlord/snapstate/snapmgr.go23
-rw-r--r--overlord/snapstate/snapstate_test.go59
-rw-r--r--secboot/export_test.go24
-rw-r--r--secboot/secboot.go8
-rw-r--r--secboot/secboot_tpm.go72
-rw-r--r--secboot/secboot_tpm_test.go71
-rw-r--r--tests/nested/core20/gadget-reseal/task.yaml3
-rw-r--r--tests/nested/core20/kernel-reseal/task.yaml14
-rw-r--r--vendor/vendor.json41
32 files changed, 1552 insertions, 129 deletions
diff --git a/boot/seal.go b/boot/seal.go
index 363bdd762d..80bc2115f6 100644
--- a/boot/seal.go
+++ b/boot/seal.go
@@ -107,10 +107,10 @@ func sealKeyToModeenv(key secboot.EncryptionKey, model *asserts.Model, modeenv *
}
}
sealKeyParams := &secboot.SealKeyParams{
- ModelParams: modelParams,
- KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"),
- TPMPolicyUpdateDataFile: filepath.Join(InstallHostFDEDataDir, "policy-update-data"),
- TPMLockoutAuthFile: filepath.Join(InstallHostFDEDataDir, "tpm-lockout-auth"),
+ ModelParams: modelParams,
+ KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"),
+ TPMPolicyAuthKeyFile: filepath.Join(InstallHostFDEDataDir, "tpm-policy-auth-key"),
+ TPMLockoutAuthFile: filepath.Join(InstallHostFDEDataDir, "tpm-lockout-auth"),
}
// finally, seal the key
if err := secbootSealKey(key, sealKeyParams); err != nil {
@@ -217,9 +217,9 @@ func resealKeyToModeenv(rootdir string, model *asserts.Model, modeenv *Modeenv,
return fmt.Errorf("cannot prepare for key resealing: %v", err)
}
resealKeyParams := &secboot.ResealKeyParams{
- ModelParams: modelParams,
- KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"),
- TPMPolicyUpdateDataFile: filepath.Join(dirs.SnapFDEDirUnder(rootdir), "policy-update-data"),
+ ModelParams: modelParams,
+ KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"),
+ TPMPolicyAuthKeyFile: filepath.Join(dirs.SnapFDEDirUnder(rootdir), "tpm-policy-auth-key"),
}
if err := secbootResealKey(resealKeyParams); err != nil {
return fmt.Errorf("cannot reseal the encryption key: %v", err)
diff --git a/client/client.go b/client/client.go
index c859be76dd..d4336eeaa2 100644
--- a/client/client.go
+++ b/client/client.go
@@ -317,7 +317,9 @@ func (client *Client) Hijack(f func(*http.Request) (*http.Response, error)) {
type doOptions struct {
// Timeout is the overall request timeout
Timeout time.Duration
- // Retry interval
+ // Retry interval.
+ // Note for a request with a Timeout but without a retry, Retry should just
+ // be set to something larger than the Timeout.
Retry time.Duration
}
@@ -344,6 +346,10 @@ var doNoTimeoutAndRetry = &doOptions{
func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (statusCode int, err error) {
opts = ensureDoOpts(opts)
+ if err := client.checkMaintenanceJSON(); err != nil {
+ return 500, err
+ }
+
var rsp *http.Response
var ctx context.Context = context.Background()
if opts.Timeout <= 0 {
@@ -413,7 +419,57 @@ func (client *Client) doSync(method, path string, query url.Values, headers map[
return client.doSyncWithOpts(method, path, query, headers, body, v, nil)
}
+// checkMaintenanceJSON checks if there is a maintenance.json file written by
+// snapd the daemon that positively identifies snapd as being unavailable due to
+// maintenance, either for snapd restarting itself to update, or rebooting the
+// system to update the kernel or base snap, etc. If there is ongoing
+// maintenance, then the maintenance object on the client is set appropriately.
+// note that currently checkMaintenanceJSON does not return non-nil errors, so
+// if the file is missing or corrupt or empty, nothing will happen
+func (client *Client) checkMaintenanceJSON() error {
+ f, err := os.Open(dirs.SnapdMaintenanceFile)
+ // just continue if we can't read the maintenance file
+ if err != nil {
+ return nil
+ }
+
+ // we have a maintenance file, try to read it
+ maintenance := &Error{}
+
+ if err := json.NewDecoder(f).Decode(&maintenance); err != nil {
+ // if the json is malformed, just ignore it for now, we only use it for
+ // positive identification of snapd down for maintenance
+ return nil
+ }
+
+ if maintenance != nil {
+ switch maintenance.Kind {
+ case ErrorKindDaemonRestart:
+ client.maintenance = maintenance
+ case ErrorKindSystemRestart:
+ client.maintenance = maintenance
+ }
+ // don't set maintenance for other kinds, as we don't know what it
+ // is yet
+
+ // this also means an empty json object in maintenance.json doesn't get
+ // treated as a real maintenance downtime for example
+ }
+
+ return nil
+}
+
func (client *Client) doSyncWithOpts(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (*ResultInfo, error) {
+ // first check maintenance.json to see if snapd is down for a restart, and
+ // set cli.maintenance as appropriate, then perform the request
+ // TODO: it would be a nice thing to skip the request if we know that snapd
+ // won't respond and return a specific error, but that's a big behavior
+ // change we probably shouldn't make right now, not to mention it probably
+ // requires adjustments in other areas too
+ if err := client.checkMaintenanceJSON(); err != nil {
+ return nil, err
+ }
+
var rsp response
statusCode, err := client.do(method, path, query, headers, body, &rsp, opts)
if err != nil {
diff --git a/client/client_test.go b/client/client_test.go
index 8b312cf169..20e2cfca4c 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -20,6 +20,7 @@
package client_test
import (
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -145,6 +146,63 @@ func (cs *clientSuite) TestClientWorks(c *C) {
c.Check(cs.req.URL.Path, Equals, "/this")
}
+func (cs *clientSuite) TestClientSetMaintenanceForMaintenanceJSON(c *C) {
+ // write a maintenance.json that says snapd is down for a restart
+ maintErr := &client.Error{
+ Kind: client.ErrorKindSystemRestart,
+ Message: "system is restarting",
+ }
+
+ b, err := json.Marshal(maintErr)
+ c.Assert(err, IsNil)
+
+ c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil)
+ c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), IsNil)
+ var v []int
+ cs.rsp = `[1,2]`
+ reqBody := ioutil.NopCloser(strings.NewReader(""))
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil)
+ c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 200)
+ c.Check(v, DeepEquals, []int{1, 2})
+ c.Assert(cs.req, NotNil)
+ c.Assert(cs.req.URL, NotNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.Body, Equals, reqBody)
+ c.Check(cs.req.URL.Path, Equals, "/this")
+
+ returnedErr := cs.cli.Maintenance()
+ c.Assert(returnedErr, Not(IsNil))
+
+ returnedMaintErr, ok := returnedErr.(*client.Error)
+ c.Check(ok, Equals, true)
+
+ c.Assert(returnedMaintErr, DeepEquals, maintErr)
+}
+
+func (cs *clientSuite) TestClientIgnoresGarbageMaintenanceJSON(c *C) {
+ // write a garbage maintenance.json that can't be unmarshalled
+ maintGarbage := []byte("blah blah blah not json")
+
+ c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil)
+ c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, maintGarbage, 0644), IsNil)
+ var v []int
+ cs.rsp = `[1,2]`
+ reqBody := ioutil.NopCloser(strings.NewReader(""))
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil)
+ c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 200)
+ c.Check(v, DeepEquals, []int{1, 2})
+ c.Assert(cs.req, NotNil)
+ c.Assert(cs.req.URL, NotNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.Body, Equals, reqBody)
+ c.Check(cs.req.URL.Path, Equals, "/this")
+
+ returnedErr := cs.cli.Maintenance()
+ c.Assert(returnedErr, IsNil)
+}
+
func (cs *clientSuite) TestClientDoNoTimeoutIgnoresRetry(c *C) {
var v []int
cs.rsp = `[1,2]`
diff --git a/client/console_conf.go b/client/console_conf.go
new file mode 100644
index 0000000000..d07899ce03
--- /dev/null
+++ b/client/console_conf.go
@@ -0,0 +1,44 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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 "time"
+
+// InternalConsoleConfStartResponse is the response from console-conf start
+// support
+type InternalConsoleConfStartResponse struct {
+ ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"`
+ ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"`
+}
+
+// InternalConsoleConfStart invokes the dedicated console-conf start support
+// to handle intervening auto-refreshes.
+// Not for general use.
+func (client *Client) InternalConsoleConfStart() ([]string, []string, error) {
+ resp := &InternalConsoleConfStartResponse{}
+ // do the post with a short timeout so that if snapd is not available due to
+ // maintenance we will return very quickly so the caller can handle that
+ opts := &doOptions{
+ Timeout: 2 * time.Second,
+ Retry: 1 * time.Hour,
+ }
+ _, err := client.doSyncWithOpts("POST", "/v2/internal/console-conf-start", nil, nil, nil, resp, opts)
+ return resp.ActiveAutoRefreshChanges, resp.ActiveAutoRefreshSnaps, err
+}
diff --git a/client/console_conf_test.go b/client/console_conf_test.go
new file mode 100644
index 0000000000..09aab5fff2
--- /dev/null
+++ b/client/console_conf_test.go
@@ -0,0 +1,63 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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 (
+ . "gopkg.in/check.v1"
+)
+
+func (cs *clientSuite) TestClientInternalConsoleConfEndpointEmpty(c *C) {
+ // no changes and no snaps
+ cs.status = 200
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {}
+ }`
+
+ chgs, snaps, err := cs.cli.InternalConsoleConfStart()
+ c.Assert(chgs, HasLen, 0)
+ c.Assert(snaps, HasLen, 0)
+ c.Assert(err, IsNil)
+ c.Check(cs.req.Method, Equals, "POST")
+ c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start")
+ c.Check(cs.doCalls, Equals, 1)
+}
+
+func (cs *clientSuite) TestClientInternalConsoleConfEndpoint(c *C) {
+ // some changes and snaps
+ cs.status = 200
+ cs.rsp = `{
+ "type": "sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel"]
+ }
+ }`
+
+ chgs, snaps, err := cs.cli.InternalConsoleConfStart()
+ c.Assert(err, IsNil)
+ c.Assert(chgs, DeepEquals, []string{"1"})
+ c.Assert(snaps, DeepEquals, []string{"pc-kernel"})
+ c.Check(cs.req.Method, Equals, "POST")
+ c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start")
+ c.Check(cs.doCalls, Equals, 1)
+}
diff --git a/cmd/snap/cmd_routine_console_conf.go b/cmd/snap/cmd_routine_console_conf.go
new file mode 100644
index 0000000000..cb412c1e1d
--- /dev/null
+++ b/cmd/snap/cmd_routine_console_conf.go
@@ -0,0 +1,144 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/snapcore/snapd/client"
+
+ "github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/i18n"
+)
+
+type cmdRoutineConsoleConfStart struct {
+ clientMixin
+}
+
+var shortRoutineConsoleConfStartHelp = i18n.G("Start console-conf snapd routine")
+var longRoutineConsoleConfStartHelp = i18n.G(`
+The console-conf-start command starts synchronization with console-conf
+
+This command is used by console-conf when it starts up. It delays refreshes if
+there are none currently ongoing, and exits with a specific error code if there
+are ongoing refreshes which console-conf should wait for before prompting the
+user to begin configuring the device.
+`)
+
+// TODO: move these to their own package for unified time constants for how
+// often or long we do things like waiting for a reboot, etc. ?
+var snapdAPIInterval = 2 * time.Second
+var snapdWaitForFullSystemReboot = 10 * time.Minute
+
+func init() {
+ c := addRoutineCommand("console-conf-start", shortRoutineConsoleConfStartHelp, longRoutineConsoleConfStartHelp, func() flags.Commander {
+ return &cmdRoutineConsoleConfStart{}
+ }, nil, nil)
+ c.hidden = true
+}
+
+func printfFunc(msg string, format ...interface{}) func() {
+ return func() {
+ fmt.Fprintf(Stderr, msg, format...)
+ }
+}
+
+func (x *cmdRoutineConsoleConfStart) Execute(args []string) error {
+ snapdReloadMsgOnce := sync.Once{}
+ systemReloadMsgOnce := sync.Once{}
+ snapRefreshMsgOnce := sync.Once{}
+
+ for {
+ chgs, snaps, err := x.client.InternalConsoleConfStart()
+ if err != nil {
+ // snapd may be under maintenance right now, either for base/kernel
+ // snap refreshes which result in a reboot, or for snapd itself
+ // which just results in a restart of the daemon
+ maybeMaintErr := x.client.Maintenance()
+ if maybeMaintErr == nil {
+ // not a maintenance error, give up
+ return err
+ }
+
+ maintErr, ok := maybeMaintErr.(*client.Error)
+ if !ok {
+ // if cli.Maintenance() didn't return a client.Error we have very weird
+ // problems
+ return fmt.Errorf("internal error: client.Maintenance() didn't return a client.Error")
+ }
+
+ if maintErr.Kind == client.ErrorKindDaemonRestart {
+ // then we need to wait for snapd to restart, so keep trying
+ // the console-conf-start endpoint until it works
+ snapdReloadMsgOnce.Do(printfFunc("Snapd is reloading, please wait...\n"))
+
+ // we know that snapd isn't available because it is in
+ // maintenance so we don't gain anything by hitting it
+ // more frequently except for perhaps a quicker latency
+ // for the user when it comes back, but it will be busy
+ // doing things when it starts up anyways so it won't be
+ // able to respond immediately
+ time.Sleep(snapdAPIInterval)
+ continue
+ } else if maintErr.Kind == client.ErrorKindSystemRestart {
+ // system is rebooting, just wait for the reboot
+ systemReloadMsgOnce.Do(printfFunc("System is rebooting, please wait for reboot...\n"))
+ time.Sleep(snapdWaitForFullSystemReboot)
+ // if we didn't reboot after 10 minutes something's probably broken
+ return fmt.Errorf("system didn't reboot after 10 minutes even though snapd daemon is in maintenance")
+ }
+ }
+
+ if len(chgs) == 0 {
+ break
+ }
+
+ if len(snaps) == 0 {
+ // internal error if we have chg id's, but no snaps
+ return fmt.Errorf("internal error: returned changes (%v) but no snap names", chgs)
+ }
+
+ snapRefreshMsgOnce.Do(func() {
+ sort.Strings(snaps)
+
+ var snapNameList string
+ switch len(snaps) {
+ case 1:
+ snapNameList = snaps[0]
+ case 2:
+ snapNameList = fmt.Sprintf("%s and %s", snaps[0], snaps[1])
+ default:
+ // don't forget the oxford comma!
+ snapNameList = fmt.Sprintf("%s, and %s", strings.Join(snaps[:len(snaps)-1], ", "), snaps[len(snaps)-1])
+ }
+
+ fmt.Fprintf(Stderr, "Snaps (%s) are refreshing, please wait...\n", snapNameList)
+ })
+
+ // don't DDOS snapd by hitting it's API too often
+ time.Sleep(snapdAPIInterval)
+ }
+
+ return nil
+}
diff --git a/cmd/snap/cmd_routine_console_conf_test.go b/cmd/snap/cmd_routine_console_conf_test.go
new file mode 100644
index 0000000000..50c529d487
--- /dev/null
+++ b/cmd/snap/cmd_routine_console_conf_test.go
@@ -0,0 +1,355 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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"
+ "net/http"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func (s *SnapSuite) TestRoutineConsoleConfStartTrivialCase(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartInconsistentAPIResponseError(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ // return just refresh changes but no snap ids
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"]
+ }
+ }`)
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, ErrorMatches, `internal error: returned changes .* but no snap names`)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartNonMaintenanceErrorReturned(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ // return internal server error
+ fmt.Fprintf(w, `{
+ "type":"error",
+ "status-code": 500,
+ "result": {
+ "message": "broken server"
+ }
+ }`)
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, ErrorMatches, "broken server")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartSingleSnap(c *C) {
+ // make the command hit the API as fast as possible for testing
+ r := snap.MockSnapdAPIInterval(0)
+ defer r()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ switch n {
+ // first 4 times we hit the API there is a snap refresh ongoing
+ case 1, 2, 3, 4:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ // return just refresh changes but no snap ids
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel"]
+ }
+ }`)
+ // 5th time we return nothing as we are done
+ case 5:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+
+ default:
+ c.Errorf("unexpected request %v", n)
+ }
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "Snaps (pc-kernel) are refreshing, please wait...\n")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartTwoSnaps(c *C) {
+ // make the command hit the API as fast as possible for testing
+ r := snap.MockSnapdAPIInterval(0)
+ defer r()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ switch n {
+ // first 4 times we hit the API there is a snap refresh ongoing
+ case 1, 2, 3, 4:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ // return just refresh changes but no snap ids
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel","core20"]
+ }
+ }`)
+ // 5th time we return nothing as we are done
+ case 5:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+
+ default:
+ c.Errorf("unexpected request %v", n)
+ }
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "Snaps (core20 and pc-kernel) are refreshing, please wait...\n")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartMultipleSnaps(c *C) {
+ // make the command hit the API as fast as possible for testing
+ r := snap.MockSnapdAPIInterval(0)
+ defer r()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ switch n {
+ // first 4 times we hit the API there are snap refreshes ongoing
+ case 1, 2, 3, 4:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel","snapd","core20","pc"]
+ }
+ }`)
+ // 5th time we return nothing as we are done
+ case 5:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+
+ default:
+ c.Errorf("unexpected request %v", n)
+ }
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), Equals, "Snaps (core20, pc, pc-kernel, and snapd) are refreshing, please wait...\n")
+}
+
+// TODO:UC20: when maintenance.json is a thing, then add a similar test as this
+// one, but using the maintenance.json file instead of the maintenance response
+// as that is closer to the real desired behavior
+func (s *SnapSuite) TestRoutineConsoleConfStartSnapdRefreshRestart(c *C) {
+ // make the command hit the API as fast as possible for testing
+ r := snap.MockSnapdAPIInterval(0)
+ defer r()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ switch n {
+
+ // 1st time we hit the API there is a snapd snap refresh ongoing
+ case 1:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["snapd"]
+ }
+ }`)
+
+ // 2nd time we hit the API, set maintenance in the response
+ case 2:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["snapd"]
+ },
+ "maintenance": {
+ "kind": "daemon-restart",
+ "message": "daemon is restarting"
+ }
+ }`)
+
+ // 3rd time we return nothing as if we are down for maintenance
+ case 3:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ // 4th time we resume responding, but still in progress
+ case 4:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["snapd"]
+ }
+ }`)
+
+ // 5th time we are actually done
+ case 5:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`)
+
+ default:
+ c.Errorf("unexpected request %v", n)
+ }
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), testutil.Contains, "Snapd is reloading, please wait...\n")
+ c.Check(s.Stderr(), testutil.Contains, "Snaps (snapd) are refreshing, please wait...\n")
+}
+
+func (s *SnapSuite) TestRoutineConsoleConfStartKernelRefreshReboot(c *C) {
+ // make the command hit the API as fast as possible for testing
+ r := snap.MockSnapdAPIInterval(0)
+ defer r()
+ r = snap.MockSnapdWaitForFullSystemReboot(0)
+ defer r()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ n++
+ switch n {
+
+ // 1st time we hit the API there is a snapd snap refresh ongoing
+ case 1:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel"]
+ }
+ }`)
+
+ // 2nd time we hit the API, set maintenance in the response, but still
+ // give a valid response (so that it reads the maintenance)
+ case 2:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+
+ fmt.Fprintf(w, `{
+ "type":"sync",
+ "status-code": 200,
+ "result": {
+ "active-auto-refreshes": ["1"],
+ "active-auto-refresh-snaps": ["pc-kernel"]
+ },
+ "maintenance": {
+ "kind": "system-restart",
+ "message": "system is restarting"
+ }
+ }`)
+
+ // 3rd time we hit the API, we need to not return anything so that the
+ // client will inspect the error and see there is a maintenance error
+ case 3:
+ c.Check(r.Method, Equals, "POST")
+ c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start")
+ default:
+ c.Errorf("unexpected %s request (number %d) to %s", r.Method, n, r.URL.Path)
+ }
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"})
+ // this is the internal error, which we will hit immediately for testing,
+ // in a real scenario a reboot would happen OOTB from the snap client
+ c.Assert(err, ErrorMatches, "system didn't reboot after 10 minutes even though snapd daemon is in maintenance")
+ c.Check(s.Stdout(), Equals, "")
+ c.Check(s.Stderr(), testutil.Contains, "System is rebooting, please wait for reboot...\n")
+ c.Check(s.Stderr(), testutil.Contains, "Snaps (pc-kernel) are refreshing, please wait...\n")
+}
diff --git a/cmd/snap/export_test.go b/cmd/snap/export_test.go
index 23f0418770..733b065488 100644
--- a/cmd/snap/export_test.go
+++ b/cmd/snap/export_test.go
@@ -384,3 +384,19 @@ func MockDownloadDirect(f func(snapName string, revision snap.Revision, dlOpts i
downloadDirect = old
}
}
+
+func MockSnapdAPIInterval(t time.Duration) (restore func()) {
+ old := snapdAPIInterval
+ snapdAPIInterval = t
+ return func() {
+ snapdAPIInterval = old
+ }
+}
+
+func MockSnapdWaitForFullSystemReboot(t time.Duration) (restore func()) {
+ old := snapdWaitForFullSystemReboot
+ snapdWaitForFullSystemReboot = t
+ return func() {
+ snapdWaitForFullSystemReboot = old
+ }
+}
diff --git a/daemon/api.go b/daemon/api.go
index 541c01b5a9..4465cc0f42 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -113,6 +113,7 @@ var api = []*Command{
serialModelCmd,
systemsCmd,
systemsActionCmd,
+ routineConsoleConfStartCmd,
}
var servicestateControl = servicestate.Control
diff --git a/daemon/api_console_conf.go b/daemon/api_console_conf.go
new file mode 100644
index 0000000000..dda28eca95
--- /dev/null
+++ b/daemon/api_console_conf.go
@@ -0,0 +1,96 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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 daemon
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/auth"
+)
+
+var (
+ routineConsoleConfStartCmd = &Command{
+ Path: "/v2/internal/console-conf-start",
+ POST: consoleConfStartRoutine,
+ }
+)
+
+var delayTime = 20 * time.Minute
+
+type consoleConfRoutine struct{}
+
+// ConsoleConfStartRoutineResult is the result of running the console-conf start
+// routine..
+type ConsoleConfStartRoutineResult struct {
+ ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"`
+ ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"`
+}
+
+func consoleConfStartRoutine(c *Command, r *http.Request, _ *auth.UserState) Response {
+ // no body expected, error if we were provided anything
+ defer r.Body.Close()
+ var routineBody struct{}
+ decoder := json.NewDecoder(r.Body)
+ if err := decoder.Decode(&routineBody); err != nil && err != io.EOF {
+ return BadRequest("cannot decode request body into console-conf operation: %v", err)
+ }
+
+ // now run the start routine first by trying to grab a lock on the refreshes
+ // for all snaps, which fails if there are any active changes refreshing
+ // snaps
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ snapAutoRefreshChanges, err := c.d.overlord.SnapManager().EnsureAutoRefreshesAreDelayed(delayTime)
+ if err != nil {
+ return InternalError(err.Error())
+ }
+
+ logger.Debugf("Ensured that new auto refreshes are delayed by %s to allow console-conf to run", delayTime)
+
+ if len(snapAutoRefreshChanges) == 0 {
+ // no changes yet, and we delayed the refresh successfully so
+ // console-conf is okay to run normally
+ return SyncResponse(&ConsoleConfStartRoutineResult{}, nil)
+ }
+
+ chgIds := make([]string, 0, len(snapAutoRefreshChanges))
+ snapNames := make([]string, 0)
+ for _, chg := range snapAutoRefreshChanges {
+ chgIds = append(chgIds, chg.ID())
+ var updatedSnaps []string
+ err := chg.Get("snap-names", &updatedSnaps)
+ if err != nil {
+ return InternalError(err.Error())
+ }
+ snapNames = append(snapNames, updatedSnaps...)
+ }
+
+ // we have changes that the client should wait for before being ready
+ return SyncResponse(&ConsoleConfStartRoutineResult{
+ ActiveAutoRefreshChanges: chgIds,
+ ActiveAutoRefreshSnaps: snapNames,
+ }, nil)
+}
diff --git a/daemon/api_console_conf_test.go b/daemon/api_console_conf_test.go
new file mode 100644
index 0000000000..19380fa09d
--- /dev/null
+++ b/daemon/api_console_conf_test.go
@@ -0,0 +1,116 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2020 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 daemon
+
+import (
+ "bytes"
+ "net/http"
+ "sort"
+ "time"
+
+ "github.com/snapcore/snapd/overlord/configstate/config"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "gopkg.in/check.v1"
+ . "gopkg.in/check.v1"
+)
+
+var _ = Suite(&consoleConfSuite{})
+
+type consoleConfSuite struct {
+ apiBaseSuite
+}
+
+func (s *consoleConfSuite) TestPostConsoleConfStartRoutine(c *C) {
+ t0 := time.Now()
+ d := s.daemonWithOverlordMock(c)
+ snapMgr, err := snapstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(snapMgr)
+
+ st := d.overlord.State()
+
+ body := bytes.NewBuffer(nil)
+ req, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body)
+ c.Assert(err, IsNil)
+
+ // no changes in state, no changes in response
+ rsp := consoleConfStartRoutine(routineConsoleConfStartCmd, req, nil).(*resp)
+ c.Check(rsp.Type, Equals, ResponseTypeSync)
+ c.Assert(rsp.Result, DeepEquals, &ConsoleConfStartRoutineResult{})
+
+ // we did set the refresh.hold time back 20 minutes though
+ st.Lock()
+ defer st.Unlock()
+
+ tr := config.NewTransaction(st)
+ var t1 time.Time
+ err = tr.Get("core", "refresh.hold", &t1)
+ c.Assert(err, IsNil)
+
+ c.Assert(t0.Add(20*time.Minute).After(t1), Equals, false)
+
+ // if we add some changes to state that are in progress, then they are
+ // returned
+
+ // now make some auto-refresh changes to make sure we get those figured out
+ chg0 := st.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg0.AddTask(st.NewTask("nop", "do nothing"))
+
+ // make it in doing state
+ chg0.SetStatus(state.DoingStatus)
+ chg0.Set("snap-names", []string{"doing-snap"})
+
+ // this one will be picked up too
+ chg1 := st.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg1.AddTask(st.NewTask("nop", "do nothing"))
+ chg1.SetStatus(state.DoStatus)
+ chg1.Set("snap-names", []string{"do-snap"})
+
+ // this one won't, it's Done
+ chg2 := st.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg2.AddTask(st.NewTask("nop", "do nothing"))
+ chg2.SetStatus(state.DoneStatus)
+ chg2.Set("snap-names", []string{"done-snap"})
+
+ // nor this one, it's Undone
+ chg3 := st.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg3.AddTask(st.NewTask("nop", "do nothing"))
+ chg3.SetStatus(state.UndoneStatus)
+ chg3.Set("snap-names", []string{"undone-snap"})
+
+ st.Unlock()
+ defer st.Lock()
+
+ req2, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body)
+ c.Assert(err, IsNil)
+ rsp2 := consoleConfStartRoutine(routineConsoleConfStartCmd, req2, nil).(*resp)
+ c.Check(rsp2.Type, Equals, ResponseTypeSync)
+ c.Assert(rsp2.Result, FitsTypeOf, &ConsoleConfStartRoutineResult{})
+ res := rsp2.Result.(*ConsoleConfStartRoutineResult)
+ sort.Strings(res.ActiveAutoRefreshChanges)
+ sort.Strings(res.ActiveAutoRefreshSnaps)
+ expChgs := []string{chg0.ID(), chg1.ID()}
+ sort.Strings(expChgs)
+ c.Assert(res, DeepEquals, &ConsoleConfStartRoutineResult{
+ ActiveAutoRefreshChanges: expChgs,
+ ActiveAutoRefreshSnaps: []string{"do-snap", "doing-snap"},
+ })
+}
diff --git a/daemon/api_systems_test.go b/daemon/api_systems_test.go
index 8cd0f1ffc4..b98cb62bc1 100644
--- a/daemon/api_systems_test.go
+++ b/daemon/api_systems_test.go
@@ -487,7 +487,7 @@ func (s *apiSuite) TestSystemActionRequestWithSeeded(c *check.C) {
// daemon is not started, only check whether reboot was scheduled as expected
// reboot flag
- c.Check(d.restartSystem, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment))
+ c.Check(d.requestedRestart, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment))
// slow reboot schedule
c.Check(cmd.Calls(), check.DeepEquals, [][]string{
{"shutdown", "-r", "+10", "reboot scheduled to update the system"},
diff --git a/daemon/daemon.go b/daemon/daemon.go
index 640010b938..4ad3798ae9 100644
--- a/daemon/daemon.go
+++ b/daemon/daemon.go
@@ -20,7 +20,9 @@
package daemon
import (
+ "bytes"
"context"
+ "encoding/json"
"fmt"
"net"
"net/http"
@@ -55,6 +57,12 @@ var ErrRestartSocket = fmt.Errorf("daemon stop requested to wait for socket acti
var systemdSdNotify = systemd.SdNotify
+const (
+ daemonRestartMsg = "system is restarting"
+ systemRestartMsg = "daemon is restarting"
+ socketRestartMsg = "daemon is stopping to wait for socket activation"
+)
+
// A Daemon listens for requests and routes them to the right command
type Daemon struct {
Version string
@@ -68,8 +76,8 @@ type Daemon struct {
router *mux.Router
standbyOpinions *standby.StandbyOpinions
- // set to remember we need to restart the system
- restartSystem state.RestartType
+ // set to what kind of restart was requested if any
+ requestedRestart state.RestartType
// set to remember that we need to exit the daemon in a way that
// prevents systemd from restarting it
restartSocket bool
@@ -259,14 +267,10 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if rsp, ok := rsp.(*resp); ok {
_, rst := st.Restarting()
- switch rst {
- case state.RestartSystem, state.RestartSystemNow:
- rsp.transmitMaintenance(client.ErrorKindSystemRestart, "system is restarting")
- case state.RestartDaemon:
- rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is restarting")
- case state.RestartSocket:
- rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is stopping to wait for socket activation")
+ if rst != state.RestartUnset {
+ rsp.Maintenance = maintenanceForRestartType(rst)
}
+
if rsp.Type != ResponseTypeError {
st.Lock()
count, stamp := st.WarningsSummary()
@@ -450,6 +454,14 @@ func (d *Daemon) Start() error {
// enable standby handling
d.initStandbyHandling()
+ // before serving actual connections remove the maintenance.json file as we
+ // are no longer down for maintenance, this state most closely corresponds
+ // to state.RestartUnset
+ err = d.updateMaintenanceFile(state.RestartUnset)
+ if err != nil {
+ return err
+ }
+
// the loop runs in its own goroutine
d.overlord.Loop()
@@ -481,6 +493,10 @@ func (d *Daemon) HandleRestart(t state.RestartType) {
// die when asked to restart (systemd should get us back up!) etc
switch t {
case state.RestartDaemon:
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ // save the restart kind to write out a maintenance.json in a bit
+ d.requestedRestart = t
case state.RestartSystem, state.RestartSystemNow:
// try to schedule a fallback slow reboot already here
// in case we get stuck shutting down
@@ -490,17 +506,20 @@ func (d *Daemon) HandleRestart(t state.RestartType) {
d.mu.Lock()
defer d.mu.Unlock()
- // remember we need to restart the system
- d.restartSystem = t
+ // save the restart kind to write out a maintenance.json in a bit
+ d.requestedRestart = t
case state.RestartSocket:
d.mu.Lock()
defer d.mu.Unlock()
+ // save the restart kind to write out a maintenance.json in a bit
+ d.requestedRestart = t
d.restartSocket = true
case state.StopDaemon:
logger.Noticef("stopping snapd as requested")
default:
logger.Noticef("internal error: restart handler called with unknown restart type: %v", t)
}
+
d.tomb.Kill(nil)
}
@@ -511,6 +530,32 @@ var (
rebootMaxTentatives = 3
)
+func (d *Daemon) updateMaintenanceFile(rst state.RestartType) error {
+ // for unset restart, just remove the maintenance.json file
+ if rst == state.RestartUnset {
+ err := os.Remove(dirs.SnapdMaintenanceFile)
+ // only return err if the error was something other than the file not
+ // existing
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ return nil
+ }
+
+ // otherwise marshal and write it out appropriately
+ b, err := json.Marshal(maintenanceForRestartType(rst))
+ if err != nil {
+ return err
+ }
+
+ err = osutil.AtomicWrite(dirs.SnapdMaintenanceFile, bytes.NewBuffer(b), 0644, 0)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
// Stop shuts down the Daemon
func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
// we need to schedule/wait for a system restart again
@@ -525,12 +570,24 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
d.tomb.Kill(nil)
+ // check the state associated with a potential restart with the lock to
+ // prevent races
d.mu.Lock()
- restartSystem := d.restartSystem != state.RestartUnset
- immediateReboot := d.restartSystem == state.RestartSystemNow
+ // needsFullReboot is whether the entire system will be rebooted or not as
+ // a consequence of this restart
+ needsFullReboot := (d.requestedRestart == state.RestartSystemNow || d.requestedRestart == state.RestartSystem)
+ immediateReboot := d.requestedRestart == state.RestartSystemNow
restartSocket := d.restartSocket
d.mu.Unlock()
+ // before not accepting any new client connections we need to write the
+ // maintenance.json file for potential clients to see after the daemon stops
+ // responding so they can read it correctly and handle the maintenance
+ err := d.updateMaintenanceFile(d.requestedRestart)
+ if err != nil {
+ logger.Noticef("error writing maintenance file: %v", err)
+ }
+
d.snapdListener.Close()
d.standbyOpinions.Stop()
@@ -547,7 +604,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
d.snapListener.Close()
}
- if restartSystem {
+ if needsFullReboot {
// give time to polling clients to notice restart
time.Sleep(rebootNoticeWait)
}
@@ -559,10 +616,9 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
d.tomb.Kill(d.serve.Shutdown(ctx))
cancel()
- if !restartSystem {
+ if !needsFullReboot {
// tell systemd that we are stopping
systemdSdNotify("STOPPING=1")
-
}
if restartSocket {
@@ -580,7 +636,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
}
d.overlord.Stop()
- err := d.tomb.Wait()
+ err = d.tomb.Wait()
if err != nil {
if err == context.DeadlineExceeded {
logger.Noticef("WARNING: cannot gracefully shut down in-flight snapd API activity within: %v", shutdownTimeout)
@@ -592,7 +648,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
// because we already scheduled a slow shutdown and
// exiting here will just restart snapd (via systemd)
// which will lead to confusing results.
- if restartSystem {
+ if needsFullReboot {
logger.Noticef("WARNING: cannot stop daemon: %v", err)
} else {
return err
@@ -600,7 +656,7 @@ func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
}
}
- if restartSystem {
+ if needsFullReboot {
return d.doReboot(sigCh, immediateReboot, rebootWaitTimeout)
}
diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
index bf6344edb2..b2bde1db8d 100644
--- a/daemon/daemon_test.go
+++ b/daemon/daemon_test.go
@@ -209,6 +209,80 @@ func (s *daemonSuite) TestCommandRestartingState(c *check.C) {
})
}
+func (s *daemonSuite) TestMaintenanceJsonDeletedOnStart(c *check.C) {
+ // write a maintenance.json file that has that the system is restarting
+ maintErr := &errorResult{
+ Kind: client.ErrorKindDaemonRestart,
+ Message: systemRestartMsg,
+ }
+
+ b, err := json.Marshal(maintErr)
+ c.Assert(err, check.IsNil)
+
+ c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), check.IsNil)
+
+ c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), check.IsNil)
+
+ d := newTestDaemon(c)
+
+ // mark as already seeded
+ s.markSeeded(d)
+ // and pretend we have snaps
+ st := d.overlord.State()
+ st.Lock()
+ snapstate.Set(st, "core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"},
+ },
+ Current: snap.R(1),
+ })
+ st.Unlock()
+ // 1 snap => extended timeout 30s + 5s
+ const extendedTimeoutUSec = "EXTEND_TIMEOUT_USEC=35000000"
+
+ l1, err := net.Listen("tcp", "127.0.0.1:0")
+ c.Assert(err, check.IsNil)
+ l2, err := net.Listen("tcp", "127.0.0.1:0")
+ c.Assert(err, check.IsNil)
+
+ snapdAccept := make(chan struct{})
+ d.snapdListener = &witnessAcceptListener{Listener: l1, accept: snapdAccept}
+
+ snapAccept := make(chan struct{})
+ d.snapListener = &witnessAcceptListener{Listener: l2, accept: snapAccept}
+
+ d.Start()
+
+ snapdDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapdAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snapd accept was not called")
+ }
+ close(snapdDone)
+ }()
+
+ snapDone := make(chan struct{})
+ go func() {
+ select {
+ case <-snapAccept:
+ case <-time.After(2 * time.Second):
+ c.Fatal("snapd accept was not called")
+ }
+ close(snapDone)
+ }()
+
+ <-snapdDone
+ <-snapDone
+
+ // maintenance.json should be removed
+ c.Assert(dirs.SnapdMaintenanceFile, testutil.FileAbsent)
+
+ d.Stop(nil)
+}
+
func (s *daemonSuite) TestFillsWarnings(c *check.C) {
d := newTestDaemon(c)
@@ -590,7 +664,12 @@ func (s *daemonSuite) TestRestartWiring(c *check.C) {
d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept}
c.Assert(d.Start(), check.IsNil)
- defer d.Stop(nil)
+ stoppedYet := false
+ defer func() {
+ if !stoppedYet {
+ d.Stop(nil)
+ }
+ }()
snapdDone := make(chan struct{})
go func() {
@@ -622,6 +701,11 @@ func (s *daemonSuite) TestRestartWiring(c *check.C) {
case <-time.After(2 * time.Second):
c.Fatal("RequestRestart -> overlord -> Kill chain didn't work")
}
+
+ d.Stop(nil)
+ stoppedYet = true
+
+ c.Assert(s.notified, check.DeepEquals, []string{"EXTEND_TIMEOUT_USEC=30000000", "READY=1", "STOPPING=1"})
}
func (s *daemonSuite) TestGracefulStop(c *check.C) {
@@ -877,7 +961,7 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest
defer func() {
d.mu.Lock()
- d.restartSystem = state.RestartUnset
+ d.requestedRestart = state.RestartUnset
d.mu.Unlock()
}()
@@ -888,7 +972,7 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest
}
d.mu.Lock()
- rs := d.restartSystem
+ rs := d.requestedRestart
d.mu.Unlock()
c.Check(rs, check.Equals, restartKind)
@@ -924,6 +1008,17 @@ func (s *daemonSuite) testRestartSystemWiring(c *check.C, restartKind state.Rest
// should be good enough
c.Check(rebootAt.Before(now.Add(10*time.Second)), check.Equals, true)
}
+
+ // finally check that maintenance.json was written appropriate for this
+ // restart reason
+ b, err := ioutil.ReadFile(dirs.SnapdMaintenanceFile)
+ c.Assert(err, check.IsNil)
+
+ maintErr := &errorResult{}
+ c.Assert(json.Unmarshal(b, maintErr), check.IsNil)
+
+ exp := maintenanceForRestartType(restartKind)
+ c.Assert(maintErr, check.DeepEquals, exp)
}
func (s *daemonSuite) TestRestartSystemGracefulWiring(c *check.C) {
diff --git a/daemon/response.go b/daemon/response.go
index d2b0091b2d..680874994c 100644
--- a/daemon/response.go
+++ b/daemon/response.go
@@ -37,6 +37,7 @@ import (
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/snapshotstate"
"github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/systemd"
@@ -67,11 +68,23 @@ type resp struct {
Maintenance *errorResult `json:"maintenance,omitempty"`
}
-func (r *resp) transmitMaintenance(kind client.ErrorKind, message string) {
- r.Maintenance = &errorResult{
- Kind: kind,
- Message: message,
- }
+func maintenanceForRestartType(rst state.RestartType) *errorResult {
+ e := &errorResult{}
+ switch rst {
+ case state.RestartSystem, state.RestartSystemNow:
+ e.Kind = client.ErrorKindSystemRestart
+ e.Message = daemonRestartMsg
+ case state.RestartDaemon:
+ e.Kind = client.ErrorKindDaemonRestart
+ e.Message = systemRestartMsg
+ case state.RestartSocket:
+ e.Kind = client.ErrorKindDaemonRestart
+ e.Message = socketRestartMsg
+ case state.RestartUnset:
+ // shouldn't happen, maintenance for unset type should just be nil
+ panic("internal error: cannot marshal maintenance for RestartUnset")
+ }
+ return e
}
func (r *resp) addWarningsToMeta(count int, stamp time.Time) {
@@ -92,7 +105,7 @@ func (r *resp) addWarningsToMeta(count int, stamp time.Time) {
// JSON representation in the API in time for the release.
// The right code style takes a bit more work and unifies
// these fields inside resp.
-// Increment the counter if you read this: 42
+// Increment the counter if you read this: 43
type Meta struct {
Sources []string `json:"sources,omitempty"`
SuggestedCurrency string `json:"suggested-currency,omitempty"`
diff --git a/dirs/dirs.go b/dirs/dirs.go
index 30bc7b4503..20dc5cac43 100644
--- a/dirs/dirs.go
+++ b/dirs/dirs.go
@@ -58,6 +58,8 @@ var (
SnapRunLockDir string
SnapBootstrapRunDir string
+ SnapdMaintenanceFile string
+
SnapdStoreSSLCertsDir string
SnapSeedDir string
@@ -314,6 +316,7 @@ func SetRootDir(rootdir string) {
SnapSeccompDir = filepath.Join(SnapSeccompBase, "bpf")
SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount")
SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta")
+ SnapdMaintenanceFile = filepath.Join(rootdir, snappyDir, "maintenance.json")
SnapBlobDir = SnapBlobDirUnder(rootdir)
// ${snappyDir}/desktop is added to $XDG_DATA_DIRS.
// Subdirectories are interpreted according to the relevant
diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go
index 19b44b4789..187423e4d0 100644
--- a/interfaces/apparmor/template.go
+++ b/interfaces/apparmor/template.go
@@ -314,6 +314,9 @@ var templateCommon = `
# Read-only of this snap
/var/lib/snapd/snaps/@{SNAP_NAME}_*.snap r,
+ # Read-only of snapd restart state for snapctl specifically
+ /var/lib/snapd/maintenance.json r,
+
# Read-only for the install directory
# bind mount used here (see 'parallel installs', above)
@{INSTALL_DIR}/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/ r,
diff --git a/interfaces/builtin/x11.go b/interfaces/builtin/x11.go
index 9d03374e64..9ff7013a21 100644
--- a/interfaces/builtin/x11.go
+++ b/interfaces/builtin/x11.go
@@ -102,6 +102,7 @@ unix (connect, receive, send, accept)
type=stream
addr="@/tmp/.X11-unix/X[0-9]*"
peer=(label=###PLUG_SECURITY_TAGS###),
+# TODO: deprecate and remove this if it doesn't break X11 server snaps.
unix (connect, receive, send, accept)
type=stream
addr="@/tmp/.ICE-unix/[0-9]*"
@@ -138,6 +139,14 @@ owner /run/user/[0-9]*/mutter/Xauthority r,
network netlink raw,
/run/udev/data/c13:[0-9]* r,
/run/udev/data/+input:* r,
+
+# Deny access to ICE granted by abstractions/X
+# See: https://bugs.launchpad.net/snapd/+bug/1901489
+deny owner @{HOME}/.ICEauthority r,
+deny owner /run/user/*/ICEauthority r,
+deny unix (connect, receive, send)
+ type=stream
+ peer=(addr="@/tmp/.ICE-unix/[0-9]*"),
`
const x11ConnectedPlugSecComp = `
diff --git a/osutil/disks/mockdisk.go b/osutil/disks/mockdisk.go
index 609a417c39..e90514831f 100644
--- a/osutil/disks/mockdisk.go
+++ b/osutil/disks/mockdisk.go
@@ -96,9 +96,23 @@ type Mountpoint struct {
func MockMountPointDisksToPartitionMapping(mockedMountPoints map[Mountpoint]*MockDiskMapping) (restore func()) {
osutil.MustBeTestBinary("mock disks only to be used in tests")
- // verify that all unique MockDiskMapping's have unique DevNum's
+ // verify that all unique MockDiskMapping's have unique DevNum's and that
+ // the srcMntPt's are all consistent
+ // we can't have the same mountpoint exist both as a decrypted device and
+ // not as a decrypted device, this is an impossible mapping, but we need to
+ // expose functionality to mock the same mountpoint as a decrypted device
+ // and as an unencrypyted device for different tests, but never at the same
+ // time with the same mapping
alreadySeen := make(map[string]*MockDiskMapping, len(mockedMountPoints))
- for _, mockDisk := range mockedMountPoints {
+ seenSrcMntPts := make(map[string]bool, len(mockedMountPoints))
+ for srcMntPt, mockDisk := range mockedMountPoints {
+ if decryptedVal, ok := seenSrcMntPts[srcMntPt.Mountpoint]; ok {
+ if decryptedVal != srcMntPt.IsDecryptedDevice {
+ msg := fmt.Sprintf("mocked source mountpoint %s is duplicated with different options - previous option for IsDecryptedDevice was %t, current option is %t", srcMntPt.Mountpoint, decryptedVal, srcMntPt.IsDecryptedDevice)
+ panic(msg)
+ }
+ }
+ seenSrcMntPts[srcMntPt.Mountpoint] = srcMntPt.IsDecryptedDevice
if old, ok := alreadySeen[mockDisk.DevNum]; ok {
if mockDisk != old {
// we already saw a disk with this DevNum as a different pointer
diff --git a/osutil/disks/mockdisk_test.go b/osutil/disks/mockdisk_test.go
index 7acb13e05a..caded22186 100644
--- a/osutil/disks/mockdisk_test.go
+++ b/osutil/disks/mockdisk_test.go
@@ -88,6 +88,31 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesUniquen
defer r()
}
+func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesConsistency(c *C) {
+ d1 := &disks.MockDiskMapping{
+ FilesystemLabelToPartUUID: map[string]string{
+ "label1": "part1",
+ },
+ DiskHasPartitions: true,
+ DevNum: "d1",
+ }
+
+ // a mountpoint mapping where the same mountpoint has different options for
+ // the source mountpoint
+ m := map[disks.Mountpoint]*disks.MockDiskMapping{
+ {Mountpoint: "mount1", IsDecryptedDevice: false}: d1,
+ {Mountpoint: "mount1", IsDecryptedDevice: true}: d1,
+ }
+
+ // mocking shouldn't work
+ c.Assert(
+ func() { disks.MockMountPointDisksToPartitionMapping(m) },
+ PanicMatches,
+ // use .* for true/false since iterating over map order is not defined
+ `mocked source mountpoint mount1 is duplicated with different options - previous option for IsDecryptedDevice was .*, current option is .*`,
+ )
+}
+
func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) {
d1 := &disks.MockDiskMapping{
FilesystemLabelToPartUUID: map[string]string{
diff --git a/overlord/snapstate/autorefresh.go b/overlord/snapstate/autorefresh.go
index a52fe3c26b..16c72cce7c 100644
--- a/overlord/snapstate/autorefresh.go
+++ b/overlord/snapstate/autorefresh.go
@@ -130,6 +130,31 @@ func (m *autoRefresh) EffectiveRefreshHold() (time.Time, error) {
return holdTime, nil
}
+func (m *autoRefresh) ensureRefreshHoldAtLeast(duration time.Duration) error {
+ now := time.Now()
+
+ // get the effective refresh hold and check if it is sooner than the
+ // specified duration in the future
+ effective, err := m.EffectiveRefreshHold()
+ if err != nil {
+ return err
+ }
+
+ if effective.IsZero() || effective.Sub(now) < duration {
+ // the effective refresh hold is sooner than the desired delay, so
+ // move it out to the specified duration
+ holdTime := now.Add(duration)
+ tr := config.NewTransaction(m.state)
+ err := tr.Set("core", "refresh.hold", &holdTime)
+ if err != nil && !config.IsNoOption(err) {
+ return err
+ }
+ tr.Commit()
+ }
+
+ return nil
+}
+
// clearRefreshHold clears refresh.hold configuration.
func (m *autoRefresh) clearRefreshHold() {
tr := config.NewTransaction(m.state)
diff --git a/overlord/snapstate/autorefresh_test.go b/overlord/snapstate/autorefresh_test.go
index f4dca3f371..78ee66b014 100644
--- a/overlord/snapstate/autorefresh_test.go
+++ b/overlord/snapstate/autorefresh_test.go
@@ -569,6 +569,98 @@ func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredReschedule(c *C)
c.Check(nextRefresh1.Before(nextRefresh), Equals, false)
}
+func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeastZeroTimes(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // setup hold-time as time.Time{} and next-refresh as now to simulate real
+ // console-conf-start situations
+ t0 := time.Now()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "refresh.hold", time.Time{})
+ tr.Commit()
+
+ af := snapstate.NewAutoRefresh(s.state)
+ snapstate.MockNextRefresh(af, t0)
+
+ err := af.EnsureRefreshHoldAtLeast(time.Hour)
+ c.Assert(err, IsNil)
+
+ s.state.Unlock()
+ err = af.Ensure()
+ s.state.Lock()
+ c.Check(err, IsNil)
+
+ // refresh did not happen
+ c.Check(s.store.ops, HasLen, 0)
+
+ // hold is now more than an hour later than when the test started
+ tr = config.NewTransaction(s.state)
+ var t1 time.Time
+ err = tr.Get("core", "refresh.hold", &t1)
+ c.Assert(err, IsNil)
+
+ // use After() == false here in case somehow the t0 + 1hr is exactly t1,
+ // Before() and After() are false for the same time instants
+ c.Assert(t0.Add(time.Hour).After(t1), Equals, false)
+}
+
+func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeast(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // setup last-refresh as happening a long time ago, and refresh-hold as
+ // having been expired
+ t0 := time.Now()
+ s.state.Set("last-refresh", t0.Add(-12*time.Hour))
+
+ holdTime := t0.Add(-1 * time.Minute)
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "refresh.hold", holdTime)
+
+ tr.Commit()
+
+ af := snapstate.NewAutoRefresh(s.state)
+ snapstate.MockNextRefresh(af, holdTime.Add(-2*time.Minute))
+
+ err := af.EnsureRefreshHoldAtLeast(time.Hour)
+ c.Assert(err, IsNil)
+
+ s.state.Unlock()
+ err = af.Ensure()
+ s.state.Lock()
+ c.Check(err, IsNil)
+
+ // refresh did not happen
+ c.Check(s.store.ops, HasLen, 0)
+
+ // hold is now more than an hour later than when the test started
+ tr = config.NewTransaction(s.state)
+ var t1 time.Time
+ err = tr.Get("core", "refresh.hold", &t1)
+ c.Assert(err, IsNil)
+
+ // use After() == false here in case somehow the t0 + 1hr is exactly t1,
+ // Before() and After() are false for the same time instants
+ c.Assert(t0.Add(time.Hour).After(t1), Equals, false)
+
+ // setting it to a shorter time will not change it
+ err = af.EnsureRefreshHoldAtLeast(30 * time.Minute)
+ c.Assert(err, IsNil)
+
+ // time is still equal to t1
+ tr = config.NewTransaction(s.state)
+ var t2 time.Time
+ err = tr.Get("core", "refresh.hold", &t2)
+ c.Assert(err, IsNil)
+
+ // when traversing json through the core config transaction, there will be
+ // different wall/monotonic clock times, we remove this ambiguity by
+ // formatting as rfc3339 which will strip this negligible difference in time
+ c.Assert(t1.Format(time.RFC3339), Equals, t2.Format(time.RFC3339))
+}
+
func (s *autoRefreshTestSuite) TestEffectiveRefreshHold(c *C) {
s.state.Lock()
defer s.state.Unlock()
diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go
index e35262efbe..c61d53cb59 100644
--- a/overlord/snapstate/export_test.go
+++ b/overlord/snapstate/export_test.go
@@ -290,3 +290,7 @@ func MockGenericRefreshCheck(fn func(info *snap.Info, canAppRunDuringRefresh fun
genericRefreshCheck = fn
return func() { genericRefreshCheck = old }
}
+
+func (m *autoRefresh) EnsureRefreshHoldAtLeast(d time.Duration) error {
+ return m.ensureRefreshHoldAtLeast(d)
+}
diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go
index 7054b28ddd..90edb0a6aa 100644
--- a/overlord/snapstate/snapmgr.go
+++ b/overlord/snapstate/snapmgr.go
@@ -556,6 +556,29 @@ func (m *SnapManager) RefreshSchedule() (string, bool, error) {
return m.autoRefresh.RefreshSchedule()
}
+// EnsureAutoRefreshesAreDelayed will delay refreshes for the specified amount
+// of time, as well as return any active auto-refresh changes that are currently
+// not ready so that the client can wait for those.
+func (m *SnapManager) EnsureAutoRefreshesAreDelayed(delay time.Duration) ([]*state.Change, error) {
+ // always delay for at least the specified time, this ensures that even if
+ // there are active refreshes right now, there won't be more auto-refreshes
+ // that happen after the current set finish
+ err := m.autoRefresh.ensureRefreshHoldAtLeast(delay)
+ if err != nil {
+ return nil, err
+ }
+
+ // look for auto refresh changes in progress
+ autoRefreshChgs := []*state.Change{}
+ for _, chg := range m.state.Changes() {
+ if chg.Kind() == "auto-refresh" && !chg.Status().Ready() {
+ autoRefreshChgs = append(autoRefreshChgs, chg)
+ }
+ }
+
+ return autoRefreshChgs, nil
+}
+
// ensureForceDevmodeDropsDevmodeFromState undoes the forced devmode
// in snapstate for forced devmode distros.
func (m *SnapManager) ensureForceDevmodeDropsDevmodeFromState() error {
diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go
index 14255717a9..3ad1ec2660 100644
--- a/overlord/snapstate/snapstate_test.go
+++ b/overlord/snapstate/snapstate_test.go
@@ -6261,3 +6261,62 @@ func (s *snapmgrTestSuite) TestForSnapSetupResetsFlags(c *C) {
RequireTypeBase: false,
})
}
+
+func (s *snapmgrTestSuite) TestEnsureAutoRefreshesAreDelayed(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ t0 := time.Now()
+ // with no changes in flight still works and we set the auto-refresh time as
+ // at least one minute past the start of the test
+ chgs, err := s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute)
+ c.Assert(err, IsNil)
+ c.Assert(chgs, HasLen, 0)
+
+ var holdTime time.Time
+ tr := config.NewTransaction(s.state)
+ err = tr.Get("core", "refresh.hold", &holdTime)
+ c.Assert(err, IsNil)
+ // use After() == false in case holdTime is _exactly_ one minute later than
+ // t0, in which case both After() and Before() will be false
+ c.Assert(t0.Add(time.Minute).After(holdTime), Equals, false)
+
+ // now make some auto-refresh changes to make sure we get those figured out
+ chg0 := s.state.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg0.AddTask(s.state.NewTask("nop", "do nothing"))
+
+ // make it in doing state
+ chg0.SetStatus(state.DoingStatus)
+
+ // this one will be picked up too
+ chg1 := s.state.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg1.AddTask(s.state.NewTask("nop", "do nothing"))
+ chg1.SetStatus(state.DoStatus)
+
+ // this one won't, it's Done
+ chg2 := s.state.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg2.AddTask(s.state.NewTask("nop", "do nothing"))
+ chg2.SetStatus(state.DoneStatus)
+
+ // nor this one, it's Undone
+ chg3 := s.state.NewChange("auto-refresh", "auto-refresh-the-things")
+ chg3.AddTask(s.state.NewTask("nop", "do nothing"))
+ chg3.SetStatus(state.UndoneStatus)
+
+ // now we get our change ID returned when calling EnsureAutoRefreshesAreDelayed
+ chgs, err = s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute)
+ c.Assert(err, IsNil)
+ // more helpful error message if we first compare the change ID's
+ expids := []string{chg0.ID(), chg1.ID()}
+ sort.Strings(expids)
+ c.Assert(chgs, HasLen, len(expids))
+ gotids := []string{chgs[0].ID(), chgs[1].ID()}
+ sort.Strings(gotids)
+ c.Assert(expids, DeepEquals, gotids)
+
+ sort.SliceStable(chgs, func(i, j int) bool {
+ return chgs[i].ID() < chgs[j].ID()
+ })
+
+ c.Assert(chgs, DeepEquals, []*state.Change{chg0, chg1})
+}
diff --git a/secboot/export_test.go b/secboot/export_test.go
index ac55ee524b..3cbb99c4ba 100644
--- a/secboot/export_test.go
+++ b/secboot/export_test.go
@@ -38,11 +38,11 @@ func MockSbConnectToDefaultTPM(f func() (*sb.TPMConnection, error)) (restore fun
}
}
-func MockSbProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) {
- old := sbProvisionTPM
- sbProvisionTPM = f
+func MockProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) {
+ old := provisionTPM
+ provisionTPM = f
return func() {
- sbProvisionTPM = old
+ provisionTPM = old
}
}
@@ -78,7 +78,7 @@ func MockSbAddSnapModelProfile(f func(profile *sb.PCRProtectionProfile, params *
}
}
-func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error) (restore func()) {
+func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath string, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error)) (restore func()) {
old := sbSealKeyToTPM
sbSealKeyToTPM = f
return func() {
@@ -86,7 +86,7 @@ func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath, polic
}
}
-func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath, policyUpdatePath string, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) {
+func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath string, authKey sb.TPMPolicyAuthKey, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) {
old := sbUpdateKeyPCRProtectionPolicy
sbUpdateKeyPCRProtectionPolicy = f
return func() {
@@ -94,16 +94,16 @@ func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath, p
}
}
-func MockSbLockAccessToSealedKeys(f func(tpm *sb.TPMConnection) error) (restore func()) {
- old := sbLockAccessToSealedKeys
- sbLockAccessToSealedKeys = f
+func MockSbBlockPCRProtectionPolicies(f func(tpm *sb.TPMConnection, pcrs []int) error) (restore func()) {
+ old := sbBlockPCRProtectionPolicies
+ sbBlockPCRProtectionPolicies = f
return func() {
- sbLockAccessToSealedKeys = old
+ sbBlockPCRProtectionPolicies = old
}
}
func MockSbActivateVolumeWithRecoveryKey(f func(volumeName, sourceDevicePath string,
- keyReader io.Reader, options *sb.ActivateWithRecoveryKeyOptions) error) (restore func()) {
+ keyReader io.Reader, options *sb.ActivateVolumeOptions) error) (restore func()) {
old := sbActivateVolumeWithRecoveryKey
sbActivateVolumeWithRecoveryKey = f
return func() {
@@ -112,7 +112,7 @@ func MockSbActivateVolumeWithRecoveryKey(f func(volumeName, sourceDevicePath str
}
func MockSbActivateVolumeWithTPMSealedKey(f func(tpm *sb.TPMConnection, volumeName, sourceDevicePath, keyPath string,
- pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error)) (restore func()) {
+ pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error)) (restore func()) {
old := sbActivateVolumeWithTPMSealedKey
sbActivateVolumeWithTPMSealedKey = f
return func() {
diff --git a/secboot/secboot.go b/secboot/secboot.go
index e36edcbd03..2ced4046ba 100644
--- a/secboot/secboot.go
+++ b/secboot/secboot.go
@@ -60,8 +60,8 @@ type SealKeyParams struct {
ModelParams []*SealKeyModelParams
// The path to store the sealed key file
KeyFile string
- // The path to the authorization policy update data file (only relevant for TPM)
- TPMPolicyUpdateDataFile string
+ // The path to the authorization policy update key file (only relevant for TPM)
+ TPMPolicyAuthKeyFile string
// The path to the lockout authorization file (only relevant for TPM)
TPMLockoutAuthFile string
}
@@ -71,6 +71,6 @@ type ResealKeyParams struct {
ModelParams []*SealKeyModelParams
// The path to the sealed key file
KeyFile string
- // The path to the authorization policy update data file (only relevant for TPM)
- TPMPolicyUpdateDataFile string
+ // The path to the authorization policy update key file (only relevant for TPM)
+ TPMPolicyAuthKeyFile string
}
diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go
index 2fbe768884..60124a2804 100644
--- a/secboot/secboot_tpm.go
+++ b/secboot/secboot_tpm.go
@@ -24,6 +24,7 @@ import (
"crypto/rand"
"errors"
"fmt"
+ "io/ioutil"
"os"
"path/filepath"
@@ -43,27 +44,29 @@ import (
const (
// Handles are in the block reserved for owner objects (0x01800000 - 0x01bfffff)
- pinHandle = 0x01880000
+ policyCounterHandle = 0x01880001
+
+ keyringPrefix = "snapd"
)
var (
sbConnectToDefaultTPM = sb.ConnectToDefaultTPM
sbMeasureSnapSystemEpochToTPM = sb.MeasureSnapSystemEpochToTPM
sbMeasureSnapModelToTPM = sb.MeasureSnapModelToTPM
- sbLockAccessToSealedKeys = sb.LockAccessToSealedKeys
+ sbBlockPCRProtectionPolicies = sb.BlockPCRProtectionPolicies
sbActivateVolumeWithTPMSealedKey = sb.ActivateVolumeWithTPMSealedKey
sbActivateVolumeWithRecoveryKey = sb.ActivateVolumeWithRecoveryKey
sbAddEFISecureBootPolicyProfile = sb.AddEFISecureBootPolicyProfile
sbAddEFIBootManagerProfile = sb.AddEFIBootManagerProfile
sbAddSystemdEFIStubProfile = sb.AddSystemdEFIStubProfile
sbAddSnapModelProfile = sb.AddSnapModelProfile
- sbProvisionTPM = sb.ProvisionTPM
sbSealKeyToTPM = sb.SealKeyToTPM
sbUpdateKeyPCRProtectionPolicy = sb.UpdateKeyPCRProtectionPolicy
randutilRandomKernelUUID = randutil.RandomKernelUUID
isTPMEnabled = isTPMEnabledImpl
+ provisionTPM = provisionTPMImpl
)
func isTPMEnabledImpl(tpm *sb.TPMConnection) bool {
@@ -116,7 +119,9 @@ func checkSecureBootEnabled() error {
return nil
}
-const tpmPCR = 12
+// initramfsPCR is the TPM PCR that we reserve for the EFI image and use
+// for measurement from the initramfs.
+const initramfsPCR = 12
func secureConnectToTPM(ekcfile string) (*sb.TPMConnection, error) {
ekCertReader, err := os.Open(ekcfile)
@@ -154,7 +159,7 @@ func measureWhenPossible(whatHow func(tpm *sb.TPMConnection) error) error {
// TPM device is available. If there's no TPM device success is returned.
func MeasureSnapSystemEpochWhenPossible() error {
measure := func(tpm *sb.TPMConnection) error {
- return sbMeasureSnapSystemEpochToTPM(tpm, tpmPCR)
+ return sbMeasureSnapSystemEpochToTPM(tpm, initramfsPCR)
}
if err := measureWhenPossible(measure); err != nil {
@@ -172,7 +177,7 @@ func MeasureSnapModelWhenPossible(findModel func() (*asserts.Model, error)) erro
if err != nil {
return err
}
- return sbMeasureSnapModelToTPM(tpm, tpmPCR, model)
+ return sbMeasureSnapModelToTPM(tpm, initramfsPCR, model)
}
if err := measureWhenPossible(measure); err != nil {
@@ -219,7 +224,11 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri
// volumes to unlock we should lock access to the sealed keys only after
// the last encrypted volume is unlocked, in which case lockKeysOnFinish
// should be set to true.
- lockErr = sbLockAccessToSealedKeys(tpm)
+ //
+ // We should only touch the PCR that we've currently reserved for the kernel
+ // EFI image. Touching others will break the ability to perform any kind of
+ // attestation using the TPM because it will make the log inconsistent.
+ lockErr = sbBlockPCRProtectionPolicies(tpm, []int{initramfsPCR})
}
}()
@@ -246,7 +255,7 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri
}
sealedKeyPath := filepath.Join(encryptionKeyDir, name+".sealed-key")
- return unlockEncryptedPartitionWithSealedKey(tpm, mapperName, encdev, sealedKeyPath, "", lockKeysOnFinish), true
+ return unlockEncryptedPartitionWithSealedKey(tpm, mapperName, encdev, sealedKeyPath, ""), true
}()
if err != nil {
return "", false, err
@@ -272,8 +281,9 @@ func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir stri
// unlockEncryptedPartitionWithRecoveryKey prompts for the recovery key and use
// it to open an encrypted device.
func unlockEncryptedPartitionWithRecoveryKey(name, device string) error {
- options := sb.ActivateWithRecoveryKeyOptions{
- Tries: 3,
+ options := sb.ActivateVolumeOptions{
+ RecoveryKeyTries: 3,
+ KeyringPrefix: keyringPrefix,
}
if err := sbActivateVolumeWithRecoveryKey(name, device, nil, &options); err != nil {
@@ -286,11 +296,11 @@ func unlockEncryptedPartitionWithRecoveryKey(name, device string) error {
// unlockEncryptedPartitionWithSealedKey unseals the keyfile and opens an encrypted
// device. If activation with the sealed key fails, this function will attempt to
// activate it with the fallback recovery key instead.
-func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string, lock bool) error {
- options := sb.ActivateWithTPMSealedKeyOptions{
- PINTries: 1,
- RecoveryKeyTries: 3,
- LockSealedKeyAccess: lock,
+func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string) error {
+ options := sb.ActivateVolumeOptions{
+ PassphraseTries: 1,
+ RecoveryKeyTries: 3,
+ KeyringPrefix: keyringPrefix,
}
// XXX: pinfile is currently not used
@@ -338,10 +348,19 @@ func SealKey(key EncryptionKey, params *SealKeyParams) error {
// Seal key to the TPM
creationParams := sb.KeyCreationParams{
- PCRProfile: pcrProfile,
- PINHandle: pinHandle,
+ PCRProfile: pcrProfile,
+ PCRPolicyCounterHandle: policyCounterHandle,
}
- return sbSealKeyToTPM(tpm, key[:], params.KeyFile, params.TPMPolicyUpdateDataFile, &creationParams)
+
+ authKey, err := sbSealKeyToTPM(tpm, key[:], params.KeyFile, &creationParams)
+ if err != nil {
+ return err
+ }
+ if err := osutil.AtomicWriteFile(params.TPMPolicyAuthKeyFile, authKey, 0600, 0); err != nil {
+ return fmt.Errorf("cannot write the policy auth key file: %v", err)
+ }
+
+ return nil
}
// ResealKey updates the PCR protection policy for the sealed encryption key according to
@@ -366,7 +385,12 @@ func ResealKey(params *ResealKeyParams) error {
return err
}
- return sbUpdateKeyPCRProtectionPolicy(tpm, params.KeyFile, params.TPMPolicyUpdateDataFile, pcrProfile)
+ authKey, err := ioutil.ReadFile(params.TPMPolicyAuthKeyFile)
+ if err != nil {
+ return fmt.Errorf("cannot read the policy auth key file: %v", err)
+ }
+
+ return sbUpdateKeyPCRProtectionPolicy(tpm, params.KeyFile, authKey, pcrProfile)
}
func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtectionProfile, error) {
@@ -408,7 +432,7 @@ func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtec
if len(mp.KernelCmdlines) != 0 {
systemdStubParams := sb.SystemdEFIStubProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
- PCRIndex: tpmPCR,
+ PCRIndex: initramfsPCR,
KernelCmdlines: mp.KernelCmdlines,
}
if err := sbAddSystemdEFIStubProfile(modelProfile, &systemdStubParams); err != nil {
@@ -420,7 +444,7 @@ func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtec
if mp.Model != nil {
snapModelParams := sb.SnapModelProfileParams{
PCRAlgorithm: tpm2.HashAlgorithmSHA256,
- PCRIndex: tpmPCR,
+ PCRIndex: initramfsPCR,
Models: []sb.SnapModel{mp.Model},
}
if err := sbAddSnapModelProfile(modelProfile, &snapModelParams); err != nil {
@@ -458,13 +482,17 @@ func tpmProvision(tpm *sb.TPMConnection, lockoutAuthFile string) error {
// TODO:UC20: ideally we should ask the firmware to clear the TPM and then reboot
// if the device has previously been provisioned, see
// https://godoc.org/github.com/snapcore/secboot#RequestTPMClearUsingPPI
- if err := sbProvisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil {
+ if err := provisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil {
logger.Noticef("TPM provisioning error: %v", err)
return fmt.Errorf("cannot provision TPM: %v", err)
}
return nil
}
+func provisionTPMImpl(tpm *sb.TPMConnection, mode sb.ProvisionMode, lockoutAuth []byte) error {
+ return tpm.EnsureProvisioned(mode, lockoutAuth)
+}
+
// buildLoadSequences builds EFI load image event trees from this package LoadChains
func buildLoadSequences(chains []*LoadChain) (loadseqs []*sb.EFIImageLoadEvent, err error) {
// this will build load event trees for the current
diff --git a/secboot/secboot_tpm_test.go b/secboot/secboot_tpm_test.go
index b3d6542b12..26263c8a13 100644
--- a/secboot/secboot_tpm_test.go
+++ b/secboot/secboot_tpm_test.go
@@ -37,6 +37,7 @@ import (
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/bootloader/efi"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/snap"
@@ -280,9 +281,9 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
device: "name",
disk: mockDiskWithEncDev,
}, {
- // activation works but lock fails (lock requested)
+ // activation works but PCR policy block fails (lock requested)
tpmEnabled: true, hasEncdev: true, lockRequest: true, activated: true,
- err: "cannot lock access to sealed keys: lock failed",
+ err: "cannot lock access to sealed keys: block failed",
device: "name",
disk: mockDiskWithEncDev,
}, {
@@ -308,7 +309,7 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
}, {
// activation works but lock fails, without encrypted device (lock requested)
tpmEnabled: true, lockRequest: true, activated: true,
- err: "cannot lock access to sealed keys: lock failed",
+ err: "cannot lock access to sealed keys: block failed",
disk: mockDiskWithUnencDev,
}, {
// happy case without encrypted device
@@ -393,14 +394,15 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
})
defer restore()
- n := 0
- restore = secboot.MockSbLockAccessToSealedKeys(func(tpm *sb.TPMConnection) error {
- n++
+ sbBlockPCRProtectionPolicesCalls := 0
+ restore = secboot.MockSbBlockPCRProtectionPolicies(func(tpm *sb.TPMConnection, pcrs []int) error {
+ sbBlockPCRProtectionPolicesCalls++
c.Assert(tpm, Equals, mockSbTPM)
+ c.Assert(pcrs, DeepEquals, []int{12})
if tc.lockOk {
return nil
}
- return errors.New("lock failed")
+ return errors.New("block failed")
})
defer restore()
@@ -412,14 +414,14 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
devicePath := filepath.Join("/dev/disk/by-partuuid", partuuid)
restore = secboot.MockSbActivateVolumeWithTPMSealedKey(func(tpm *sb.TPMConnection, volumeName, sourceDevicePath,
- keyPath string, pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error) {
+ keyPath string, pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error) {
c.Assert(volumeName, Equals, "name-"+randomUUID)
c.Assert(sourceDevicePath, Equals, devicePath)
c.Assert(keyPath, Equals, filepath.Join("encrypt-key-dir", "name.sealed-key"))
- c.Assert(*options, DeepEquals, sb.ActivateWithTPMSealedKeyOptions{
- PINTries: 1,
- RecoveryKeyTries: 3,
- LockSealedKeyAccess: tc.lockRequest,
+ c.Assert(*options, DeepEquals, sb.ActivateVolumeOptions{
+ PassphraseTries: 1,
+ RecoveryKeyTries: 3,
+ KeyringPrefix: "snapd",
})
if !tc.activated {
return false, errors.New("activation error")
@@ -429,7 +431,7 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
defer restore()
restore = secboot.MockSbActivateVolumeWithRecoveryKey(func(name, device string, keyReader io.Reader,
- options *sb.ActivateWithRecoveryKeyOptions) error {
+ options *sb.ActivateVolumeOptions) error {
return tc.rkErr
})
defer restore()
@@ -446,14 +448,14 @@ func (s *secbootSuite) TestUnlockIfEncrypted(c *C) {
} else {
c.Assert(err, ErrorMatches, tc.err)
}
- // LockAccessToSealedKeys should be called whenever there is a TPM device
+ // BlockPCRProtectionPolicies should be called whenever there is a TPM device
// detected, regardless of whether secure boot is enabled or there is an
// encrypted volume to unlock. If we have multiple encrypted volumes, we
// should call it after the last one is unlocked.
if tc.tpmErr == nil && tc.lockRequest {
- c.Assert(n, Equals, 1)
+ c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1)
} else {
- c.Assert(n, Equals, 0)
+ c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0)
}
}
}
@@ -593,9 +595,9 @@ func (s *secbootSuite) TestSealKey(c *C) {
Model: &asserts.Model{},
},
},
- KeyFile: "keyfile",
- TPMPolicyUpdateDataFile: "policy-update-data-file",
- TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"),
+ KeyFile: "keyfile",
+ TPMPolicyAuthKeyFile: filepath.Join(tmpDir, "policy-auth-key-file"),
+ TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"),
}
myKey := secboot.EncryptionKey{}
@@ -752,7 +754,7 @@ func (s *secbootSuite) TestSealKey(c *C) {
// mock provisioning
provisioningCalls := 0
- restore = secboot.MockSbProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error {
+ restore = secboot.MockProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error {
provisioningCalls++
c.Assert(t, Equals, tpm)
c.Assert(mode, Equals, sb.ProvisionModeFull)
@@ -763,14 +765,13 @@ func (s *secbootSuite) TestSealKey(c *C) {
// mock sealing
sealCalls := 0
- restore = secboot.MockSbSealKeyToTPM(func(t *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error {
+ restore = secboot.MockSbSealKeyToTPM(func(t *sb.TPMConnection, key []byte, keyPath string, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error) {
sealCalls++
c.Assert(t, Equals, tpm)
c.Assert(key, DeepEquals, myKey[:])
c.Assert(keyPath, Equals, myParams.KeyFile)
- c.Assert(policyUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile)
- c.Assert(params.PINHandle, Equals, tpm2.Handle(0x01880000))
- return tc.sealErr
+ c.Assert(params.PCRPolicyCounterHandle, Equals, tpm2.Handle(0x01880001))
+ return sb.TPMPolicyAuthKey{}, tc.sealErr
})
defer restore()
@@ -786,6 +787,7 @@ func (s *secbootSuite) TestSealKey(c *C) {
c.Assert(addEFISbPolicyCalls, Equals, 2)
c.Assert(addSystemdEfiStubCalls, Equals, 2)
c.Assert(addSnapModelCalls, Equals, 2)
+ c.Assert(osutil.FileExists(myParams.TPMPolicyAuthKeyFile), Equals, true)
} else {
c.Assert(err, ErrorMatches, tc.expectedErr)
}
@@ -821,6 +823,11 @@ func (s *secbootSuite) TestResealKey(c *C) {
{tpmEnabled: true, resealErr: mockErr, resealCalls: 1, expectedErr: "some error"},
{tpmEnabled: true, resealCalls: 1, expectedErr: ""},
} {
+ mockTPMPolicyAuthKey := []byte{1, 3, 3, 7}
+ mockTPMPolicyAuthKeyFile := filepath.Join(c.MkDir(), "policy-auth-key-file")
+ err := ioutil.WriteFile(mockTPMPolicyAuthKeyFile, mockTPMPolicyAuthKey, 0600)
+ c.Assert(err, IsNil)
+
mockEFI := bootloader.NewBootFile("", filepath.Join(c.MkDir(), "file.efi"), bootloader.RoleRecovery)
if !tc.missingFile {
err := ioutil.WriteFile(mockEFI.Path, nil, 0644)
@@ -835,8 +842,8 @@ func (s *secbootSuite) TestResealKey(c *C) {
Model: &asserts.Model{},
},
},
- KeyFile: "keyfile",
- TPMPolicyUpdateDataFile: "policy-update-data-file",
+ KeyFile: "keyfile",
+ TPMPolicyAuthKeyFile: mockTPMPolicyAuthKeyFile,
}
sequences := []*sb.EFIImageLoadEvent{
@@ -905,17 +912,17 @@ func (s *secbootSuite) TestResealKey(c *C) {
// mock PCR protection policy update
resealCalls := 0
- restore = secboot.MockSbUpdateKeyPCRProtectionPolicy(func(t *sb.TPMConnection, keyPath, polUpdatePath string, profile *sb.PCRProtectionProfile) error {
+ restore = secboot.MockSbUpdateKeyPCRProtectionPolicy(func(t *sb.TPMConnection, keyPath string, authKey sb.TPMPolicyAuthKey, profile *sb.PCRProtectionProfile) error {
resealCalls++
c.Assert(t, Equals, tpm)
c.Assert(keyPath, Equals, myParams.KeyFile)
- c.Assert(polUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile)
+ c.Assert(authKey, DeepEquals, sb.TPMPolicyAuthKey(mockTPMPolicyAuthKey))
c.Assert(profile, Equals, pcrProfile)
return tc.resealErr
})
defer restore()
- err := secboot.ResealKey(myParams)
+ err = secboot.ResealKey(myParams)
if tc.expectedErr == "" {
c.Assert(err, IsNil)
c.Assert(addEFISbPolicyCalls, Equals, 1)
@@ -931,9 +938,9 @@ func (s *secbootSuite) TestResealKey(c *C) {
func (s *secbootSuite) TestSealKeyNoModelParams(c *C) {
myKey := secboot.EncryptionKey{}
myParams := secboot.SealKeyParams{
- KeyFile: "keyfile",
- TPMPolicyUpdateDataFile: "policy-update-data-file",
- TPMLockoutAuthFile: "lockout-auth-file",
+ KeyFile: "keyfile",
+ TPMPolicyAuthKeyFile: "policy-auth-key-file",
+ TPMLockoutAuthFile: "lockout-auth-file",
}
err := secboot.SealKey(myKey, &myParams)
diff --git a/tests/nested/core20/gadget-reseal/task.yaml b/tests/nested/core20/gadget-reseal/task.yaml
index 727a25f1a3..41259d960c 100644
--- a/tests/nested/core20/gadget-reseal/task.yaml
+++ b/tests/nested/core20/gadget-reseal/task.yaml
@@ -46,6 +46,9 @@ execute: |
nested_wait_for_reboot "${boot_id}"
nested_exec sudo snap watch "${REMOTE_CHG_ID}"
+ # sanity check that the gadget asset was changed
+ nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /run/mnt/ubuntu-boot/EFI/boot/grubx64.efi
+
# the gadget has changed, we should see resealing
SEALED_KEY_MTIME_3="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)"
test "$SEALED_KEY_MTIME_3" -gt "$SEALED_KEY_MTIME_2"
diff --git a/tests/nested/core20/kernel-reseal/task.yaml b/tests/nested/core20/kernel-reseal/task.yaml
index fbc722cefd..883a7cbee7 100644
--- a/tests/nested/core20/kernel-reseal/task.yaml
+++ b/tests/nested/core20/kernel-reseal/task.yaml
@@ -6,12 +6,17 @@ prepare: |
# shellcheck source=tests/lib/nested.sh
. "$TESTSLIB/nested.sh"
- snap download pc-kernel --channel=20/edge --basename=pc-kernel
- unsquashfs -d pc-kernel pc-kernel.snap
+ # we cannot use the kernel from store as it may have a version of
+ # snap-bootstrap that will not be able to unseal the keys and unlock the
+ # encrypted volumes, instead use the kernel we repacked when building the UC20
+ # image
+ KERNEL_SNAP="$(ls "$NESTED_ASSETS_DIR"/pc-kernel_*.snap)"
+ unsquashfs -d pc-kernel "$KERNEL_SNAP"
# ensure we really have the header we expect
grep -q -a "This program cannot be run in DOS mode" pc-kernel/kernel.efi
# modify the kernel so that the hash changes
- sed -i 's/This program cannot be run in DOS mode/This program cannot be run in D0S mode/' pc-kernel/kernel.efi
+ sed -i 's/This program cannot be run in DOS mode/This program cannot be run in XXX mode/' pc-kernel/kernel.efi
+ grep -q -a "This program cannot be run in XXX mode" pc-kernel/kernel.efi
KEY_NAME=$(nested_get_snakeoil_key)
SNAKEOIL_KEY="$PWD/$KEY_NAME.key"
@@ -36,6 +41,9 @@ execute: |
nested_wait_for_reboot "${boot_id}"
nested_exec sudo snap watch "${REMOTE_CHG_ID}"
+ # sanity check that we are using the right kernel
+ nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /boot/grub/kernel.efi
+
# ensure ubuntu-data.sealed-key mtime is newer
SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)"
test "$SEALED_KEY_MTIME_2" -gt "$SEALED_KEY_MTIME_1"
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 5c4c82d231..4644dd521c 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -25,10 +25,10 @@
"revisionTime": "2020-08-24T11:54:14Z"
},
{
- "checksumSHA1": "eDjzake0GpHm9kfTH7FMUWX8zVA=",
- "path": "github.com/chrisccoulson/tcglog-parser",
- "revision": "7b0f085a85398d368e10382a21a44ec2226c35b3",
- "revisionTime": "2020-02-28T14:36:39Z"
+ "checksumSHA1": "QGwWyf2f/5+FacNj2iKpjp8N5hg=",
+ "path": "github.com/canonical/tcglog-parser",
+ "revision": "12a3a7bcf5a14486e838fea211e8d4aa280e9671",
+ "revisionTime": "2020-09-08T16:50:21Z"
},
{
"checksumSHA1": "zg16zjZTQ9R89+UOLmEZxHgxDtM=",
@@ -116,40 +116,40 @@
"revisionTime": "2017-09-28T14:21:59Z"
},
{
- "checksumSHA1": "bNQROczU0gF+BeQHApftqWNUMe8=",
+ "checksumSHA1": "sKSczCVVhI1zTfY4zQBPYs/34vQ=",
"path": "github.com/snapcore/secboot",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "c7jHLQSWFWbymTcFWZMQH0C5Wik=",
"path": "github.com/snapcore/secboot/internal/efi",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "loFEiH6evGaDnDSlQgk3ugemkcU=",
"path": "github.com/snapcore/secboot/internal/pe1.14",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "kDay47kq9OgDplpkrYw0/a8Z+YY=",
"path": "github.com/snapcore/secboot/internal/tcg",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "PRS8ACUu14shrvAgb747Izc25ns=",
"path": "github.com/snapcore/secboot/internal/tcti",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "TnfofdyojXYWOwdWCKMY5RCeI7s=",
"path": "github.com/snapcore/secboot/internal/truststore",
- "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530",
- "revisionTime": "2020-09-10T15:49:09Z"
+ "revision": "0ebc6c7e1dc3c184baf8c4a2b936afa99f988acb",
+ "revisionTime": "2020-10-15T16:53:38Z"
},
{
"checksumSHA1": "3AmEm18mKj8XxBuru/ix4OOpRkE=",
@@ -301,6 +301,13 @@
"path": "gopkg.in/yaml.v2",
"revision": "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb",
"revisionTime": "2018-03-26T05:07:29Z"
+ },
+ {
+ "checksumSHA1": "tZ9GNzrjTZEPDhoJEkouGPKRmZk=",
+ "origin": "github.com/pedronis/maze.io-x-crypto/afis",
+ "path": "maze.io/x/crypto/afis",
+ "revision": "9b94c9afe06676a7da39a608a48ed4e31f5d125f",
+ "revisionTime": "2019-01-31T09:06:03Z"
}
],
"rootPath": "github.com/snapcore/snapd"