github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/usersession/agent/rest_api.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 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 agent 21 22 import ( 23 "encoding/json" 24 "mime" 25 "net/http" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/snapcore/snapd/systemd" 31 "github.com/snapcore/snapd/timeout" 32 ) 33 34 var restApi = []*Command{ 35 rootCmd, 36 sessionInfoCmd, 37 serviceControlCmd, 38 } 39 40 var ( 41 rootCmd = &Command{ 42 Path: "/", 43 GET: nil, 44 } 45 46 sessionInfoCmd = &Command{ 47 Path: "/v1/session-info", 48 GET: sessionInfo, 49 } 50 51 serviceControlCmd = &Command{ 52 Path: "/v1/service-control", 53 POST: postServiceControl, 54 } 55 ) 56 57 func sessionInfo(c *Command, r *http.Request) Response { 58 m := map[string]interface{}{ 59 "version": c.s.Version, 60 } 61 return SyncResponse(m) 62 } 63 64 type serviceInstruction struct { 65 Action string `json:"action"` 66 Services []string `json:"services"` 67 } 68 69 var ( 70 stopTimeout = time.Duration(timeout.DefaultTimeout) 71 killWait = 5 * time.Second 72 ) 73 74 func serviceStart(inst *serviceInstruction, sysd systemd.Systemd) Response { 75 // Refuse to start non-snap services 76 for _, service := range inst.Services { 77 if !strings.HasPrefix(service, "snap.") { 78 return InternalError("cannot start non-snap service %v", service) 79 } 80 } 81 82 startErrors := make(map[string]string) 83 var started []string 84 for _, service := range inst.Services { 85 if err := sysd.Start(service); err != nil { 86 startErrors[service] = err.Error() 87 break 88 } 89 started = append(started, service) 90 } 91 // If we got any failures, attempt to stop the services we started. 92 stopErrors := make(map[string]string) 93 if len(startErrors) != 0 { 94 for _, service := range started { 95 if err := sysd.Stop(service, stopTimeout); err != nil { 96 stopErrors[service] = err.Error() 97 } 98 } 99 } 100 if len(startErrors) == 0 { 101 return SyncResponse(nil) 102 } 103 return SyncResponse(&resp{ 104 Type: ResponseTypeError, 105 Status: 500, 106 Result: &errorResult{ 107 Message: "some user services failed to start", 108 Kind: errorKindServiceControl, 109 Value: map[string]interface{}{ 110 "start-errors": startErrors, 111 "stop-errors": stopErrors, 112 }, 113 }, 114 }) 115 } 116 117 func serviceStop(inst *serviceInstruction, sysd systemd.Systemd) Response { 118 // Refuse to stop non-snap services 119 for _, service := range inst.Services { 120 if !strings.HasPrefix(service, "snap.") { 121 return InternalError("cannot stop non-snap service %v", service) 122 } 123 } 124 125 stopErrors := make(map[string]string) 126 for _, service := range inst.Services { 127 if err := sysd.Stop(service, stopTimeout); err != nil { 128 stopErrors[service] = err.Error() 129 } 130 } 131 if len(stopErrors) == 0 { 132 return SyncResponse(nil) 133 } 134 return SyncResponse(&resp{ 135 Type: ResponseTypeError, 136 Status: 500, 137 Result: &errorResult{ 138 Message: "some user services failed to stop", 139 Kind: errorKindServiceControl, 140 Value: map[string]interface{}{ 141 "stop-errors": stopErrors, 142 }, 143 }, 144 }) 145 } 146 147 func serviceDaemonReload(inst *serviceInstruction, sysd systemd.Systemd) Response { 148 if len(inst.Services) != 0 { 149 return InternalError("daemon-reload should not be called with any services") 150 } 151 if err := sysd.DaemonReload(); err != nil { 152 return InternalError("cannot reload daemon: %v", err) 153 } 154 return SyncResponse(nil) 155 } 156 157 var serviceInstructionDispTable = map[string]func(*serviceInstruction, systemd.Systemd) Response{ 158 "start": serviceStart, 159 "stop": serviceStop, 160 "daemon-reload": serviceDaemonReload, 161 } 162 163 var systemdLock sync.Mutex 164 165 type dummyReporter struct{} 166 167 func (dummyReporter) Notify(string) {} 168 169 func postServiceControl(c *Command, r *http.Request) Response { 170 contentType := r.Header.Get("Content-Type") 171 mediaType, params, err := mime.ParseMediaType(contentType) 172 if err != nil { 173 return BadRequest("cannot parse content type: %v", err) 174 } 175 176 if mediaType != "application/json" { 177 return BadRequest("unknown content type: %s", contentType) 178 } 179 180 charset := strings.ToUpper(params["charset"]) 181 if charset != "" && charset != "UTF-8" { 182 return BadRequest("unknown charset in content type: %s", contentType) 183 } 184 185 decoder := json.NewDecoder(r.Body) 186 var inst serviceInstruction 187 if err := decoder.Decode(&inst); err != nil { 188 return BadRequest("cannot decode request body into service instruction: %v", err) 189 } 190 impl := serviceInstructionDispTable[inst.Action] 191 if impl == nil { 192 return BadRequest("unknown action %s", inst.Action) 193 } 194 // Prevent multiple systemd actions from being carried out simultaneously 195 systemdLock.Lock() 196 defer systemdLock.Unlock() 197 sysd := systemd.New(systemd.UserMode, dummyReporter{}) 198 return impl(&inst, sysd) 199 }