Adds api
This commit is contained in:
47
cmd/tpapi/config.go
Normal file
47
cmd/tpapi/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/logutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type configStructure struct {
|
||||
// time configuration
|
||||
TimeFormat string `json:"time_format"`
|
||||
TimeZone *time.Location
|
||||
TimeZoneUTC *time.Location
|
||||
|
||||
// logging
|
||||
LogLevel int `json:"log_level"`
|
||||
Log *logutils.LevelFilter `json:"log_level_fileter"`
|
||||
|
||||
// webserver
|
||||
WebSrvPort int `json:"webserver_port"`
|
||||
WebSrvIP string `json:"webserver_ip"`
|
||||
WebSrvReadTimeout int `json:"webserver_read_timeout"`
|
||||
WebSrvWriteTimeout int `json:"webserver_write_timeout"`
|
||||
WebSrvIdleTimeout int `json:"webserver_idle_timeout"`
|
||||
|
||||
// prometheus
|
||||
Prometheus configPrometheus
|
||||
}
|
||||
|
||||
type configPrometheus struct {
|
||||
NumGroupCreated prometheus.Gauge
|
||||
NumGroupChanged prometheus.Gauge
|
||||
NumGroupDeleted prometheus.Gauge
|
||||
}
|
||||
|
||||
var config = configStructure{
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
Log: &logutils.LevelFilter{
|
||||
Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"},
|
||||
Writer: os.Stderr,
|
||||
},
|
||||
WebSrvReadTimeout: 2,
|
||||
WebSrvWriteTimeout: 10,
|
||||
WebSrvIdleTimeout: 2,
|
||||
}
|
215
cmd/tpapi/httpServer.go
Normal file
215
cmd/tpapi/httpServer.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
"tplink/internal/tplink"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func httpAccessLog(req *http.Request) {
|
||||
log.Printf("[TRACE] %s - %s - %s\n", req.Method, req.RemoteAddr, req.RequestURI)
|
||||
}
|
||||
|
||||
func crossSiteOrigin(w http.ResponseWriter) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
}
|
||||
|
||||
func httpServer(host string, port int) {
|
||||
path := http.NewServeMux()
|
||||
|
||||
connection := &http.Server{
|
||||
Addr: host + ":" + strconv.FormatInt(int64(port), 10),
|
||||
Handler: path,
|
||||
ReadTimeout: time.Duration(config.WebSrvReadTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(config.WebSrvWriteTimeout) * time.Second,
|
||||
IdleTimeout: time.Duration(config.WebSrvIdleTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// metrics
|
||||
path.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
|
||||
httpAccessLog(r)
|
||||
crossSiteOrigin(w)
|
||||
promhttp.Handler().ServeHTTP(w, r)
|
||||
})
|
||||
// api
|
||||
path.HandleFunc("/api/v1/tplink", webTPLink)
|
||||
// healthcheck
|
||||
path.HandleFunc("/healthcheck", webHealthCheck)
|
||||
// root
|
||||
path.HandleFunc("/", webRoot)
|
||||
|
||||
if err := connection.ListenAndServe(); err != nil {
|
||||
log.Fatalf("[ERROR] %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func webRoot(w http.ResponseWriter, r *http.Request) {
|
||||
httpAccessLog(r)
|
||||
crossSiteOrigin(w)
|
||||
|
||||
if strings.ToLower(r.Method) == "get" {
|
||||
tmpltWebRoot(w)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Request to '/' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
|
||||
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
|
||||
}
|
||||
}
|
||||
|
||||
func webHealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
httpAccessLog(r)
|
||||
crossSiteOrigin(w)
|
||||
|
||||
if strings.ToLower(r.Method) == "get" {
|
||||
tmpltHealthCheck(w)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Request to '/healthcheck' was made using the wrong method: expected %s, got %s\n", "GET", strings.ToUpper(r.Method))
|
||||
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
|
||||
}
|
||||
}
|
||||
|
||||
func webTPLink(w http.ResponseWriter, r *http.Request) {
|
||||
httpAccessLog(r)
|
||||
crossSiteOrigin(w)
|
||||
|
||||
if strings.ToLower(r.Method) == "get" {
|
||||
keys := r.URL.Query()
|
||||
if len(keys) == 0 {
|
||||
log.Printf("[INFO] Required parameters missing: no host, state, or deviceid was provided.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for host, state, or deviceid was provided.")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
host string
|
||||
hostSet bool
|
||||
state bool
|
||||
stateSet bool
|
||||
id int
|
||||
)
|
||||
|
||||
for k, v := range keys {
|
||||
log.Printf("%s: %v\n", k, v)
|
||||
switch strings.ToLower(k) {
|
||||
case "host":
|
||||
if len(v) != 1 {
|
||||
log.Printf("[INFO] Host was defined multiple times.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameter host was defined multiple times.")
|
||||
return
|
||||
}
|
||||
_, err := net.LookupHost(v[0])
|
||||
if err != nil {
|
||||
log.Printf("[INFO] Unable to resolve provided hostname: %s\n", v[0])
|
||||
tmpltError(w, http.StatusBadRequest, "Unable to resolve provided hostname.")
|
||||
return
|
||||
}
|
||||
hostSet = true
|
||||
host = v[0]
|
||||
|
||||
case "state":
|
||||
if len(v) != 1 {
|
||||
log.Printf("[INFO] State was defined multiple times.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameter state was defined multiple times.")
|
||||
return
|
||||
}
|
||||
s, err := strconv.ParseBool(v[0])
|
||||
if err != nil {
|
||||
log.Printf("[INFO] State is not boolean: %s\n", v[0])
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameter state is not boolean.")
|
||||
return
|
||||
}
|
||||
stateSet = true
|
||||
state = s
|
||||
|
||||
case "deviceid":
|
||||
if len(v) != 1 {
|
||||
log.Printf("[INFO] Deviceid was defined multiple times.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameter device was defined multiple times.")
|
||||
return
|
||||
}
|
||||
d, err := strconv.Atoi(v[0])
|
||||
if err != nil {
|
||||
log.Printf("[INFO] Deviceid is not an integer: %s\n", v[0])
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameter deviceid is not an integer.")
|
||||
return
|
||||
}
|
||||
id = d
|
||||
}
|
||||
}
|
||||
|
||||
if !hostSet {
|
||||
log.Printf("[INFO] Required parameter missing: no host was provided.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for host provided.")
|
||||
return
|
||||
}
|
||||
if !stateSet {
|
||||
log.Printf("[INFO] Required parameter missing: no state was provided.\n")
|
||||
tmpltError(w, http.StatusBadRequest, "Required parameters missing: no parameter for state provided.")
|
||||
return
|
||||
}
|
||||
|
||||
t := tplink.Tplink{
|
||||
Host: host,
|
||||
SwitchID: id,
|
||||
}
|
||||
if err := t.ChangeStateMultiSwitch(state); err != nil {
|
||||
log.Printf("[INFO] Unable to change device state: %v\n", err)
|
||||
tmpltError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to change device state: %v\n", err))
|
||||
return
|
||||
}
|
||||
|
||||
sysInfo, err := t.SystemInfo()
|
||||
if err != nil {
|
||||
log.Printf("[INFO] Unable to get info from target device: %v\n", err)
|
||||
tmpltError(w, http.StatusInternalServerError, fmt.Sprintf("Unable to read info from device: %v\n", err))
|
||||
return
|
||||
}
|
||||
|
||||
var ds int
|
||||
if len(sysInfo.System.GetSysinfo.Children) == 0 {
|
||||
ds = sysInfo.System.GetSysinfo.RelayState
|
||||
} else {
|
||||
ds = sysInfo.System.GetSysinfo.Children[id].State
|
||||
}
|
||||
|
||||
var fs string
|
||||
if ds == 0 {
|
||||
fs = "OFF"
|
||||
} else {
|
||||
fs = "ON"
|
||||
}
|
||||
|
||||
o := struct {
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Host string `json:"host" yaml:"host"`
|
||||
DeviceID int `json:"device_id" yaml:"deviceID"`
|
||||
}{
|
||||
Status: fs,
|
||||
Host: host,
|
||||
DeviceID: id,
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
output, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
|
||||
}
|
||||
w.Write(output) //nolint:errcheck
|
||||
|
||||
} else if strings.ToLower(r.Method) == "options" {
|
||||
return
|
||||
} else {
|
||||
log.Printf("[DEBUG] Request to '%s' was made using the wrong method: expected %s, got %s", "GET|OPTIONS", r.URL.Path, strings.ToUpper(r.Method))
|
||||
tmpltError(w, http.StatusBadRequest, "Invalid http method.")
|
||||
}
|
||||
}
|
67
cmd/tpapi/httpTemplates.go
Normal file
67
cmd/tpapi/httpTemplates.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func tmpltError(w http.ResponseWriter, s int, m string) {
|
||||
var (
|
||||
output []byte
|
||||
o = struct {
|
||||
Error bool `json:"error" yaml:"error"`
|
||||
ErrorMsg string `json:"errorMessage" yaml:"errorMessage"`
|
||||
}{
|
||||
Error: true,
|
||||
ErrorMsg: m,
|
||||
}
|
||||
err error
|
||||
)
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
output, err = json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
|
||||
}
|
||||
w.WriteHeader(s)
|
||||
w.Write(output) //nolint:errcheck
|
||||
}
|
||||
|
||||
func tmpltWebRoot(w http.ResponseWriter) {
|
||||
o := struct {
|
||||
Application string `json:"application" yaml:"application"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
}{
|
||||
Application: "TP-Link Web API",
|
||||
Description: "API for automated calls to TP-Link devices",
|
||||
Version: "v1.0.0",
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
output, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] Unable to marshal error message: %v.", err)
|
||||
}
|
||||
w.Write(output) //nolint:errcheck
|
||||
}
|
||||
|
||||
func tmpltHealthCheck(w http.ResponseWriter) {
|
||||
o := struct {
|
||||
WebServer bool `json:"webServerActive" yaml:"webServerActive"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
}{
|
||||
WebServer: true,
|
||||
Status: "healthy",
|
||||
}
|
||||
|
||||
output, err := json.MarshalIndent(o, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("[TRACE] Unable to marshal status message: %v.", err)
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Write(output) //nolint:errcheck
|
||||
}
|
96
cmd/tpapi/init.go
Normal file
96
cmd/tpapi/init.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/logutils"
|
||||
)
|
||||
|
||||
// getEnvString returns string from environment variable
|
||||
func getEnvString(env, def string) (val string) { //nolint:deadcode
|
||||
val = os.Getenv(env)
|
||||
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getEnvInt returns int from environment variable
|
||||
func getEnvInt(env string, def int) (ret int) {
|
||||
val := os.Getenv(env)
|
||||
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
ret, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setLogLevel(l int) {
|
||||
switch {
|
||||
case l <= 20:
|
||||
config.Log.SetMinLevel(logutils.LogLevel("ERROR"))
|
||||
case l > 20 && l <= 40:
|
||||
config.Log.SetMinLevel(logutils.LogLevel("WARNING"))
|
||||
case l > 40 && l <= 60:
|
||||
config.Log.SetMinLevel(logutils.LogLevel("INFO"))
|
||||
case l > 60 && l <= 80:
|
||||
config.Log.SetMinLevel(logutils.LogLevel("DEBUG"))
|
||||
case l > 80:
|
||||
config.Log.SetMinLevel(logutils.LogLevel("TRACE"))
|
||||
}
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
var tz string
|
||||
|
||||
// log configuration
|
||||
flag.IntVar(&config.LogLevel,
|
||||
"log",
|
||||
getEnvInt("LOG_LEVEL", 0),
|
||||
"(LOG_LEVEL) Set the log verbosity.",
|
||||
)
|
||||
// locality configuration
|
||||
flag.StringVar(&tz,
|
||||
"timezone",
|
||||
getEnvString("TIMEZONE", "America/Chicago"),
|
||||
"(TIMEZONE) Timezone for the sunrise/sunset calculation.",
|
||||
)
|
||||
// local webserver configuration
|
||||
flag.IntVar(&config.WebSrvPort,
|
||||
"http-port",
|
||||
getEnvInt("HTTP_PORT", 8080),
|
||||
"(HTTP_PORT) Listen port for internal webserver")
|
||||
flag.StringVar(&config.WebSrvIP,
|
||||
"http-ip",
|
||||
getEnvString("HTTP_IP", "0.0.0.0"),
|
||||
"(HTTP_IP) Listen ip for internal webserver")
|
||||
flag.IntVar(&config.WebSrvIdleTimeout,
|
||||
"http-idle-timeout",
|
||||
getEnvInt("HTTP_IDLE_TIMEOUT", 2),
|
||||
"(HTTP_IDLE_TIMEOUT) Idle timeout for internal webserver")
|
||||
flag.IntVar(&config.WebSrvReadTimeout,
|
||||
"http-read-timeout",
|
||||
getEnvInt("HTTP_READ_TIMEOUT", 5),
|
||||
"(HTTP_READ_TIMEOUT) Read timeout for internal webserver")
|
||||
flag.IntVar(&config.WebSrvWriteTimeout,
|
||||
"http-write-timeout",
|
||||
getEnvInt("HTTP_WRITE_TIMEOUT", 2),
|
||||
"(HTTP_WRITE_TIMEOUT) Write timeout for internal webserver")
|
||||
flag.Parse()
|
||||
|
||||
setLogLevel(config.LogLevel)
|
||||
log.SetOutput(config.Log)
|
||||
|
||||
log.Printf("[DEBUG] Initialization complete.")
|
||||
}
|
28
cmd/tpapi/main.go
Normal file
28
cmd/tpapi/main.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func forever() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
sig := <-c
|
||||
log.Printf("[INFO] shutting down, detected signal: %s", sig)
|
||||
}
|
||||
|
||||
func main() {
|
||||
initialize()
|
||||
|
||||
defer func() {
|
||||
log.Println("[DEBUG] shutdown sequence complete")
|
||||
}()
|
||||
|
||||
go httpServer(config.WebSrvIP, config.WebSrvPort)
|
||||
|
||||
forever()
|
||||
}
|
Reference in New Issue
Block a user