github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/daemon/api_systems_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2020-2021 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"os"
    29  	"path"
    30  	"path/filepath"
    31  	"strings"
    32  
    33  	"gopkg.in/check.v1"
    34  
    35  	"github.com/snapcore/snapd/asserts/assertstest"
    36  	"github.com/snapcore/snapd/boot"
    37  	"github.com/snapcore/snapd/bootloader"
    38  	"github.com/snapcore/snapd/bootloader/bootloadertest"
    39  	"github.com/snapcore/snapd/client"
    40  	"github.com/snapcore/snapd/daemon"
    41  	"github.com/snapcore/snapd/dirs"
    42  	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
    43  	"github.com/snapcore/snapd/overlord/devicestate"
    44  	"github.com/snapcore/snapd/overlord/hookstate"
    45  	"github.com/snapcore/snapd/overlord/state"
    46  	"github.com/snapcore/snapd/seed"
    47  	"github.com/snapcore/snapd/seed/seedtest"
    48  	"github.com/snapcore/snapd/snap"
    49  	"github.com/snapcore/snapd/snap/snaptest"
    50  	"github.com/snapcore/snapd/testutil"
    51  )
    52  
    53  var _ = check.Suite(&systemsSuite{})
    54  
    55  type systemsSuite struct {
    56  	apiBaseSuite
    57  }
    58  
    59  func (s *systemsSuite) SetUpTest(c *check.C) {
    60  	s.apiBaseSuite.SetUpTest(c)
    61  
    62  	s.expectRootAccess()
    63  }
    64  
    65  func (s *systemsSuite) mockSystemSeeds(c *check.C) (restore func()) {
    66  	// now create a minimal uc20 seed dir with snaps/assertions
    67  	seed20 := &seedtest.TestingSeed20{
    68  		SeedSnaps: seedtest.SeedSnaps{
    69  			StoreSigning: s.StoreSigning,
    70  			Brands:       s.Brands,
    71  		},
    72  		SeedDir: dirs.SnapSeedDir,
    73  	}
    74  
    75  	restore = seed.MockTrusted(seed20.StoreSigning.Trusted)
    76  
    77  	assertstest.AddMany(s.StoreSigning.Database, s.Brands.AccountsAndKeys("my-brand")...)
    78  	// add essential snaps
    79  	seed20.MakeAssertedSnap(c, "name: snapd\nversion: 1\ntype: snapd", nil, snap.R(1), "my-brand", s.StoreSigning.Database)
    80  	seed20.MakeAssertedSnap(c, "name: pc\nversion: 1\ntype: gadget\nbase: core20", nil, snap.R(1), "my-brand", s.StoreSigning.Database)
    81  	seed20.MakeAssertedSnap(c, "name: pc-kernel\nversion: 1\ntype: kernel", nil, snap.R(1), "my-brand", s.StoreSigning.Database)
    82  	seed20.MakeAssertedSnap(c, "name: core20\nversion: 1\ntype: base", nil, snap.R(1), "my-brand", s.StoreSigning.Database)
    83  	seed20.MakeSeed(c, "20191119", "my-brand", "my-model", map[string]interface{}{
    84  		"display-name": "my fancy model",
    85  		"architecture": "amd64",
    86  		"base":         "core20",
    87  		"snaps": []interface{}{
    88  			map[string]interface{}{
    89  				"name":            "pc-kernel",
    90  				"id":              seed20.AssertedSnapID("pc-kernel"),
    91  				"type":            "kernel",
    92  				"default-channel": "20",
    93  			},
    94  			map[string]interface{}{
    95  				"name":            "pc",
    96  				"id":              seed20.AssertedSnapID("pc"),
    97  				"type":            "gadget",
    98  				"default-channel": "20",
    99  			}},
   100  	}, nil)
   101  	seed20.MakeSeed(c, "20200318", "my-brand", "my-model-2", map[string]interface{}{
   102  		"display-name": "same brand different model",
   103  		"architecture": "amd64",
   104  		"base":         "core20",
   105  		"snaps": []interface{}{
   106  			map[string]interface{}{
   107  				"name":            "pc-kernel",
   108  				"id":              seed20.AssertedSnapID("pc-kernel"),
   109  				"type":            "kernel",
   110  				"default-channel": "20",
   111  			},
   112  			map[string]interface{}{
   113  				"name":            "pc",
   114  				"id":              seed20.AssertedSnapID("pc"),
   115  				"type":            "gadget",
   116  				"default-channel": "20",
   117  			}},
   118  	}, nil)
   119  
   120  	return restore
   121  }
   122  
   123  func (s *systemsSuite) TestSystemsGetSome(c *check.C) {
   124  	m := boot.Modeenv{
   125  		Mode: "run",
   126  	}
   127  	err := m.WriteTo("")
   128  	c.Assert(err, check.IsNil)
   129  
   130  	d := s.daemonWithOverlordMockAndStore(c)
   131  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   132  	c.Assert(err, check.IsNil)
   133  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   134  	c.Assert(err, check.IsNil)
   135  	d.Overlord().AddManager(mgr)
   136  
   137  	st := d.Overlord().State()
   138  	st.Lock()
   139  	st.Set("seeded-systems", []map[string]interface{}{{
   140  		"system": "20200318", "model": "my-model-2", "brand-id": "my-brand",
   141  		"revision": 2, "timestamp": "2009-11-10T23:00:00Z",
   142  		"seed-time": "2009-11-10T23:00:00Z",
   143  	}})
   144  	st.Unlock()
   145  
   146  	s.expectAuthenticatedAccess()
   147  
   148  	restore := s.mockSystemSeeds(c)
   149  	defer restore()
   150  
   151  	req, err := http.NewRequest("GET", "/v2/systems", nil)
   152  	c.Assert(err, check.IsNil)
   153  	rsp := s.syncReq(c, req, nil)
   154  
   155  	c.Assert(rsp.Status, check.Equals, 200)
   156  	sys := rsp.Result.(*daemon.SystemsResponse)
   157  
   158  	c.Assert(sys, check.DeepEquals, &daemon.SystemsResponse{
   159  		Systems: []client.System{
   160  			{
   161  				Current: false,
   162  				Label:   "20191119",
   163  				Model: client.SystemModelData{
   164  					Model:       "my-model",
   165  					BrandID:     "my-brand",
   166  					DisplayName: "my fancy model",
   167  				},
   168  				Brand: snap.StoreAccount{
   169  					ID:          "my-brand",
   170  					Username:    "my-brand",
   171  					DisplayName: "My-brand",
   172  					Validation:  "unproven",
   173  				},
   174  				Actions: []client.SystemAction{
   175  					{Title: "Install", Mode: "install"},
   176  				},
   177  			}, {
   178  				Current: true,
   179  				Label:   "20200318",
   180  				Model: client.SystemModelData{
   181  					Model:       "my-model-2",
   182  					BrandID:     "my-brand",
   183  					DisplayName: "same brand different model",
   184  				},
   185  				Brand: snap.StoreAccount{
   186  					ID:          "my-brand",
   187  					Username:    "my-brand",
   188  					DisplayName: "My-brand",
   189  					Validation:  "unproven",
   190  				},
   191  				Actions: []client.SystemAction{
   192  					{Title: "Reinstall", Mode: "install"},
   193  					{Title: "Recover", Mode: "recover"},
   194  					{Title: "Run normally", Mode: "run"},
   195  				},
   196  			},
   197  		}})
   198  }
   199  
   200  func (s *systemsSuite) TestSystemsGetNone(c *check.C) {
   201  	m := boot.Modeenv{
   202  		Mode: "run",
   203  	}
   204  	err := m.WriteTo("")
   205  	c.Assert(err, check.IsNil)
   206  
   207  	// model assertion setup
   208  	d := s.daemonWithOverlordMockAndStore(c)
   209  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   210  	c.Assert(err, check.IsNil)
   211  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   212  	c.Assert(err, check.IsNil)
   213  	d.Overlord().AddManager(mgr)
   214  
   215  	s.expectAuthenticatedAccess()
   216  
   217  	// no system seeds
   218  	req, err := http.NewRequest("GET", "/v2/systems", nil)
   219  	c.Assert(err, check.IsNil)
   220  	rsp := s.syncReq(c, req, nil)
   221  
   222  	c.Assert(rsp.Status, check.Equals, 200)
   223  	sys := rsp.Result.(*daemon.SystemsResponse)
   224  
   225  	c.Assert(sys, check.DeepEquals, &daemon.SystemsResponse{})
   226  }
   227  
   228  func (s *systemsSuite) TestSystemActionRequestErrors(c *check.C) {
   229  	// modenev must be mocked before daemon is initialized
   230  	m := boot.Modeenv{
   231  		Mode: "run",
   232  	}
   233  	err := m.WriteTo("")
   234  	c.Assert(err, check.IsNil)
   235  
   236  	d := s.daemonWithOverlordMockAndStore(c)
   237  
   238  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   239  	c.Assert(err, check.IsNil)
   240  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   241  	c.Assert(err, check.IsNil)
   242  	d.Overlord().AddManager(mgr)
   243  
   244  	restore := s.mockSystemSeeds(c)
   245  	defer restore()
   246  
   247  	st := d.Overlord().State()
   248  
   249  	type table struct {
   250  		label, body, error string
   251  		status             int
   252  		unseeded           bool
   253  	}
   254  	tests := []table{
   255  		{
   256  			label:  "foobar",
   257  			body:   `"bogus"`,
   258  			error:  "cannot decode request body into system action:.*",
   259  			status: 400,
   260  		}, {
   261  			label:  "",
   262  			body:   `{"action":"do","mode":"install"}`,
   263  			error:  "system action requires the system label to be provided",
   264  			status: 400,
   265  		}, {
   266  			label:  "foobar",
   267  			body:   `{"action":"do"}`,
   268  			error:  "system action requires the mode to be provided",
   269  			status: 400,
   270  		}, {
   271  			label:  "foobar",
   272  			body:   `{"action":"nope","mode":"install"}`,
   273  			error:  `unsupported action "nope"`,
   274  			status: 400,
   275  		}, {
   276  			label:  "foobar",
   277  			body:   `{"action":"do","mode":"install"}`,
   278  			error:  `requested seed system "foobar" does not exist`,
   279  			status: 404,
   280  		}, {
   281  			// valid system label but incorrect action
   282  			label:  "20191119",
   283  			body:   `{"action":"do","mode":"foobar"}`,
   284  			error:  `requested action is not supported by system "20191119"`,
   285  			status: 400,
   286  		}, {
   287  			// valid label and action, but seeding is not complete yet
   288  			label:    "20191119",
   289  			body:     `{"action":"do","mode":"install"}`,
   290  			error:    `cannot request system action, system is seeding`,
   291  			status:   500,
   292  			unseeded: true,
   293  		},
   294  	}
   295  	for _, tc := range tests {
   296  		st.Lock()
   297  		if tc.unseeded {
   298  			st.Set("seeded", nil)
   299  			m := boot.Modeenv{
   300  				Mode:           "run",
   301  				RecoverySystem: tc.label,
   302  			}
   303  			err := m.WriteTo("")
   304  			c.Assert(err, check.IsNil)
   305  		} else {
   306  			st.Set("seeded", true)
   307  		}
   308  		st.Unlock()
   309  		c.Logf("tc: %#v", tc)
   310  		req, err := http.NewRequest("POST", path.Join("/v2/systems", tc.label), strings.NewReader(tc.body))
   311  		c.Assert(err, check.IsNil)
   312  		rspe := s.errorReq(c, req, nil)
   313  		c.Check(rspe.Status, check.Equals, tc.status)
   314  		c.Check(rspe.Message, check.Matches, tc.error)
   315  	}
   316  }
   317  
   318  func (s *systemsSuite) TestSystemActionRequestWithSeeded(c *check.C) {
   319  	bt := bootloadertest.Mock("mock", c.MkDir())
   320  	bootloader.Force(bt)
   321  	defer func() { bootloader.Force(nil) }()
   322  
   323  	cmd := testutil.MockCommand(c, "shutdown", "")
   324  	defer cmd.Restore()
   325  
   326  	restore := s.mockSystemSeeds(c)
   327  	defer restore()
   328  
   329  	model := s.Brands.Model("my-brand", "pc", map[string]interface{}{
   330  		"architecture": "amd64",
   331  		// UC20
   332  		"grade": "dangerous",
   333  		"base":  "core20",
   334  		"snaps": []interface{}{
   335  			map[string]interface{}{
   336  				"name":            "pc-kernel",
   337  				"id":              snaptest.AssertedSnapID("pc-kernel"),
   338  				"type":            "kernel",
   339  				"default-channel": "20",
   340  			},
   341  			map[string]interface{}{
   342  				"name":            "pc",
   343  				"id":              snaptest.AssertedSnapID("pc"),
   344  				"type":            "gadget",
   345  				"default-channel": "20",
   346  			},
   347  		},
   348  	})
   349  
   350  	currentSystem := []map[string]interface{}{{
   351  		"system": "20191119", "model": "my-model", "brand-id": "my-brand",
   352  		"revision": 2, "timestamp": "2009-11-10T23:00:00Z",
   353  		"seed-time": "2009-11-10T23:00:00Z",
   354  	}}
   355  
   356  	tt := []struct {
   357  		currentMode    string
   358  		actionMode     string
   359  		expUnsupported bool
   360  		expRestart     bool
   361  		comment        string
   362  	}{
   363  		{
   364  			// from run mode -> install mode works to reinstall the system
   365  			currentMode: "run",
   366  			actionMode:  "install",
   367  			expRestart:  true,
   368  			comment:     "run mode to install mode",
   369  		},
   370  		{
   371  			// from run mode -> recover mode works to recover the system
   372  			currentMode: "run",
   373  			actionMode:  "recover",
   374  			expRestart:  true,
   375  			comment:     "run mode to recover mode",
   376  		},
   377  		{
   378  			// from run mode -> run mode is no-op
   379  			currentMode: "run",
   380  			actionMode:  "run",
   381  			comment:     "run mode to run mode",
   382  		},
   383  		{
   384  			// from recover mode -> run mode works to stop recovering and "restore" the system to normal
   385  			currentMode: "recover",
   386  			actionMode:  "run",
   387  			expRestart:  true,
   388  			comment:     "recover mode to run mode",
   389  		},
   390  		{
   391  			// from recover mode -> install mode works to stop recovering and reinstall the system if all is lost
   392  			currentMode: "recover",
   393  			actionMode:  "install",
   394  			expRestart:  true,
   395  			comment:     "recover mode to install mode",
   396  		},
   397  		{
   398  			// from recover mode -> recover mode is no-op
   399  			currentMode:    "recover",
   400  			actionMode:     "recover",
   401  			expUnsupported: true,
   402  			comment:        "recover mode to recover mode",
   403  		},
   404  		{
   405  			// from install mode -> install mode is no-no
   406  			currentMode:    "install",
   407  			actionMode:     "install",
   408  			expUnsupported: true,
   409  			comment:        "install mode to install mode not supported",
   410  		},
   411  		{
   412  			// from install mode -> run mode is no-no
   413  			currentMode:    "install",
   414  			actionMode:     "run",
   415  			expUnsupported: true,
   416  			comment:        "install mode to run mode not supported",
   417  		},
   418  		{
   419  			// from install mode -> recover mode is no-no
   420  			currentMode:    "install",
   421  			actionMode:     "recover",
   422  			expUnsupported: true,
   423  			comment:        "install mode to recover mode not supported",
   424  		},
   425  	}
   426  
   427  	for _, tc := range tt {
   428  		c.Logf("tc: %v", tc.comment)
   429  		// daemon setup - need to do this per-test because we need to re-read
   430  		// the modeenv during devicemgr startup
   431  		m := boot.Modeenv{
   432  			Mode: tc.currentMode,
   433  		}
   434  		if tc.currentMode != "run" {
   435  			m.RecoverySystem = "20191119"
   436  		}
   437  		err := m.WriteTo("")
   438  		c.Assert(err, check.IsNil)
   439  		d := s.daemon(c)
   440  		st := d.Overlord().State()
   441  		st.Lock()
   442  		// devicemgr needs boot id to request a reboot
   443  		st.VerifyReboot("boot-id-0")
   444  		// device model
   445  		assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""))
   446  		assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...)
   447  		s.mockModel(c, st, model)
   448  		if tc.currentMode == "run" {
   449  			// only set in run mode
   450  			st.Set("seeded-systems", currentSystem)
   451  		}
   452  		// the seeding is done
   453  		st.Set("seeded", true)
   454  		st.Unlock()
   455  
   456  		body := map[string]string{
   457  			"action": "do",
   458  			"mode":   tc.actionMode,
   459  		}
   460  		b, err := json.Marshal(body)
   461  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   462  		buf := bytes.NewBuffer(b)
   463  		req, err := http.NewRequest("POST", "/v2/systems/20191119", buf)
   464  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   465  		// as root
   466  		s.asRootAuth(req)
   467  		rec := httptest.NewRecorder()
   468  		s.serveHTTP(c, rec, req)
   469  		if tc.expUnsupported {
   470  			c.Check(rec.Code, check.Equals, 400, check.Commentf(tc.comment))
   471  		} else {
   472  			c.Check(rec.Code, check.Equals, 200, check.Commentf(tc.comment))
   473  		}
   474  
   475  		var rspBody map[string]interface{}
   476  		err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   477  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   478  
   479  		var expResp map[string]interface{}
   480  		if tc.expUnsupported {
   481  			expResp = map[string]interface{}{
   482  				"result": map[string]interface{}{
   483  					"message": fmt.Sprintf("requested action is not supported by system %q", "20191119"),
   484  				},
   485  				"status":      "Bad Request",
   486  				"status-code": 400.0,
   487  				"type":        "error",
   488  			}
   489  		} else {
   490  			expResp = map[string]interface{}{
   491  				"result":      nil,
   492  				"status":      "OK",
   493  				"status-code": 200.0,
   494  				"type":        "sync",
   495  			}
   496  			if tc.expRestart {
   497  				expResp["maintenance"] = map[string]interface{}{
   498  					"kind":    "system-restart",
   499  					"message": "system is restarting",
   500  					"value": map[string]interface{}{
   501  						"op": "reboot",
   502  					},
   503  				}
   504  
   505  				// daemon is not started, only check whether reboot was scheduled as expected
   506  
   507  				// reboot flag
   508  				c.Check(d.RequestedRestart(), check.Equals, state.RestartSystemNow, check.Commentf(tc.comment))
   509  				// slow reboot schedule
   510  				c.Check(cmd.Calls(), check.DeepEquals, [][]string{
   511  					{"shutdown", "-r", "+10", "reboot scheduled to update the system"},
   512  				},
   513  					check.Commentf(tc.comment),
   514  				)
   515  			}
   516  		}
   517  
   518  		c.Assert(rspBody, check.DeepEquals, expResp, check.Commentf(tc.comment))
   519  
   520  		cmd.ForgetCalls()
   521  		s.resetDaemon()
   522  	}
   523  
   524  }
   525  
   526  func (s *systemsSuite) TestSystemActionBrokenSeed(c *check.C) {
   527  	m := boot.Modeenv{
   528  		Mode: "run",
   529  	}
   530  	err := m.WriteTo("")
   531  	c.Assert(err, check.IsNil)
   532  
   533  	d := s.daemonWithOverlordMockAndStore(c)
   534  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   535  	c.Assert(err, check.IsNil)
   536  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   537  	c.Assert(err, check.IsNil)
   538  	d.Overlord().AddManager(mgr)
   539  
   540  	// the seeding is done
   541  	st := d.Overlord().State()
   542  	st.Lock()
   543  	st.Set("seeded", true)
   544  	st.Unlock()
   545  
   546  	restore := s.mockSystemSeeds(c)
   547  	defer restore()
   548  
   549  	err = os.Remove(filepath.Join(dirs.SnapSeedDir, "systems", "20191119", "model"))
   550  	c.Assert(err, check.IsNil)
   551  
   552  	body := `{"action":"do","title":"reinstall","mode":"install"}`
   553  	req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body))
   554  	c.Assert(err, check.IsNil)
   555  	rspe := s.errorReq(c, req, nil)
   556  	c.Check(rspe.Status, check.Equals, 500)
   557  	c.Check(rspe.Message, check.Matches, `cannot load seed system: cannot load assertions: .*`)
   558  }
   559  
   560  func (s *systemsSuite) TestSystemActionNonRoot(c *check.C) {
   561  	d := s.daemonWithOverlordMockAndStore(c)
   562  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   563  	c.Assert(err, check.IsNil)
   564  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   565  	c.Assert(err, check.IsNil)
   566  	d.Overlord().AddManager(mgr)
   567  
   568  	body := `{"action":"do","title":"reinstall","mode":"install"}`
   569  
   570  	// pretend to be a simple user
   571  	req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body))
   572  	c.Assert(err, check.IsNil)
   573  	// non root
   574  	s.asUserAuth(c, req)
   575  
   576  	rec := httptest.NewRecorder()
   577  	s.serveHTTP(c, rec, req)
   578  	c.Assert(rec.Code, check.Equals, 403)
   579  
   580  	var rspBody map[string]interface{}
   581  	err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   582  	c.Check(err, check.IsNil)
   583  	c.Check(rspBody, check.DeepEquals, map[string]interface{}{
   584  		"result": map[string]interface{}{
   585  			"message": "access denied",
   586  			"kind":    "login-required",
   587  		},
   588  		"status":      "Forbidden",
   589  		"status-code": 403.0,
   590  		"type":        "error",
   591  	})
   592  }
   593  
   594  func (s *systemsSuite) TestSystemRebootNeedsRoot(c *check.C) {
   595  	s.daemon(c)
   596  
   597  	restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   598  		c.Fatalf("request reboot should not get called")
   599  		return nil
   600  	})
   601  	defer restore()
   602  
   603  	body := `{"action":"reboot"}`
   604  	url := "/v2/systems"
   605  	req, err := http.NewRequest("POST", url, strings.NewReader(body))
   606  	c.Assert(err, check.IsNil)
   607  	// non root
   608  	s.asUserAuth(c, req)
   609  
   610  	rec := httptest.NewRecorder()
   611  	s.serveHTTP(c, rec, req)
   612  	c.Check(rec.Code, check.Equals, 403)
   613  }
   614  
   615  func (s *systemsSuite) TestSystemRebootHappy(c *check.C) {
   616  	s.daemon(c)
   617  
   618  	for _, tc := range []struct {
   619  		systemLabel, mode string
   620  	}{
   621  		{"", ""},
   622  		{"20200101", ""},
   623  		{"", "run"},
   624  		{"", "recover"},
   625  		{"20200101", "run"},
   626  		{"20200101", "recover"},
   627  	} {
   628  		called := 0
   629  		restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   630  			called++
   631  			c.Check(dm, check.NotNil)
   632  			c.Check(systemLabel, check.Equals, tc.systemLabel)
   633  			c.Check(mode, check.Equals, tc.mode)
   634  			return nil
   635  		})
   636  		defer restore()
   637  
   638  		body := fmt.Sprintf(`{"action":"reboot", "mode":"%s"}`, tc.mode)
   639  		url := "/v2/systems"
   640  		if tc.systemLabel != "" {
   641  			url += "/" + tc.systemLabel
   642  		}
   643  		req, err := http.NewRequest("POST", url, strings.NewReader(body))
   644  		c.Assert(err, check.IsNil)
   645  		s.asRootAuth(req)
   646  
   647  		rec := httptest.NewRecorder()
   648  		s.serveHTTP(c, rec, req)
   649  		c.Check(rec.Code, check.Equals, 200)
   650  		c.Check(called, check.Equals, 1)
   651  	}
   652  }
   653  
   654  func (s *systemsSuite) TestSystemRebootUnhappy(c *check.C) {
   655  	s.daemon(c)
   656  
   657  	for _, tc := range []struct {
   658  		rebootErr        error
   659  		expectedHttpCode int
   660  		expectedErr      string
   661  	}{
   662  		{fmt.Errorf("boom"), 500, "boom"},
   663  		{os.ErrNotExist, 404, `requested seed system "" does not exist`},
   664  		{devicestate.ErrUnsupportedAction, 400, `requested action is not supported by system ""`},
   665  	} {
   666  		called := 0
   667  		restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   668  			called++
   669  			return tc.rebootErr
   670  		})
   671  		defer restore()
   672  
   673  		body := `{"action":"reboot"}`
   674  		url := "/v2/systems"
   675  		req, err := http.NewRequest("POST", url, strings.NewReader(body))
   676  		c.Assert(err, check.IsNil)
   677  		s.asRootAuth(req)
   678  
   679  		rec := httptest.NewRecorder()
   680  		s.serveHTTP(c, rec, req)
   681  		c.Check(rec.Code, check.Equals, tc.expectedHttpCode)
   682  		c.Check(called, check.Equals, 1)
   683  
   684  		var rspBody map[string]interface{}
   685  		err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   686  		c.Check(err, check.IsNil)
   687  		c.Check(rspBody["status-code"], check.Equals, float64(tc.expectedHttpCode))
   688  		result := rspBody["result"].(map[string]interface{})
   689  		c.Check(result["message"], check.Equals, tc.expectedErr)
   690  	}
   691  }