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