github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_systems_test.go (about)

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