Skip to content

Commit

Permalink
usersession: add a /v1/services endpoint to session agent
Browse files Browse the repository at this point in the history
  • Loading branch information
jhenstridge committed Aug 15, 2019
1 parent 9df99b6 commit 5219dd2
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 1 deletion.
1 change: 1 addition & 0 deletions usersession/agent/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ package agent

var (
SessionInfoCmd = sessionInfoCmd
ServicesCmd = servicesCmd
)
114 changes: 114 additions & 0 deletions usersession/agent/rest_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,20 @@
package agent

import (
"encoding/json"
"net/http"
"strings"
"time"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/timeout"
)

var restApi = []*Command{
rootCmd,
sessionInfoCmd,
servicesCmd,
}

var (
Expand All @@ -38,6 +46,11 @@ var (
Path: "/v1/session-info",
GET: sessionInfo,
}

servicesCmd = &Command{
Path: "/v1/services",
POST: postServices,
}
)

func sessionInfo(c *Command, r *http.Request) Response {
Expand All @@ -46,3 +59,104 @@ func sessionInfo(c *Command, r *http.Request) Response {
}
return SyncResponse(m)
}

type serviceInstruction struct {
Action string `json:"action"`
Services []string `json:"services"`
}

var killWait = 5 * time.Second

func stopOneService(service string, sysd systemd.Systemd) error {
err := sysd.Stop(service, time.Duration(timeout.DefaultTimeout))
if err != nil && systemd.IsTimeout(err) {
// ignore errors for kill; nothing we'd do differently at this point
sysd.Kill(service, "TERM", "")
time.Sleep(killWait)
sysd.Kill(service, "KILL", "")
}
return err
}

func serviceStart(inst *serviceInstruction, sysd systemd.Systemd) Response {
// Refuse to start non-snap services
for _, service := range inst.Services {
if !strings.HasPrefix(service, "snap.") {
return InternalError("cannot start service %v", service)
}
}

var startErrors map[string]string
var started []string
for _, service := range inst.Services {
if err := sysd.Start(service); err != nil {
startErrors[service] = err.Error()
break
}
started = append(started, service)
}
// If we got any failures, attempt to stop the services we started.
if len(startErrors) != 0 {
for _, service := range started {
if err := stopOneService(service, sysd); err != nil {
startErrors[service] = err.Error()
}
}
}
return SyncResponse(startErrors)
}

func serviceStop(inst *serviceInstruction, sysd systemd.Systemd) Response {
// Refuse to start non-snap services
for _, service := range inst.Services {
if !strings.HasPrefix(service, "snap.") {
return InternalError("cannot stop service %v", service)
}
}

var stopErrors map[string]string
for _, service := range inst.Services {
if err := stopOneService(service, sysd); err != nil {
stopErrors[service] = err.Error()
}
}
return SyncResponse(stopErrors)
}

func serviceDaemonReload(inst *serviceInstruction, sysd systemd.Systemd) Response {
if len(inst.Services) != 0 {
return InternalError("daemon-reload should not be called with any services")
}
if err := sysd.DaemonReload(); err != nil {
return InternalError("cannot reload daemon: %v", err)
}
return SyncResponse(nil)
}

var serviceInstructionDispTable = map[string]func(*serviceInstruction, systemd.Systemd) Response{
"start": serviceStart,
"stop": serviceStop,
"daemon-reload": serviceDaemonReload,
}

type dummyReporter struct{}

func (dummyReporter) Notify(string) {}

func postServices(c *Command, r *http.Request) Response {
if contentType := r.Header.Get("Content-Type"); contentType != "application/json" {
return BadRequest("unknown content type: %s", contentType)
}

decoder := json.NewDecoder(r.Body)
var inst serviceInstruction
if err := decoder.Decode(&inst); err != nil {
return BadRequest("cannot decode request body into service instruction: %v", err)
}
impl := serviceInstructionDispTable[inst.Action]
if impl == nil {
return BadRequest("unknown action %s", inst.Action)
}
sysd := systemd.New(dirs.GlobalRootDir, systemd.UserMode, dummyReporter{})
return impl(&inst, sysd)
}
101 changes: 100 additions & 1 deletion usersession/agent/rest_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,48 @@
package agent_test

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"time"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/testutil"
"github.com/snapcore/snapd/usersession/agent"
)

type restSuite struct{}
type restSuite struct {
testutil.BaseTest
sysdLog [][]string
}

var _ = Suite(&restSuite{})

func (s *restSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
dirs.SetRootDir(c.MkDir())
xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil)

s.sysdLog = nil
restore := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
s.sysdLog = append(s.sysdLog, cmd)
return []byte("ActiveState=inactive\n"), nil
})
s.AddCleanup(restore)
restore = systemd.MockStopDelays(time.Millisecond, 25*time.Second)
s.AddCleanup(restore)
}

func (s *restSuite) TearDownTest(c *C) {
dirs.SetRootDir("")
s.BaseTest.TearDownTest(c)
}

type resp struct {
Expand Down Expand Up @@ -74,3 +93,83 @@ func (s *restSuite) TestSessionInfo(c *C) {
"version": "42b1",
})
}

func (s *restSuite) TestServices(c *C) {
// the agent.Services end point only supports POST requests
c.Assert(agent.ServicesCmd.GET, IsNil)
c.Check(agent.ServicesCmd.PUT, IsNil)
c.Check(agent.ServicesCmd.POST, NotNil)
c.Check(agent.ServicesCmd.DELETE, IsNil)

c.Check(agent.ServicesCmd.Path, Equals, "/v1/services")
}

func (s *restSuite) TestServicesDaemonReload(c *C) {
_, err := agent.New()
c.Assert(err, IsNil)

req, err := http.NewRequest("POST", "/v1/services", bytes.NewBufferString(`{"action":"daemon-reload"}`))
req.Header.Set("Content-Type", "application/json")
c.Assert(err, IsNil)
rec := httptest.NewRecorder()
agent.ServicesCmd.POST(agent.ServicesCmd, req).ServeHTTP(rec, req)
c.Check(rec.Code, Equals, 200)
c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")

var rsp resp
c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
c.Check(rsp.Result, IsNil)

c.Check(s.sysdLog, DeepEquals, [][]string{
{"--user", "daemon-reload"},
})
}

func (s *restSuite) TestServicesStart(c *C) {
_, err := agent.New()
c.Assert(err, IsNil)

req, err := http.NewRequest("POST", "/v1/services", bytes.NewBufferString(`{"action":"start","services":["snap.foo.service", "snap.bar.service"]}`))
req.Header.Set("Content-Type", "application/json")
c.Assert(err, IsNil)
rec := httptest.NewRecorder()
agent.ServicesCmd.POST(agent.ServicesCmd, req).ServeHTTP(rec, req)
c.Check(rec.Code, Equals, 200)
c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")

var rsp resp
c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
c.Check(rsp.Result, IsNil)

c.Check(s.sysdLog, DeepEquals, [][]string{
{"--user", "start", "snap.foo.service"},
{"--user", "start", "snap.bar.service"},
})
}

func (s *restSuite) TestServicesStop(c *C) {
_, err := agent.New()
c.Assert(err, IsNil)

req, err := http.NewRequest("POST", "/v1/services", bytes.NewBufferString(`{"action":"stop","services":["snap.foo.service", "snap.bar.service"]}`))
req.Header.Set("Content-Type", "application/json")
c.Assert(err, IsNil)
rec := httptest.NewRecorder()
agent.ServicesCmd.POST(agent.ServicesCmd, req).ServeHTTP(rec, req)
c.Check(rec.Code, Equals, 200)
c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json")

var rsp resp
c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil)
c.Check(rsp.Type, Equals, agent.ResponseTypeSync)
c.Check(rsp.Result, IsNil)

c.Check(s.sysdLog, DeepEquals, [][]string{
{"--user", "stop", "snap.foo.service"},
{"--user", "show", "--property=ActiveState", "snap.foo.service"},
{"--user", "stop", "snap.bar.service"},
{"--user", "show", "--property=ActiveState", "snap.bar.service"},
})
}

0 comments on commit 5219dd2

Please sign in to comment.