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:
Pablo Carranza Velez 2017-11-01 00:51:36 -07:00
parent d3e98eab11
commit 7ae7ceab73
3 changed files with 77 additions and 181 deletions

View File

@ -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

View File

@ -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")
}

View File

@ -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)