github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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.syncReq(c, req, nil)
   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.syncReq(c, req, nil)
   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.errorReq(c, req, nil)
   303  		c.Check(rsp.Status, check.Equals, tc.status)
   304  		c.Check(rsp.ErrorResult().Message, check.Matches, tc.error)
   305  	}
   306  }
   307  
   308  func (s *systemsSuite) TestSystemActionRequestWithSeeded(c *check.C) {
   309  	bt := bootloadertest.Mock("mock", c.MkDir())
   310  	bootloader.Force(bt)
   311  	defer func() { bootloader.Force(nil) }()
   312  
   313  	cmd := testutil.MockCommand(c, "shutdown", "")
   314  	defer cmd.Restore()
   315  
   316  	restore := s.mockSystemSeeds(c)
   317  	defer restore()
   318  
   319  	model := s.Brands.Model("my-brand", "pc", map[string]interface{}{
   320  		"architecture": "amd64",
   321  		// UC20
   322  		"grade": "dangerous",
   323  		"base":  "core20",
   324  		"snaps": []interface{}{
   325  			map[string]interface{}{
   326  				"name":            "pc-kernel",
   327  				"id":              snaptest.AssertedSnapID("oc-kernel"),
   328  				"type":            "kernel",
   329  				"default-channel": "20",
   330  			},
   331  			map[string]interface{}{
   332  				"name":            "pc",
   333  				"id":              snaptest.AssertedSnapID("pc"),
   334  				"type":            "gadget",
   335  				"default-channel": "20",
   336  			},
   337  		},
   338  	})
   339  
   340  	currentSystem := []map[string]interface{}{{
   341  		"system": "20191119", "model": "my-model", "brand-id": "my-brand",
   342  		"revision": 2, "timestamp": "2009-11-10T23:00:00Z",
   343  		"seed-time": "2009-11-10T23:00:00Z",
   344  	}}
   345  
   346  	tt := []struct {
   347  		currentMode    string
   348  		actionMode     string
   349  		expUnsupported bool
   350  		expRestart     bool
   351  		comment        string
   352  	}{
   353  		{
   354  			// from run mode -> install mode works to reinstall the system
   355  			currentMode: "run",
   356  			actionMode:  "install",
   357  			expRestart:  true,
   358  			comment:     "run mode to install mode",
   359  		},
   360  		{
   361  			// from run mode -> recover mode works to recover the system
   362  			currentMode: "run",
   363  			actionMode:  "recover",
   364  			expRestart:  true,
   365  			comment:     "run mode to recover mode",
   366  		},
   367  		{
   368  			// from run mode -> run mode is no-op
   369  			currentMode: "run",
   370  			actionMode:  "run",
   371  			comment:     "run mode to run mode",
   372  		},
   373  		{
   374  			// from recover mode -> run mode works to stop recovering and "restore" the system to normal
   375  			currentMode: "recover",
   376  			actionMode:  "run",
   377  			expRestart:  true,
   378  			comment:     "recover mode to run mode",
   379  		},
   380  		{
   381  			// from recover mode -> install mode works to stop recovering and reinstall the system if all is lost
   382  			currentMode: "recover",
   383  			actionMode:  "install",
   384  			expRestart:  true,
   385  			comment:     "recover mode to install mode",
   386  		},
   387  		{
   388  			// from recover mode -> recover mode is no-op
   389  			currentMode:    "recover",
   390  			actionMode:     "recover",
   391  			expUnsupported: true,
   392  			comment:        "recover mode to recover mode",
   393  		},
   394  		{
   395  			// from install mode -> install mode is no-no
   396  			currentMode:    "install",
   397  			actionMode:     "install",
   398  			expUnsupported: true,
   399  			comment:        "install mode to install mode not supported",
   400  		},
   401  		{
   402  			// from install mode -> run mode is no-no
   403  			currentMode:    "install",
   404  			actionMode:     "run",
   405  			expUnsupported: true,
   406  			comment:        "install mode to run mode not supported",
   407  		},
   408  		{
   409  			// from install mode -> recover mode is no-no
   410  			currentMode:    "install",
   411  			actionMode:     "recover",
   412  			expUnsupported: true,
   413  			comment:        "install mode to recover mode not supported",
   414  		},
   415  	}
   416  
   417  	for _, tc := range tt {
   418  		c.Logf("tc: %v", tc.comment)
   419  		// daemon setup - need to do this per-test because we need to re-read
   420  		// the modeenv during devicemgr startup
   421  		m := boot.Modeenv{
   422  			Mode: tc.currentMode,
   423  		}
   424  		if tc.currentMode != "run" {
   425  			m.RecoverySystem = "20191119"
   426  		}
   427  		err := m.WriteTo("")
   428  		c.Assert(err, check.IsNil)
   429  		d := s.daemon(c)
   430  		st := d.Overlord().State()
   431  		st.Lock()
   432  		// devicemgr needs boot id to request a reboot
   433  		st.VerifyReboot("boot-id-0")
   434  		// device model
   435  		assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""))
   436  		assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...)
   437  		s.mockModel(c, st, model)
   438  		if tc.currentMode == "run" {
   439  			// only set in run mode
   440  			st.Set("seeded-systems", currentSystem)
   441  		}
   442  		// the seeding is done
   443  		st.Set("seeded", true)
   444  		st.Unlock()
   445  
   446  		body := map[string]string{
   447  			"action": "do",
   448  			"mode":   tc.actionMode,
   449  		}
   450  		b, err := json.Marshal(body)
   451  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   452  		buf := bytes.NewBuffer(b)
   453  		req, err := http.NewRequest("POST", "/v2/systems/20191119", buf)
   454  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   455  		// as root
   456  		req.RemoteAddr = "pid=100;uid=0;socket=;"
   457  		rec := httptest.NewRecorder()
   458  		s.serveHTTP(c, rec, req)
   459  		if tc.expUnsupported {
   460  			c.Check(rec.Code, check.Equals, 400, check.Commentf(tc.comment))
   461  		} else {
   462  			c.Check(rec.Code, check.Equals, 200, check.Commentf(tc.comment))
   463  		}
   464  
   465  		var rspBody map[string]interface{}
   466  		err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   467  		c.Assert(err, check.IsNil, check.Commentf(tc.comment))
   468  
   469  		var expResp map[string]interface{}
   470  		if tc.expUnsupported {
   471  			expResp = map[string]interface{}{
   472  				"result": map[string]interface{}{
   473  					"message": fmt.Sprintf("requested action is not supported by system %q", "20191119"),
   474  				},
   475  				"status":      "Bad Request",
   476  				"status-code": 400.0,
   477  				"type":        "error",
   478  			}
   479  		} else {
   480  			expResp = map[string]interface{}{
   481  				"result":      nil,
   482  				"status":      "OK",
   483  				"status-code": 200.0,
   484  				"type":        "sync",
   485  			}
   486  			if tc.expRestart {
   487  				expResp["maintenance"] = map[string]interface{}{
   488  					"kind":    "system-restart",
   489  					"message": "system is restarting",
   490  				}
   491  
   492  				// daemon is not started, only check whether reboot was scheduled as expected
   493  
   494  				// reboot flag
   495  				c.Check(d.RequestedRestart(), check.Equals, state.RestartSystemNow, check.Commentf(tc.comment))
   496  				// slow reboot schedule
   497  				c.Check(cmd.Calls(), check.DeepEquals, [][]string{
   498  					{"shutdown", "-r", "+10", "reboot scheduled to update the system"},
   499  				},
   500  					check.Commentf(tc.comment),
   501  				)
   502  			}
   503  		}
   504  
   505  		c.Assert(rspBody, check.DeepEquals, expResp, check.Commentf(tc.comment))
   506  
   507  		cmd.ForgetCalls()
   508  		s.resetDaemon()
   509  	}
   510  
   511  }
   512  
   513  func (s *systemsSuite) TestSystemActionBrokenSeed(c *check.C) {
   514  	m := boot.Modeenv{
   515  		Mode: "run",
   516  	}
   517  	err := m.WriteTo("")
   518  	c.Assert(err, check.IsNil)
   519  
   520  	d := s.daemonWithOverlordMockAndStore(c)
   521  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   522  	c.Assert(err, check.IsNil)
   523  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   524  	c.Assert(err, check.IsNil)
   525  	d.Overlord().AddManager(mgr)
   526  
   527  	// the seeding is done
   528  	st := d.Overlord().State()
   529  	st.Lock()
   530  	st.Set("seeded", true)
   531  	st.Unlock()
   532  
   533  	restore := s.mockSystemSeeds(c)
   534  	defer restore()
   535  
   536  	err = os.Remove(filepath.Join(dirs.SnapSeedDir, "systems", "20191119", "model"))
   537  	c.Assert(err, check.IsNil)
   538  
   539  	body := `{"action":"do","title":"reinstall","mode":"install"}`
   540  	req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body))
   541  	c.Assert(err, check.IsNil)
   542  	rsp := s.errorReq(c, req, nil)
   543  	c.Check(rsp.Status, check.Equals, 500)
   544  	c.Check(rsp.ErrorResult().Message, check.Matches, `cannot load seed system: cannot load assertions: .*`)
   545  }
   546  
   547  func (s *systemsSuite) TestSystemActionNonRoot(c *check.C) {
   548  	d := s.daemonWithOverlordMockAndStore(c)
   549  	hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner())
   550  	c.Assert(err, check.IsNil)
   551  	mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil)
   552  	c.Assert(err, check.IsNil)
   553  	d.Overlord().AddManager(mgr)
   554  
   555  	body := `{"action":"do","title":"reinstall","mode":"install"}`
   556  
   557  	// pretend to be a simple user
   558  	req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body))
   559  	c.Assert(err, check.IsNil)
   560  	// non root
   561  	req.RemoteAddr = "pid=100;uid=1234;socket=;"
   562  
   563  	rec := httptest.NewRecorder()
   564  	s.serveHTTP(c, rec, req)
   565  	c.Assert(rec.Code, check.Equals, 401)
   566  
   567  	var rspBody map[string]interface{}
   568  	err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   569  	c.Check(err, check.IsNil)
   570  	c.Check(rspBody, check.DeepEquals, map[string]interface{}{
   571  		"result": map[string]interface{}{
   572  			"message": "access denied",
   573  			"kind":    "login-required",
   574  		},
   575  		"status":      "Unauthorized",
   576  		"status-code": 401.0,
   577  		"type":        "error",
   578  	})
   579  }
   580  
   581  func (s *systemsSuite) TestSystemRebootNeedsRoot(c *check.C) {
   582  	s.daemon(c)
   583  
   584  	restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   585  		c.Fatalf("request reboot should not get called")
   586  		return nil
   587  	})
   588  	defer restore()
   589  
   590  	body := `{"action":"reboot"}`
   591  	url := "/v2/systems"
   592  	req, err := http.NewRequest("POST", url, strings.NewReader(body))
   593  	c.Assert(err, check.IsNil)
   594  	req.RemoteAddr = "pid=100;uid=1000;socket=;"
   595  
   596  	rec := httptest.NewRecorder()
   597  	s.serveHTTP(c, rec, req)
   598  	c.Check(rec.Code, check.Equals, 401)
   599  }
   600  
   601  func (s *systemsSuite) TestSystemRebootHappy(c *check.C) {
   602  	s.daemon(c)
   603  
   604  	for _, tc := range []struct {
   605  		systemLabel, mode string
   606  	}{
   607  		{"", ""},
   608  		{"20200101", ""},
   609  		{"", "run"},
   610  		{"", "recover"},
   611  		{"20200101", "run"},
   612  		{"20200101", "recover"},
   613  	} {
   614  		called := 0
   615  		restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   616  			called++
   617  			c.Check(dm, check.NotNil)
   618  			c.Check(systemLabel, check.Equals, tc.systemLabel)
   619  			c.Check(mode, check.Equals, tc.mode)
   620  			return nil
   621  		})
   622  		defer restore()
   623  
   624  		body := fmt.Sprintf(`{"action":"reboot", "mode":"%s"}`, tc.mode)
   625  		url := "/v2/systems"
   626  		if tc.systemLabel != "" {
   627  			url += "/" + tc.systemLabel
   628  		}
   629  		req, err := http.NewRequest("POST", url, strings.NewReader(body))
   630  		c.Assert(err, check.IsNil)
   631  		req.RemoteAddr = "pid=100;uid=0;socket=;"
   632  
   633  		rec := httptest.NewRecorder()
   634  		s.serveHTTP(c, rec, req)
   635  		c.Check(rec.Code, check.Equals, 200)
   636  		c.Check(called, check.Equals, 1)
   637  	}
   638  }
   639  
   640  func (s *systemsSuite) TestSystemRebootUnhappy(c *check.C) {
   641  	s.daemon(c)
   642  
   643  	for _, tc := range []struct {
   644  		rebootErr        error
   645  		expectedHttpCode int
   646  		expectedErr      string
   647  	}{
   648  		{fmt.Errorf("boom"), 500, "boom"},
   649  		{os.ErrNotExist, 404, `requested seed system "" does not exist`},
   650  		{devicestate.ErrUnsupportedAction, 400, `requested action is not supported by system ""`},
   651  	} {
   652  		called := 0
   653  		restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error {
   654  			called++
   655  			return tc.rebootErr
   656  		})
   657  		defer restore()
   658  
   659  		body := fmt.Sprintf(`{"action":"reboot"}`)
   660  		url := "/v2/systems"
   661  		req, err := http.NewRequest("POST", url, strings.NewReader(body))
   662  		c.Assert(err, check.IsNil)
   663  		req.RemoteAddr = "pid=100;uid=0;socket=;"
   664  
   665  		rec := httptest.NewRecorder()
   666  		s.serveHTTP(c, rec, req)
   667  		c.Check(rec.Code, check.Equals, tc.expectedHttpCode)
   668  		c.Check(called, check.Equals, 1)
   669  
   670  		var rspBody map[string]interface{}
   671  		err = json.Unmarshal(rec.Body.Bytes(), &rspBody)
   672  		c.Check(err, check.IsNil)
   673  		c.Check(rspBody["status-code"], check.Equals, float64(tc.expectedHttpCode))
   674  		result := rspBody["result"].(map[string]interface{})
   675  		c.Check(result["message"], check.Equals, tc.expectedErr)
   676  	}
   677  }