mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-04-24 13:05:51 +00:00
gosuper: add internal endpoints to get VPN and log-to-display status, and remove purge and IP address endpoints
Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
This commit is contained in:
parent
d3e98eab11
commit
7ae7ceab73
@ -4,31 +4,18 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"resin-supervisor/gosuper/systemd"
|
||||
)
|
||||
|
||||
// Compile the expression once, usually at init time.
|
||||
// Use raw strings to avoid having to quote the backslashes.
|
||||
var dockerMatch = regexp.MustCompile(`(balena[0-9]+)|(docker[0-9]+)|(rce[0-9]+)|(tun[0-9]+)|(resin-vpn)|(resin-dns)`)
|
||||
|
||||
// APIResponse The api response sent from go supervisor
|
||||
type APIResponse struct {
|
||||
Data interface{}
|
||||
Error string
|
||||
}
|
||||
|
||||
//PurgeBody struct for the ApplicationId interfact
|
||||
type PurgeBody struct {
|
||||
ApplicationId interface{}
|
||||
}
|
||||
|
||||
// VPNBody interface for post request received by VPN control end point
|
||||
type VPNBody struct {
|
||||
Enable bool
|
||||
@ -57,24 +44,6 @@ func parseJSONBody(destination interface{}, request *http.Request) error {
|
||||
return decoder.Decode(&destination)
|
||||
}
|
||||
|
||||
func parsePurgeBody(request *http.Request) (appId string, err error) {
|
||||
var body PurgeBody
|
||||
if err = parseJSONBody(&body, request); err != nil {
|
||||
return
|
||||
}
|
||||
switch v := body.ApplicationId.(type) {
|
||||
case string:
|
||||
appId = v
|
||||
case float64:
|
||||
if v != 0 {
|
||||
appId = strconv.Itoa(int(v))
|
||||
}
|
||||
default:
|
||||
log.Printf("Invalid appId type %T\n", v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{}, string, int), sendError func(error)) {
|
||||
sendResponse = func(data interface{}, errorMsg string, statusCode int) {
|
||||
jsonResponse(writer, APIResponse{data, errorMsg}, statusCode)
|
||||
@ -85,35 +54,6 @@ func responseSenders(writer http.ResponseWriter) (sendResponse func(interface{},
|
||||
return
|
||||
}
|
||||
|
||||
// PurgeHandler Purges the data of the appID's application in the /data partition
|
||||
func PurgeHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
log.Println("Purging /data")
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
sendBadRequest := func(errorMsg string) {
|
||||
sendResponse("Error", errorMsg, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if appId, err := parsePurgeBody(request); err != nil {
|
||||
sendBadRequest("Invalid request")
|
||||
} else if appId == "" {
|
||||
sendBadRequest("applicationId is required")
|
||||
} else if !IsValidAppId(appId) {
|
||||
sendBadRequest(fmt.Sprintf("Invalid applicationId '%s'", appId))
|
||||
} else if _, err = os.Stat(ResinDataPath + appId); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
sendResponse("Error", fmt.Sprintf("Invalid applicationId '%s': Directory does not exist", appId), http.StatusNotFound)
|
||||
} else {
|
||||
sendError(err)
|
||||
}
|
||||
} else if err = os.RemoveAll(ResinDataPath + appId); err != nil {
|
||||
sendError(err)
|
||||
} else if err = os.Mkdir(ResinDataPath+appId, 0755); err != nil {
|
||||
sendError(err)
|
||||
} else {
|
||||
sendResponse("OK", "", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func inASecond(theFunc func()) {
|
||||
time.Sleep(time.Duration(time.Second))
|
||||
theFunc()
|
||||
@ -145,55 +85,6 @@ func ShutdownHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
go inASecond(func() { systemd.Logind.PowerOff(false) })
|
||||
}
|
||||
|
||||
// This function returns all active IPs of the interfaces that arent docker/rce and loopback
|
||||
func ipAddress() (ipAddresses []string, err error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ipAddresses, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if (iface.Flags&net.FlagUp == 0) || (iface.Flags&net.FlagLoopback != 0) || dockerMatch.MatchString(iface.Name) {
|
||||
continue // Interface down or Interface is loopback or Interface is a docker IP
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return ipAddresses, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
default:
|
||||
log.Printf("Warning: Unrecognised type %T\n", v)
|
||||
continue
|
||||
}
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
if ip = ip.To4(); ip == nil {
|
||||
continue // This isnt an IPv4 Addresss
|
||||
}
|
||||
ipAddresses = append(ipAddresses, ip.String())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//IPAddressHandler is used to reply back with an array of the IPaddress used by the system.
|
||||
func IPAddressHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
if ipAddr, err := ipAddress(); err != nil {
|
||||
sendError(err)
|
||||
} else {
|
||||
payload := make(map[string][]string)
|
||||
payload["IPAddresses"] = ipAddr
|
||||
sendResponse(payload, "", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
//VPNControl is used to control VPN service status with dbus
|
||||
func VPNControl(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
@ -224,11 +115,76 @@ func VPNControl(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse("OK", "", http.StatusAccepted)
|
||||
}
|
||||
|
||||
func getUnitStatus(unitName string) (state bool, err error) {
|
||||
if systemd.Dbus == nil {
|
||||
err = fmt.Errorf("Systemd dbus unavailable, cannot get unit status.")
|
||||
return
|
||||
}
|
||||
if activeState, e := systemd.Dbus.GetUnitProperty(unitName, "ActiveState"); e != nil {
|
||||
err = fmt.Errorf("Unable to get unit status: %v", e)
|
||||
return
|
||||
} else {
|
||||
state = activeState.Value.String() == `"active"`
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func unitStatusHandler(serviceName string, writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
if status, err := getUnitStatus(serviceName); err != nil {
|
||||
sendError(fmt.Errorf("Unable to get VPN status: %v", err))
|
||||
return
|
||||
} else {
|
||||
sendResponse(status, "", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func VPNStatus(writer http.ResponseWriter, request *http.Request) {
|
||||
unitStatusHandler("openvpn-resin.service", writer, request)
|
||||
}
|
||||
|
||||
func logToDisplayServiceName() (serviceName string, err error) {
|
||||
serviceName = "resin-info@tty1.service"
|
||||
serviceNameOld := "tty-replacement.service"
|
||||
if systemd.Dbus == nil {
|
||||
err = fmt.Errorf("Systemd dbus unavailable, cannot get log to display service.")
|
||||
return
|
||||
}
|
||||
if loaded, e := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); e != nil {
|
||||
err = fmt.Errorf("Unable to get log to display load status: %v", e)
|
||||
return
|
||||
} else if loaded.Value.String() == `"not-found"` {
|
||||
// If the resin-info service is not found, we're on an older OS
|
||||
// which uses a different service name
|
||||
serviceName = serviceNameOld
|
||||
}
|
||||
if loaded, e := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); e != nil {
|
||||
err = fmt.Errorf("Unable to get log to display load status: %v", e)
|
||||
return
|
||||
} else if loaded.Value.String() == `"not-found"` {
|
||||
// We might be in a different OS that just doesn't have the service
|
||||
serviceName = ""
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func LogToDisplayStatus(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
serviceName, err := logToDisplayServiceName()
|
||||
if err != nil {
|
||||
sendError(err)
|
||||
return
|
||||
} else if serviceName == "" {
|
||||
sendResponse("Error", "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
unitStatusHandler(serviceName, writer, request)
|
||||
}
|
||||
|
||||
//LogToDisplayControl is used to control tty-replacement service status with dbus
|
||||
func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
serviceName := "resin-info@tty1.service"
|
||||
serviceNameOld := "tty-replacement.service"
|
||||
var body LogToDisplayBody
|
||||
if err := parseJSONBody(&body, request); err != nil {
|
||||
log.Println(err)
|
||||
@ -241,20 +197,19 @@ func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if loaded, err := systemd.Dbus.GetUnitProperty(serviceName, "LoadState"); err != nil {
|
||||
sendError(fmt.Errorf("Unable to get log to display load status: %v", err))
|
||||
serviceName, err := logToDisplayServiceName()
|
||||
if err != nil {
|
||||
sendError(err)
|
||||
return
|
||||
} else if serviceName == "" {
|
||||
sendResponse("Error", "Not found", http.StatusNotFound)
|
||||
return
|
||||
} else if loaded.Value.String() == `"not-found"` {
|
||||
// If the resin-info service is not found, we're on an older OS
|
||||
// which uses a different service name
|
||||
serviceName = serviceNameOld
|
||||
}
|
||||
|
||||
if activeState, err := systemd.Dbus.GetUnitProperty(serviceName, "ActiveState"); err != nil {
|
||||
if status, err := getUnitStatus(serviceName); err != nil {
|
||||
sendError(fmt.Errorf("Unable to get log to display status: %v", err))
|
||||
return
|
||||
} else {
|
||||
status := activeState.Value.String() == `"active"`
|
||||
enable := body.Enable
|
||||
if status == enable {
|
||||
// Nothing to do, return Data = false to signal nothing was changed
|
||||
|
@ -20,12 +20,12 @@ func setupApi(router *mux.Router) {
|
||||
})
|
||||
|
||||
apiv1 := router.PathPrefix("/v1").Subrouter()
|
||||
apiv1.HandleFunc("/ipaddr", IPAddressHandler).Methods("GET")
|
||||
apiv1.HandleFunc("/purge", PurgeHandler).Methods("POST")
|
||||
apiv1.HandleFunc("/reboot", RebootHandler).Methods("POST")
|
||||
apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST")
|
||||
apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST")
|
||||
apiv1.HandleFunc("/set-log-to-display", LogToDisplayControl).Methods("POST")
|
||||
apiv1.HandleFunc("/vpncontrol", VPNStatus).Methods("GET")
|
||||
apiv1.HandleFunc("/log-to-display", LogToDisplayControl).Methods("POST")
|
||||
apiv1.HandleFunc("/log-to-display", LogToDisplayStatus).Methods("GET")
|
||||
apiv1.HandleFunc("/restart-service", RestartService).Methods("POST")
|
||||
}
|
||||
|
||||
|
@ -1,69 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var purgeTests = []struct {
|
||||
JsonInput string
|
||||
AppId string
|
||||
JsonResponse string
|
||||
IsSuccess bool
|
||||
HttpStatus int
|
||||
}{
|
||||
{`{"applicationId": "1"}`, "1", `{"Data":"OK","Error":""}`, true, http.StatusOK},
|
||||
{`{"applicationId": 1}`, "1", `{"Data":"OK","Error":""}`, true, http.StatusOK},
|
||||
{`{"applicationId": "hi"}`, "1", `{"Data":"Error","Error":"Invalid applicationId 'hi'"}`, false, http.StatusBadRequest},
|
||||
{`{"applicationId": "2"}`, "1", `{"Data":"Error","Error":"Invalid applicationId '2': Directory does not exist"}`, false, http.StatusNotFound},
|
||||
{`{}`, "1", `{"Data":"Error","Error":"applicationId is required"}`, false, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
func TestPurge(t *testing.T) {
|
||||
for i, testCase := range purgeTests {
|
||||
t.Logf("Testing Purge case #%d", i)
|
||||
request, err := http.NewRequest("POST", "/v1/purge", strings.NewReader(testCase.JsonInput))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value")
|
||||
writer := httptest.NewRecorder()
|
||||
ResinDataPath = "test-data/"
|
||||
dataPath := ResinDataPath + testCase.AppId
|
||||
testFile := dataPath + "/test"
|
||||
|
||||
if err = os.MkdirAll(dataPath, 0755); err != nil {
|
||||
t.Fatal("Could not create test directory for purge")
|
||||
} else if err = ioutil.WriteFile(testFile, []byte("test"), 0777); err != nil {
|
||||
t.Fatal("Could not create test file for purge")
|
||||
}
|
||||
|
||||
PurgeHandler(writer, request)
|
||||
|
||||
if writer.Code != testCase.HttpStatus {
|
||||
t.Errorf("Purge didn't return %v, got %v", testCase.HttpStatus, writer.Code)
|
||||
}
|
||||
if !strings.EqualFold(writer.Body.String(), testCase.JsonResponse) {
|
||||
t.Errorf(`Purge response didn't match the expected JSON, expected "%s" got: "%s"`, testCase.JsonResponse, writer.Body.String())
|
||||
}
|
||||
|
||||
if dirContents, err := ioutil.ReadDir(dataPath); err != nil {
|
||||
t.Errorf("Could not read the data path after purge: %s", err)
|
||||
} else {
|
||||
fileCount := len(dirContents)
|
||||
if fileCount > 0 && testCase.IsSuccess {
|
||||
t.Error("Data directory not empty after purge")
|
||||
} else if fileCount == 0 && !testCase.IsSuccess {
|
||||
t.Error("Data directory empty after purge (but it failed)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
if config, err := ReadConfig("config_for_test.json"); err != nil {
|
||||
t.Error(err)
|
||||
|
Loading…
x
Reference in New Issue
Block a user