Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
.secret
stats.json
client/config.yml
client/config.yml
config.yml
25 changes: 3 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,8 @@ and instead break things up into smaller chunks, maybe moving this to it's own
GitHub / Gitlab org, which will allow it to be much more modular and open ended
when it comes to what kind of information you want to monitor and how.

### Steps
### Polling

1. Rewriting the core functionality, which is a basic stats dashboard
and server management through an admin interface. We also want to rethink how to
handle Active Online Reporting - websockets still _seems_ like the best option
for this but there's got to be a better way. Might opt for Go master server
side as it's something I have experience in and should offer good performance etc.

2. Rebuild the client based on the specifications of #1, and deciding the best
way to build and distribute the package w/ configuration - I personally want to
go with something we can compile into a very small package and ship with a
master server-generated configuration file of some sort. Was thinking C++ might
be a good option over Go size wise but there could be additional time overhead.

3. Write a straightforward API both client (or "node") side, which would allow
applications to construct and send custom messages to the master server, and
master server side, which will handle said customer messages. This should be fairly
straightforward once we have #1 and #2 complete.

4. A plugin system - I don't really know what form this would take since it's pretty
far down the line, but it's something to consider. Some sort of simple scripting
language that we can easily write an interpreter for in Go for the master server and
would expose various variables / functions.
There is an optional route that works well with plugins - the `/poll` endpoint.

Requires a few headers
2 changes: 2 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
)

type UsageStats struct {
Type string `json:"type"`
Hostname string `json:"hostname"`
Cpu float64 `json:"cpu"`
Memory float64 `json:"memory"`
Expand Down Expand Up @@ -106,6 +107,7 @@ func main() {
// Fetch usage stats using gopsutil.
func GetStats() (UsageStats, error) {
stats := UsageStats{
Type: "server",
}
diskUsage, err := disk.Usage("/")
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion config.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
port: 9090
interval: 1
interval: 1
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"github.com/gmemstr/platypus/common"
pluginhandler "github.com/gmemstr/platypus/pluginhandler"
"github.com/gmemstr/platypus/router"
"github.com/gmemstr/platypus/stats"
"github.com/go-yaml/yaml"
Expand All @@ -17,6 +18,7 @@ import (

func main() {
GenFiles()
pluginhandler.RegisterPlugins()

file, err := ioutil.ReadFile("config.yml")
if err != nil {
Expand Down
132 changes: 132 additions & 0 deletions pluginhandler/pluginhandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package pluginhandler

import (
"fmt"
"github.com/containous/yaegi/interp"
"github.com/containous/yaegi/stdlib"
"github.com/go-yaml/yaml"
"io/ioutil"
"regexp"
"strings"
)

var Plugins map[string] Plugin
// @TODO: This will cache for available hooks for a hook.
var PluginsForHooks map[string] HookFunc

type Plugin struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Homepage string `yaml:"homepage"`
ImplementsHooks []HookFunc
}

type HookFunc struct {
Type string
Name string
Func string
Contents string
}

// Searches through plugins directory and registers plugins with a valid
// plugin.yml file. Should only be run at startup.
// @TODO: Investigate reloading?
func RegisterPlugins() {
Plugins = make(map[string]Plugin)

plugins, err := ioutil.ReadDir("plugins")
if err != nil {
fmt.Println("Unable to read plugins directory")
return
}

for _, plugin := range plugins {
pluginName := plugin.Name()
registeredPlugin, ok := Plugins[pluginName]
// Already registered.
if ok {
continue
}

registeredPlugin = Plugin{}
pluginInfo, err := ioutil.ReadFile("plugins/" + pluginName + "/plugin.yml")
err = yaml.Unmarshal(pluginInfo, &registeredPlugin)
// Malformed plugin.yml, @TODO log this somewhere for debugging
if err != nil {
continue
}

pluginContents, err := ioutil.ReadFile("plugins/" + pluginName + "/plugin.go")
if err != nil {
continue
}
pluginContent := string(pluginContents)

hookRe := regexp.MustCompile(`((func)\s\w+\(.*\)(\s|.)*{(\s|.)*})`)
funcs := hookRe.FindStringSubmatch(pluginContent)

for range funcs {
re := regexp.MustCompile(`((func)\s\w+)`)
foundFuncName := re.FindStringSubmatch(pluginContent)
realFuncName := strings.TrimLeft(foundFuncName[1], "func ")

hookFunc := HookFunc{
Type: realFuncName,
Func: pluginName + "." + realFuncName,
Contents: pluginContent,
}

registeredPlugin.ImplementsHooks = append(registeredPlugin.ImplementsHooks, hookFunc)
}
Plugins[pluginName] = registeredPlugin
fmt.Println("Registered plugin " + pluginName)
}

}

// Loop through registered plugins and execute any that match the hook.
func ExecuteHook(original string, hook string) string {
data := original
for _, plugin := range Plugins {
for _, hookImplementor := range plugin.ImplementsHooks {
if hookImplementor.Type == hook {
// @TODO: Handle errors :(
data, err := executePlugin(original, hookImplementor)
if err != nil {
fmt.Println(err.Error())
}
if data == "" {
continue
}
}
}
}
// If the plugins returned us nothing, return our original data.
if data == "" {
return original
}
return data
}

// Execute a plugins hook function, and return the string result.
func executePlugin(data string, hook HookFunc) (string, error) {
result := data
interpreter := interp.New(interp.Options{})
interpreter.Use(stdlib.Symbols)
_, err := interpreter.Eval(hook.Contents)
if err != nil {
return result, err
}
function, err := interpreter.Eval(hook.Func)
if err != nil {
return "", err
}

callable := function.Interface().(func(string) (string, error))
result, err = callable(data)
if err != nil {
return "", err
}

return result, nil
}
9 changes: 9 additions & 0 deletions plugins/hello_world/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hello_world

import "fmt"

func IncomingData(data string) (string, error) {
fmt.Println("Hello world! from a plugin")

return "", nil
}
3 changes: 3 additions & 0 deletions plugins/hello_world/plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name:
homepage:
version:
1 change: 1 addition & 0 deletions plugins/telegram/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
config.yml
47 changes: 47 additions & 0 deletions plugins/telegram/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package telegram

import (
"bufio"
"fmt"
"net/http"
"net/url"
"os"
)

type TelegramConfiguration struct {
ApiKey string `yaml:"apikey"`
Channels string `yaml:"channels"`
}

func Offline(data string) (string, error) {
file, err := os.Open("plugins/telegram/config.yml")
if err != nil {
return "", err
}

var configStrings []string
scanner := bufio.NewScanner(file)
for scanner.Scan() {
configStrings = append(configStrings, scanner.Text())
}
config := TelegramConfiguration{
ApiKey: configStrings[0],
Channels: configStrings[1],
}

requestData := fmt.Sprintf(`?chat_id=%v&text=%v`,
config.Channels, url.QueryEscape(data + " just went offline!"))
requestUrl := "https://api.telegram.org/bot" + config.ApiKey

request, err := http.NewRequest("GET", requestUrl + "/sendMessage" + requestData, nil)
if err != nil {
return "", err
}
request.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(request)
defer resp.Body.Close()

return "", nil
}
3 changes: 3 additions & 0 deletions plugins/telegram/plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: "Telegram Bot"
homepage: "https://github.com/gmemstr/Platypus"
version: 0.0.1
46 changes: 46 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package router

import (
"bytes"
"encoding/json"
"fmt"
"github.com/gmemstr/platypus/common"
"github.com/gmemstr/platypus/pluginhandler"
"github.com/gmemstr/platypus/stats"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"io/ioutil"
"log"
"net/http"
"time"
Expand Down Expand Up @@ -54,6 +58,10 @@ func Init() *mux.Router {
StatsWs(),
)).Methods("GET")

r.Handle("/poll", Handle(
handlePoll(),
)).Methods("GET", "POST")

return r
}

Expand Down Expand Up @@ -120,3 +128,41 @@ func rootHandler() common.Handler {
return common.ReadAndServeFile(file, w)
}
}

func handlePoll() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
authkey := r.Header.Get("X-PLATYPUS-AUTH")
appType := r.Header.Get("X-PLATYPUS-TYPE")

secretKey, err := ioutil.ReadFile(".secret")
if err != nil {
return nil
}
key := string(secretKey)
if key != authkey {
return nil
}
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
payload := buf.String()

processed := pluginhandler.ExecuteHook(payload, "IncomingData")
s := stats.Server{Custom: processed, Type: appType}

stats.Servers[appType] = s

response := pluginhandler.ExecuteHook("", "OutgoingData")
fmt.Fprintln(w, response)
jsonServers, err := json.MarshalIndent(stats.Servers, "", " ")
if err != nil {
return nil
}
err = ioutil.WriteFile("stats.json", jsonServers, 0644)
if err != nil {
return nil
}

return nil
}
}

Loading