diff --git a/.gitignore b/.gitignore index 607fc22..c1892c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,93 +1,4 @@ -Scripts/aor_output/* -config.json -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject - -Scripts/aor_config\.json +.idea/ +.secret +stats.json +client/config.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6ee8a2b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# Use an official Python runtime as a base image -FROM python:3.6-wheezy - -# Set the working directory to /app -WORKDIR /Platypus - -# Copy the current directory contents into the container at /app -ADD . /Platypus - -# Install any needed packages specified in requirements.txt -RUN pip install -r requirements.txt - -# Make port 80 available to the world outside this container -EXPOSE 8080 - -# Run app.py when the container launches -CMD ["python", "src/Tornado.py"] \ No newline at end of file diff --git a/README.md b/README.md index ed177d7..6eefe8d 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,70 @@ - ![https://img.shields.io/badge/Status-v3.0%20In%20Progress-green.svg](https://img.shields.io/badge/Status-v3.0%20In%20Progress-green.svg) - # Platypus +## Simple realtime server monitoring + +[![https://img.shields.io/badge/demo-status.gmem.ca-black.svg](https://img.shields.io/badge/demo-status.gmem.ca-black.svg?style=for-the-badge)](https://status.gmem.ca) +[![https://img.shields.io/badge/frontend-gmemstr%2Fplatypus--react-blue.svg](https://img.shields.io/badge/frontend-gmemstr%2Fplatypus--react-blue.svg?style=for-the-badge)](https://github.com/gmemstr/platypus-react) + +### Dependencies + +```bash +go get github.com/gorilla/mux +go get github.com/gorilla/websocket +go get github.com/go-yaml/yaml +go get github.com/shirou/gopsutil +``` + +### Usage + +Master server: +```bash +go build -o platypus main.go +chmod +x platypus +./platypus +``` + +Client servers: +```bash +go build -o platypus_client client/client.go +chmod +x platypus_client +nano config.yml +# Input your secret key and master server IP here, secret key found on master server in .secret +# master: example.com +# secret: s3cr3tk3y +# End config +./platypus_client +``` + +Navigate to your master server and check out the stats. -[Live Stable Branch](https://status.ggserv.xyz) +## Rewrite -Active online and usage monitor using websockets and Python +Rewriting this from the ground up. Why did I do this in Python. +The goal of the rewrite is to move away from using Python for the entire stack +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. -## Features - - Setup script - - Websocket-based uptime monitoring (**AOR**) - - Auto-post to Slack when server goes offline - - Provide live server usage statistics with web frontend - - Simple JSON API for building apps - - Admin interface for managing servers +### Steps -## Getting Started +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. -Please see the wiki page [Getting Started](https://github.com/gmemstr/Platypus/wiki/Getting-Started) +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. -## Requirements - - Python 3.x (2.x not officially supported) - - pip - - `pip install -r requirements.txt` - - MariaDB or MySQL +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. -## Running -See [Getting Started](https://github.com/gmmemstr/Platypus/wiki/Getting-Started) +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. \ No newline at end of file diff --git a/Scripts/.dockerignore b/Scripts/.dockerignore deleted file mode 100644 index 623a110..0000000 --- a/Scripts/.dockerignore +++ /dev/null @@ -1,90 +0,0 @@ -Migration/ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject diff --git a/Scripts/Dockerfile b/Scripts/Dockerfile deleted file mode 100644 index d204aaa..0000000 --- a/Scripts/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -# Use an official Python runtime as a base image -FROM python:slim - -# Set the working directory to /aor -WORKDIR /aor - -# Copy the current directory contents into the container at /aor -ADD . /aor - -# Install any needed packages specified in requirements.txt -RUN pip install -r requirements.txt - -# Run app.py when the container launches -CMD ["python", "aor.py"] \ No newline at end of file diff --git a/Scripts/Migration/convert-to-serverhandler.py b/Scripts/Migration/convert-to-serverhandler.py deleted file mode 100644 index 9e39a2d..0000000 --- a/Scripts/Migration/convert-to-serverhandler.py +++ /dev/null @@ -1,37 +0,0 @@ -import MySQLdb -import json -import uuid - -db = MySQLdb.connect(user="root", db="platypus") -c = db.cursor() - -c.execute("SELECT * FROM server") -dbdata = c.fetchall() - -servers = {} - -with open("src/cache/data.json") as data_file: - cache = json.load(data_file) - - -c.execute( - "ALTER TABLE `server` ADD COLUMN IF NOT EXISTS `ip` VARCHAR(50) NULL," + - "ADD COLUMN IF NOT EXISTS `uuid` VARCHAR(50) NULL") - -db.commit() - -for server in dbdata: - id = server[0] - servers[id] = { - "online": True - } - - uid = str(uuid.uuid4()) - print(id, server[2], "registred with uuid", uid) - c.execute("UPDATE server SET uuid=%s where id=%s", (uid, id)) - db.commit() - -with open("src/cache/data.json", "w") as data_file: - json.dump(servers, data_file, indent=4) - -print("Done") diff --git a/Scripts/Migration/json-to-mysql.py b/Scripts/Migration/json-to-mysql.py deleted file mode 100644 index b419d38..0000000 --- a/Scripts/Migration/json-to-mysql.py +++ /dev/null @@ -1,14 +0,0 @@ -# Used for migrating to MariaDB/MySQL database, -# commited just in case you want to use it yourself. -import json -import MySQLdb - -db = MySQLdb.connect(user="root", db="platypus") -c=db.cursor() -servers = json.load(open("src/cache/servers.json")) - -for s in servers: - c.execute("INSERT INTO server (id,name,hostname,location) VALUES (%s, %s, %s, %s)", - (s['id'], s['name'], s['hostname'], s['location'])) - -db.commit() \ No newline at end of file diff --git a/Scripts/Migration/register-ips.py b/Scripts/Migration/register-ips.py deleted file mode 100644 index e98e7ba..0000000 --- a/Scripts/Migration/register-ips.py +++ /dev/null @@ -1,15 +0,0 @@ -import socket -import MySQLdb - -db = MySQLdb.connect(user="root", db="platypus") -c = db.cursor() - -c.execute("SELECT * FROM server") -dbdata = c.fetchall() - -for server in dbdata: - ip = socket.gethostbyname(server[2]) - c.execute("UPDATE server SET IP=%s where id=%s", (ip, server[0])) - db.commit() - -print("Done") diff --git a/Scripts/aor.py b/Scripts/aor.py deleted file mode 100644 index aebd796..0000000 --- a/Scripts/aor.py +++ /dev/null @@ -1,66 +0,0 @@ -import websocket -import json -import psutil -import uuid -from time import sleep - -glblws = None - -with open("aor_config.json") as data_file: - config = json.load(data_file) - - -def GetStats(): - s = {} - s["cpu"] = round(psutil.cpu_percent()) # Used CPU - s["memory"] = round(psutil.virtual_memory().percent) # Used memory - s["disk"] = round(psutil.disk_usage('C:\\').percent) # Used disk - return s - - -def on_message(ws, message): - raw = json.loads(message) - if raw["success"] is True: - print("DEBUG", "SENDING DATA") - stats = GetStats() - data = { - "masterkey": config["masterkey"], - "uuid": config["uuid"], - "stats": { - "cpu": stats["cpu"], - "disk": stats["disk"], - "memory": stats["memory"] - } - } - j = json.dumps(data) - ws.send(j) - sleep(config["interval"]) - - -def on_error(ws, error): - # print(error) - pass - - -def on_close(ws): - print("Master has gone away") - - -def on_open(ws): - glblws = ws - print("Connected to master") - - -if __name__ == "__main__": - if config["uuid"] is None or config["uuid"] == "": - config["uuid"] = str(uuid.uuid4()) - with open('aor_config.json', 'w+') as data_file: - json.dump(config, data_file, indent=4) - - ws = websocket.WebSocketApp("ws://%s/aor" % config["master_url"], - on_message=on_message, - on_error=on_error, - on_close=on_close) - ws.on_open = on_open - - ws.run_forever() diff --git a/Scripts/aor_config.json b/Scripts/aor_config.json deleted file mode 100644 index 6316da1..0000000 --- a/Scripts/aor_config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "uuid": "73916a0b-a037-4ed4-be79-78c29ef95460", - "masterkey": "aaa", - "master_url": "localhost:8080", - "interval": 5 -} \ No newline at end of file diff --git a/Scripts/requirements.txt b/Scripts/requirements.txt deleted file mode 100644 index 8853cfa..0000000 --- a/Scripts/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -psutil==5.2.1 -websocket-client==0.40.0 \ No newline at end of file diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..4bea6ce --- /dev/null +++ b/client/client.go @@ -0,0 +1,126 @@ +package main + +import ( + "encoding/json" + "github.com/go-yaml/yaml" + "github.com/gorilla/websocket" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/mem" + "io/ioutil" + "log" + "math" + "net/url" + "os" + "time" +) + +type UsageStats struct { + Hostname string `json:"hostname"` + Cpu float64 `json:"cpu"` + Memory float64 `json:"memory"` + Disk float64 `json:"disk"` + Secret string `json:"secret"` +} + +type Configuration struct { + Master string `yaml:"master"` + Secret string `yaml:"secret"` +} + +func main() { + conf := Configuration{} + file, err := ioutil.ReadFile("config.yml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(file, &conf) + if err != nil { + panic(err) + } + + if err != nil { + panic(err) + } + + addr := conf.Master + u := url.URL{Scheme: "ws", Host: addr, Path: "/stats"} + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + panic(err) + } + defer func() { + err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + log.Println("write close:", err) + } + c.Close() + }() + + done := make(chan struct{}) + go func() { + defer close(done) + for { + _, message, err := c.ReadMessage() + if err != nil { + break + } + log.Printf("recv: %v", message) + } + }() + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-done: + return + case <-ticker.C: + stats, err := GetStats() + if err != nil { + break + } + stats.Secret = conf.Secret + statsJson, err := json.Marshal(stats) + if err != nil { + break + } + err = c.WriteMessage(websocket.TextMessage, statsJson) + if err != nil { + log.Println("write:", err) + return + } + } + } +} + +// Fetch usage stats using gopsutil. +func GetStats() (UsageStats, error) { + stats := UsageStats{ + } + diskUsage, err := disk.Usage("/") + if err != nil { + return stats, err + } + stats.Disk = math.Round(diskUsage.UsedPercent) + + cpuUsage, err := cpu.Percent(0, false) + if err != nil { + return stats, err + } + stats.Cpu = math.Round(cpuUsage[0]) + + memUsage, err := mem.VirtualMemory() + if err != nil { + return stats, err + } + stats.Memory = math.Round(memUsage.UsedPercent) + + hostname, err := os.Hostname() + if err != nil { + return stats, err + } + stats.Hostname = hostname + return stats, nil +} diff --git a/common/common.go b/common/common.go new file mode 100644 index 0000000..a628b89 --- /dev/null +++ b/common/common.go @@ -0,0 +1,63 @@ +package common + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" +) + +// RouterContext contains any information to be shared with middlewares. +type RouterContext struct{} + +// Handler is the signature of HTTP Handler that is passed to Handle function +type Handler func(rc *RouterContext, w http.ResponseWriter, r *http.Request) *HTTPError + +type HTTPError struct { + // Message to log in console + Message string + // Status code that'll be sent in response + StatusCode int +} + +type Configuration struct { + Port int `yaml:"port"` + Interval int `yaml:"interval"` +} + +var Config Configuration + +// ReadAndServeFile reads the file from specified location and sends it in response +func ReadAndServeFile(name string, w http.ResponseWriter) *HTTPError { + f, err := os.Open(name) + if err != nil { + + if os.IsNotExist(err) { + return &HTTPError{ + Message: fmt.Sprintf("%s not found", name), + StatusCode: http.StatusNotFound, + } + } + + return &HTTPError{ + Message: fmt.Sprintf("error in reading %s: %v\n", name, err), + StatusCode: http.StatusInternalServerError, + } + } + + defer f.Close() + stats, err := f.Stat() + if err != nil { + log.Printf("error in fetching %s's stats: %v\n", name, err) + } else { + w.Header().Add("Content-Length", strconv.FormatInt(stats.Size(), 10)) + } + + _, err = io.Copy(w, f) + if err != nil { + log.Printf("error in copying %s to response: %v\n", name, err) + } + return nil +} diff --git a/config.json b/config.json deleted file mode 100644 index ea7e047..0000000 --- a/config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "comments": [ - "This is the config file for Platypus.", - "These comments are ignored. If you need", - "any guidance see the README", - "Default password is p1atyPus, run python setup.py to", - "generate a new one." - ], - "master_key": "aaa", - "enable_slackbot": false, - "enable_webserver": true, - "webserver_port": 8080, - "slack_api_key": "", - "slack_channel": "", - "admin_username": "admin", - "admin_password": "$2b$12$5dv0lbCdMhfMniKQ/Fc80O2XXdvCqRpNfv8Cw7Nal8tRxV8SIb4zO", - "sql_user": "root", - "sql_host": "localhost", - "sql_pass": "", - "sql_db": "server", - "company_name": "Platypus" -} \ No newline at end of file diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..c0ce275 --- /dev/null +++ b/config.yml @@ -0,0 +1,2 @@ +port: 9090 +interval: 1 diff --git a/main.go b/main.go new file mode 100644 index 0000000..c3bee4c --- /dev/null +++ b/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/gmemstr/platypus/common" + "github.com/gmemstr/platypus/router" + "github.com/gmemstr/platypus/stats" + "github.com/go-yaml/yaml" + "io/ioutil" + "log" + "net/http" + "os" +) + +func main() { + GenFiles() + + file, err := ioutil.ReadFile("config.yml") + if err != nil { + panic(err) + } + err = yaml.Unmarshal(file, &common.Config) + if err != nil { + panic(err) + } + + // Repopulate servers from cache file. + statsCache, err := ioutil.ReadFile("stats.json") + if err != nil { + panic(err) + } + err = json.Unmarshal(statsCache, &stats.Servers) + if err != nil { + panic(err) + } + // Cache server stats in event master goes down. + defer func() { + jsonServers, err := json.MarshalIndent(stats.Servers, "", " ") + if err != nil { + panic(err) + } + err = ioutil.WriteFile("stats.json", jsonServers, 0644) + if err != nil { + panic(err) + } + }() + + // Start up server. + r := router.Init() + fmt.Println("Your Platytpus instance is live on port :9090") + log.Fatal(http.ListenAndServe(":9090", r)) +} + +// Generate barebones files required to run. +func GenFiles() { + if _, err := os.Stat(".secret"); os.IsNotExist(err) { + fmt.Println("Generating secret key to .secret, use this to configure your servers") + SecretKey() + } + if _, err := os.Stat("stats.json"); os.IsNotExist(err) { + err = ioutil.WriteFile("stats.json", []byte("{}"), 0644) + if err != nil { + panic(err) + } + } + if _, err := os.Stat("config.yml"); os.IsNotExist(err) { + err = ioutil.WriteFile("config.yml", []byte("port: 9090\ninterval: 5\n"), 0644) + if err != nil { + panic(err) + } + } +} + +func SecretKey() { + key, err := GenerateRandomString(32) + if err != nil { + panic(err) + } + err = ioutil.WriteFile(".secret", []byte(key), 0644) + if err != nil { + panic(err) + } +} + +// From https://stackoverflow.com/questions/32349807/how-can-i-generate-a-random-int-using-the-crypto-rand-package +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // Note that err == nil only if we read len(b) bytes. + if err != nil { + return nil, err + } + + return b, nil +} + +// GenerateRandomString returns a URL-safe, base64 encoded +// securely generated random string. +func GenerateRandomString(s int) (string, error) { + b, err := GenerateRandomBytes(s) + return hex.EncodeToString(b), err +} diff --git a/platypus b/platypus new file mode 100755 index 0000000..59fe26b Binary files /dev/null and b/platypus differ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d140994..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -bcrypt==3.1.3 -cffi==1.10.0 -mysqlclient==1.3.10 -pycparser==2.17 -requests==2.13.0 -six==1.10.0 -slackclient==1.0.5 -tornado==4.4.2 - diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..56298ec --- /dev/null +++ b/router/router.go @@ -0,0 +1,122 @@ +package router + +import ( + "fmt" + "github.com/gmemstr/platypus/common" + "github.com/gmemstr/platypus/stats" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + "log" + "net/http" + "time" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func Handle(handlers ...common.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + rc := &common.RouterContext{} + for _, handler := range handlers { + err := handler(rc, w, r) + if err != nil { + log.Printf("%v", err) + + w.Write([]byte(http.StatusText(err.StatusCode))) + + return + } + } + }) +} + +func Init() *mux.Router { + + r := mux.NewRouter() + + // "Static" paths + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("web/static")))) + + // Paths that require specific handlers + r.Handle("/", Handle( + rootHandler(), + )).Methods("GET") + + r.Handle("/stats", Handle( + stats.Handler(), + )).Methods("GET") + + r.Handle("/getstats", Handle( + StatsWs(), + )).Methods("GET") + + return r +} + +func StatsWs() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + panic(err) + } + defer c.Close() + s := stats.Servers + err = c.WriteJSON(s) + if err != nil { + c.Close() + } + done := make(chan struct{}) + go func() { + defer close(done) + for { + _, message, err := c.ReadMessage() + if err != nil { + break + } + log.Printf("recv: %v", message) + } + }() + + interval := time.Duration(common.Config.Interval) * time.Second + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-done: + return nil + case <-ticker.C: + s := stats.Servers + err = c.WriteJSON(s) + if err != nil { + break + } + } + } + + return nil + } +} + +func rootHandler() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + + var file string + switch r.URL.Path { + case "/": + w.Header().Set("Content-Type", "text/html") + file = "web/index.html" + default: + return &common.HTTPError{ + Message: fmt.Sprintf("%s: Not Found", r.URL.Path), + StatusCode: http.StatusNotFound, + } + } + + return common.ReadAndServeFile(file, w) + } +} diff --git a/setup.py b/setup.py deleted file mode 100644 index 8eed138..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -import MySQLdb -from src.Config import Config -import bcrypt - -config = Config() - -if __name__ == "__main__": - print("Platypus setup utility") - print("SQL Database Setup") - sql_host = input("Host: ") - sql_user = input("User: ") - sql_password = input("Password: ") - sql_database = input("Table: ") - - con = MySQLdb.connect(host=sql_host, user=sql_user, passwd=sql_password) - c = con.cursor() - - c.execute("CREATE DATABASE IF NOT EXISTS platypus") - c.execute("USE platypus") - c.execute("CREATE TABLE IF NOT EXISTS " + sql_database + - " (id int NOT NULL AUTO_INCREMENT," + - "name varchar(120), hostname varchar(120)," + - "online boolean DEFAULT true, ip varchar(50), uuid varchar(50)" + - " PRIMARY KEY(id))") - con.commit() - - print("SQL Databse created, moving along.") - print("Admin user creation") - admin_user = input("Username: ") - admin_password = input("Password: ") - admin_password2 = input("Confirm Password: ") - - if admin_password == admin_password2: - admin_password2 = admin_password2.encode("utf8") - hashed = bcrypt.hashpw(admin_password2, bcrypt.gensalt()) - else: - print("Passwords did not match. Your password:", admin_password2) - - print("Slackbot defaulting to off") - print("Webserver defaulting to on") - print("Setting config") - # Set SQL Config - config.Set("sql_user", sql_user) - config.Set("sql_pass", sql_password) - config.Set("sql_host", sql_host) - config.Set("sql_db", sql_database) - # Set Aadmin Config - config.Set("admin_username", admin_user) - config.Set("admin_password", str(hashed).strip("b'")) - # Finished - print("Further configuration can be found in config.json") diff --git a/src/Aor.py b/src/Aor.py deleted file mode 100644 index 02307ef..0000000 --- a/src/Aor.py +++ /dev/null @@ -1,91 +0,0 @@ -import tornado.ioloop -import tornado.websocket -import json -import ServerHandler -import Config - -config = Config.Config() -sql = ServerHandler.Sql() -AliveSockets = set() - - -class Aor(tornado.websocket.WebSocketHandler): - - def open(self): - print(self.request.remote_ip, - "connected to AOR, awaiting authentication") - - try: - print(self.request.remote_ip) - self.server = sql.Ip(self.request.remote_ip) - self.write_message('{"success":true,"require_auth":"true"}') - except: - print(self.request.remote_ip, - "socket closed, unable to find IP in database") - self.close() - - def on_message(self, message): - message = json.loads(message) - if message["masterkey"] != config.Get("master_key"): - self.close() - print("Server gave us an invalid master key") - - else: - try: - message = json.loads(message) - except: - self.write_message('{"success":false,"message":"invalid_message"}') - - if self.server[0] != None: - j = json.dumps({ - "id": self.server[0], - "online": True, - "cpu": message["stats"]["cpu"], - "disk": message["stats"]["disk"], - "memory": message["stats"]["memory"] - }) - SendFetchMessage(j) - self.write_message('{"success":true,"message":"recieved_stats"}') - - else: - if message["masterkey"] == config.Get("master_key"): - print(self.server[1], "registered uuid", message["uuid"]) - sql.Register(self.server, message["uuid"]) - self.write_message( - '{"success":true,"message":"registered_uuid"}') - self.server[3] = message["uuid"] - - def on_close(self): - if self.server is not None: - print(self.server[1], "disconnected, assuming offline!") - j = json.dumps({ - "id": self.server[0], - "online": False, - "cpu": 0, - "disk": 0, - "memory": 0 - }) - SendFetchMessage(j) - - -class FetchWebsocket(tornado.websocket.WebSocketHandler): - - def open(self): - AliveSockets.add(self) - print("Fetch websocket opened - client " + self.request.remote_ip) - - def on_message(self, message): - print(self.request.remote_ip + " requested panel " + message) - # res = scan.Fetch(message) - res = "placeholder" - self.write_message(res) - print(self.request.remote_ip + " was sent " + message) - - def on_close(self): - AliveSockets.remove(self) - print("Fetch websocket closed - client " + self.request.remote_ip) - - -def SendFetchMessage(message): - for ws in AliveSockets: - ws.write_message(message) diff --git a/src/Config.py b/src/Config.py deleted file mode 100644 index 0f3b7b1..0000000 --- a/src/Config.py +++ /dev/null @@ -1,23 +0,0 @@ -# Fetch / set config values. -# Seperate from Cache.py however -# the two may be merged into a single -# file at some point thanks to classes -import json - -class Config: - def __init__(self): - with open('config.json') as config: - self.config = json.load(config) - - def Get(self, property): - if(property == "*"): - return self.config - else: - return self.config[property] - def Set(self, property, newvalue): - self.config[property] = newvalue - - with open('config.json', 'w') as config: - json.dump(self.config, config, indent=4) - - return True \ No newline at end of file diff --git a/src/ServerHandler.py b/src/ServerHandler.py deleted file mode 100644 index b814e2e..0000000 --- a/src/ServerHandler.py +++ /dev/null @@ -1,146 +0,0 @@ -# This module manages the data on the servers. It: -# - (sql) Handles server registration w/ uid -# - (sql) Verifies server uid -# - (sql) Deletes servers -# - (sql) Get server metadata from database -# - (cache) Store server stats for API / non-ws suported browsers -# - (cache) Delivers server stats - -import MySQLdb -import Config -import json - - -class Sql: - - def __init__(self): - self.config = Config.Config() - - self.sqluser = self.config.Get("sql_user") - self.sqlpass = self.config.Get("sql_pass") - self.sqltable = self.config.Get("sql_db") - - self.db = MySQLdb.connect( - user=self.sqluser, passwd=self.sqlpass, db="platypus") - self.c = self.db.cursor() - - # Check connection with database - def CheckConnection(self): - try: - self.db.ping() - # Else reconnect - except MySQLdb.MySQLError: - self.db = MySQLdb.connect( - user=self.sqluser, passwd=self.sqlpass, db="platypus") - - # Register server with IP and uid - def Register(self, server, uid): - self.CheckConnection() - - self.c.execute("UPDATE " + self.sqltable + " SET uuid = %s WHERE id=%s", - (uid, server[0])) - self.db.commit() - - print("Server registered", uid) - # Verify server has correct uid - - def New(self, name, hostname, ip): - self.CheckConnection() - - self.c.execute("INSERT INTO `" + self.sqltable + "` (name,hostname,ip) VALUES (%s,%s,%s)", - (name, hostname, ip)) - self.db.commit() - - def Delete(self, id): - self.CheckConnection() - - self.c.execute("DELETE FROM `" + self.sqltable + - "` WHERE id=%s" % int(id)) - - self.db.commit() - - def Verify(self, uid): - self.CheckConnection() - - self.c.execute("SELECT * FROM `" + self.sqltable + - "` WHERE uuid='%s'" % uid) - dbdata = self.c.fetchone() - - if dbdata is None: - return False - if dbdata[10] == uid: - return True - else: - return False - - # Get server data from database - def Get(self, id="*"): - - self.CheckConnection() - if id != "*": - self.c.execute( - "SELECT id,name,hostname,ip FROM `" + - self.sqltable + "` WHERE id=%s" % id) - dbdata = self.c.fetchone() - else: - print(self.sqltable) - self.c.execute("SELECT id,name,hostname,ip FROM `" + - self.sqltable + "`") - dbdata = self.c.fetchall() - - return dbdata - - def Uuid(self, uid): - self.CheckConnection() - - self.c.execute("SELECT id,name,hostname,ip FROM `" + self.sqltable + - "` WHERE uuid='%s'" % uid) - dbdata = self.c.fetchone() - - return dbdata - - def Ip(self, ip): - self.CheckConnection() - - try: - self.c.execute("SELECT id,name,hostname,uuid FROM `" + - self.sqltable + "` WHERE ip='%s'" % ip) - dbdata = self.c.fetchone() - print(dbdata) - return dbdata - - except MySQLdb.DataError: - raise ValueError("IP of server not registered") - - -class Cache: - - def __init__(self): - with open("src/cache/data.json", "r") as data_file: - self.cache = json.load(data_file) - - def Update(self, id, stats): - id = str(id) - self.cache[id] = stats - self.__dump() - - def TriggerOffline(self, id): - self.cache[id]["online"] = False - self.__dump() - - def TriggerOnline(self, id): - self.cache[id]["online"] = True - self.__dump() - - def Fetch(self): - with open("src/cache/data.json", "r") as data_file: - res = json.load(data_file) - - return res - - def __dump(self): - with open('src/cache/data.json', 'w+') as data_file: - json.dump(self.cache, data_file, indent=4) - - with open("src/cache/data.json", "r") as data_file: - self.cache = json.load(data_file) diff --git a/src/Slackbot.py b/src/Slackbot.py deleted file mode 100644 index 6078557..0000000 --- a/src/Slackbot.py +++ /dev/null @@ -1,23 +0,0 @@ -from slackclient import SlackClient -from Config import Config -import threading - -config = Config() -channel = config.Get("slack_channel") -token = config.Get("slack_api_key") -sc = SlackClient(token) - - -class Bot: - - def Post(self, message, icon): - return sc.api_call( - "chat.postMessage", channel=config.Get("slack_channel"), text=message, - username="Platypus", icon_emoji=icon) - - def SingleReport(self, name, hostname, status): - if config.Get("enable_slackbot"): - message = "%s (%s) has just gone %s!", (name, hostname, status) - self.Post(message, ":exclamation:") - else: - return 0 diff --git a/src/Tornado.py b/src/Tornado.py deleted file mode 100644 index 3c47c33..0000000 --- a/src/Tornado.py +++ /dev/null @@ -1,109 +0,0 @@ -import tornado.ioloop -import tornado.web -import tornado.websocket -import socket -import bcrypt -from secrets import token_urlsafe -import ServerHandler -import Aor -import Config - -sql = ServerHandler.Sql() -cache = ServerHandler.Cache() -config = Config.Config() - - -class BaseHandler(tornado.web.RequestHandler): - - def get_current_user(self): - return self.get_secure_cookie("i") - - -class MainHandler(tornado.web.RequestHandler): - - def get(self): - self.render("templates/index.html", - company=config.Get("company_name"), - servers=sql.Get(), stats=cache.Fetch()) - - -class ResourceHandler(tornado.web.RequestHandler): - - def get(self, resource): - try: - res = open('src/static/' + resource).read() - self.set_header("Content-Type", 'text/css; charset="utf-8"') - self.write(res) - except: - self.write("404 - not found") - - -class LoginManager(tornado.web.RequestHandler): - - def get(self): - self.render("templates/login.html") - - def post(self): - username = self.get_body_argument("username") - password = self.get_body_argument("password").encode('utf8') - admin_password = config.Get("admin_password").encode('utf8') - if username == config.Get("admin_username") and bcrypt.checkpw(password, admin_password): - self.set_secure_cookie("i", token_urlsafe(32)) - self.redirect("/admin") - else: - self.redirect("/login") - - -class AdminInterface(BaseHandler): - - @tornado.web.authenticated - def get(self): - self.render("templates/admin.html", servers=sql.Get()) - - @tornado.web.authenticated - def post(self): - ip = socket.gethostbyname(self.get_body_argument("hostname")) - sql.New(self.get_body_argument("name"), - self.get_body_argument("hostname"), - ip) - self.write("success") - - -class AdminInterfaceDelete(BaseHandler): - - @tornado.web.authenticated - def post(self, id): - sql.Delete(id) - self.write("success") - - -def make_app(): - - settings = { - "cookie_secret": token_urlsafe(32), - "login_url": "/login", - "xsrf_cookies": True, - "debug": True - } - - return tornado.web.Application([ - (r"/", MainHandler), - (r"/static/(.*)", ResourceHandler), - (r"/fetch", Aor.FetchWebsocket), - (r"/login", LoginManager), - (r"/admin", AdminInterface), - (r"/admin/delete/([0-9]+)", AdminInterfaceDelete), - (r"/aor", Aor.Aor) - ], **settings) - - -def run_app(): - port = config.Get("webserver_port") - app = make_app() - app.listen(port) - print("Platypus master listening on port :", port) - - tornado.ioloop.IOLoop.current().start() - - -run_app() diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/cache/data.json b/src/cache/data.json deleted file mode 100644 index 587e20b..0000000 --- a/src/cache/data.json +++ /dev/null @@ -1,392 +0,0 @@ -{ - "3": { - "online": true, - "cpu": 0, - "memory": -300, - "disk": 0 - }, - "6": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 38 - }, - "7": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 23 - }, - "13": { - "online": true, - "cpu": 1, - "memory": -300, - "disk": 43 - }, - "20": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 0 - }, - "34": { - "online": true, - "cpu": 0, - "memory": -300, - "disk": 0 - }, - "38": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 95 - }, - "43": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 30 - }, - "47": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 29 - }, - "49": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 14 - }, - "57": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 9 - }, - "60": { - "online": true, - "cpu": 0, - "memory": 300, - "disk": 0 - }, - "62": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 8 - }, - "64": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 9 - }, - "65": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 5 - }, - "66": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 7 - }, - "67": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 14 - }, - "68": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 23 - }, - "69": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 8 - }, - "71": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 26 - }, - "72": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 12 - }, - "73": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 6 - }, - "77": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 22 - }, - "78": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 8 - }, - "79": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 9 - }, - "80": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 11 - }, - "81": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 20 - }, - "82": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 4 - }, - "83": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 3 - }, - "85": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 11 - }, - "86": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 30 - }, - "87": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 26 - }, - "89": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 21 - }, - "91": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 35 - }, - "93": { - "online": true, - "cpu": 0, - "memory": -300, - "disk": 0 - }, - "94": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 66 - }, - "95": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 17 - }, - "96": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 14 - }, - "97": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 52 - }, - "98": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 21 - }, - "99": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 7 - }, - "100": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 0 - }, - "101": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 0 - }, - "102": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 11 - }, - "103": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 0 - }, - "104": { - "online": true, - "cpu": 1, - "memory": -300, - "disk": 55 - }, - "106": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 7 - }, - "107": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 25 - }, - "109": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 7 - }, - "110": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 14 - }, - "111": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 14 - }, - "112": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 20 - }, - "113": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 18 - }, - "114": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 86 - }, - "115": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 7 - }, - "117": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 9 - }, - "118": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 2 - }, - "119": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 8 - }, - "120": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 22 - }, - "121": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 3 - }, - "123": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 22 - }, - "125": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 76 - }, - "126": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 71 - }, - "127": { - "online": true, - "cpu": 1, - "memory": 300, - "disk": 4 - }, - "1": { - "online": true, - "cpu": 60, - "disk": 1, - "memory": 36 - } -} \ No newline at end of file diff --git a/src/static/Logo-02.png b/src/static/Logo-02.png deleted file mode 100644 index 6fe4cf1..0000000 Binary files a/src/static/Logo-02.png and /dev/null differ diff --git a/src/static/Logo-06.png b/src/static/Logo-06.png deleted file mode 100644 index 4029af8..0000000 Binary files a/src/static/Logo-06.png and /dev/null differ diff --git a/src/static/index.css b/src/static/index.css deleted file mode 100644 index 748d5e8..0000000 --- a/src/static/index.css +++ /dev/null @@ -1,183 +0,0 @@ -@import 'https://fonts.googleapis.com/css?family=Roboto:300,400,500'; -body { - transition: 0.25s; - font-family: 'Roboto', sans-serif; - background-color: #f9f9f9; - font-weight: 300; -} -h1,h2{ - font-weight: 500; -} -.nightmode { - background-color: #1B2836; - color: #C7CBCE; -} -table { - border-collapse: collapse; - width: 100%; -} - -td{ - border: none; - text-align: center; - padding: 8px; - width: 10px; -} -.nightmode td { - border: none; -} -.nightmode strong { - color: #1DA1F2; -} -.toggs { - text-align: center; -} -th { - background: #f9f9f9; text-align: center; padding: 10px; -} -.nightmode th { - background-color: #1B2836; -} -tr:nth-child(even) { - background-color: #f2f2f2; -} -.nightmode tr:nth-child(even) { - background-color: #414F5C; -} -.options { - text-align: center; - font-size: 2vh; -} -h1 { - text-align: center; - font-size: 4vh; -} -h2 { - font-size: 3vh; -} -strong { - color: #f44b42; - font-size: 75%; - font-weight: bolder; -} - -.switch { - position: relative; - display: inline-block; - width: 60px; - height: 12px; -} - -.switch input {display:none;} - -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; -} - -.slider:before { - position: absolute; - content: ""; - height: 5px; - width: 26px; - left: 4px; - bottom: 4px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; -} - -input:checked + .slider { - background-color: #2196F3; -} - -input:focus + .slider { - box-shadow: 0 0 1px #2196F3; -} - -input:checked + .slider:before { - -webkit-transform: translateX(26px); - -ms-transform: translateX(26px); - transform: translateX(26px); -} - -.tooltip { - position: relative; - display: inline-block; -} - -.tooltip .tooltiptext { - visibility: hidden; - width: 240px; - background-color: black; - color: #fff; - text-align: center; - border-radius: 6px; - padding: 5px 0; - position: absolute; - z-index: 1; - top: 150%; - left: 30%; - margin-left: -60px; -} - -.tooltip .tooltiptext::after { - content: ""; - position: absolute; - bottom: 100%; - left: 50%; - margin-left: -5px; - border-width: 5px; - border-style: solid; - border-color: transparent transparent black transparent; -} - -.tooltip:hover .tooltiptext { - visibility: visible; -} - -.loginform { - position: fixed; - top: 30%; - left: 50%; - /* bring your own prefixes */ - transform: translate(-50%, -50%); - padding: 5%; - border:1px solid; - border-radius: 5px; - background-color: white; - box-shadow: 1px 5px 5px #888888; - padding-top: 0; -} - -.loginform label { - display: block; - text-align: center; -} - -.loginform input { - padding: 2%; - width:100%; - border: 1px solid; -} - -.loginform .submit { - margin-top: 10px; - padding:1%; - width: 50%; -} -button, input[type=submit] { - background:none; - background-color: #f9f9f9; - border:1px solid black; -} -#uid { - width: 100%; -} \ No newline at end of file diff --git a/src/static/platypus.js b/src/static/platypus.js deleted file mode 100644 index 16509d3..0000000 --- a/src/static/platypus.js +++ /dev/null @@ -1,54 +0,0 @@ -window.onload = checkNight; -// Function that refreshes each -// panels stats indivudually -var table = document.getElementById("table"); -var i = 0; -var ws = new WebSocket("ws://"+document.location.host+"/fetch"); - - -ws.onmessage = function(evt) { - //console.log(evt.data, panel) - setRow(evt.data); -}; - - -function setRow(text) { - var res = JSON.parse(text); - var panel = res["id"] - console.log("---------- \n" + panel) - var row = document.getElementById(panel); - console.log(row.cells + "\n----------") - //console.log(row); - //console.log(panel) - //console.log(res['online']); - if (res['online'] == false || res['online'] == null) { - row.cells[0].innerHTML = row.cells[0].innerHTML + " OFFLINE"; - row.cells[1].innerHTML = "0%"; - row.cells[2].innerHTML = "0%"; - row.cells[3].innerHTML = "0%"; - } else { - row.cells[1].innerHTML = res['cpu'] + "%"; - row.cells[2].innerHTML = res['memory'] + "%"; - row.cells[3].innerHTML = res['disk'] + "%"; - } -} - -function toggleNight() { - if (localStorage.nightmode != "true") { - document.body.classList.add("nightmode"); - localStorage.setItem("nightmode", true); - } else { - document.body.classList.remove("nightmode"); - localStorage.setItem("nightmode", false); - } -} - -function checkNight() { - if (localStorage.nightmode == "true") { - console.log("Night time"); - document.getElementById("night").checked = true; - document.body.classList.add("nightmode"); - } else { - document.body.classList.remove("nightmode"); - } -} diff --git a/src/templates/admin.html b/src/templates/admin.html deleted file mode 100644 index 08d894b..0000000 --- a/src/templates/admin.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Platypus Admin - - - - -

Platypus Admin

-
-
-

Register Server

-
- {% module xsrf_form_html() %} - - - - - -
-
-

Remove Servers

- - - - - - - - - {% for s in servers %} - - - - - - - {% end %} -
idNameHostnameIP Addr.
{{s[0]}}{{s[1]}}{{s[2]}}{{s[3]}}
- {% module xsrf_form_html() %} - - -
- -
-
- - \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html deleted file mode 100644 index 81d3b2b..0000000 --- a/src/templates/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - {{ company }} | Platypus - - - - - - - - -

{{company}} Server Status

-
- Night mode
- - - - - - {% for s in servers %} - - - - - - - {% end %} -
PanelCPU UsedMemory UsedDisk Used
{{ s[1] }} {% if stats[str(s[0])]['online'] == False %}OFFLINE{% end %}
- - - - - - \ No newline at end of file diff --git a/src/templates/login.html b/src/templates/login.html deleted file mode 100644 index 984fc99..0000000 --- a/src/templates/login.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - Platypus Login - - - -
-

Platypus Admin Login

- {% module xsrf_form_html() %} - - - - - -
- - \ No newline at end of file diff --git a/stats/stats.go b/stats/stats.go new file mode 100644 index 0000000..7bb440f --- /dev/null +++ b/stats/stats.go @@ -0,0 +1,123 @@ +package stats + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/gmemstr/platypus/common" + "github.com/gorilla/websocket" + "io/ioutil" + "net/http" +) + +type UsageStats struct { + Hostname string `json:"hostname"` + Cpu float64 `json:"cpu"` + Memory float64 `json:"memory"` + Disk float64 `json:"disk"` + Secret string `json:"secret"` +} + +type Server struct { + Stats UsageStats `json:"stats"` + Online bool `json:"online"` +} + +var Servers map[string] Server +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +// Handles individual connections, parsing JSON data from client and writing a message. +func Handler() common.Handler { + return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { + hostname := "" + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + panic(err) + } + defer c.Close() + + // @TODO Set up escalation process when socket closes. + c.SetCloseHandler(CloseHandler) + + for { + mt, message, err := c.ReadMessage() + if err != nil { + _ = SetOffline(hostname) + break + } + stats := UsageStats{} + err = json.Unmarshal(message, &stats) + if err != nil { + break + } + hostname = stats.Hostname + secretKey, err := ioutil.ReadFile(".secret") + key := string(secretKey) + if stats.Secret != key { + _ = c.WriteMessage(mt, []byte("invalid secret key")) + _ = c.Close() + } + // Blank out secret key after comparing. + stats.Secret = "" + err = WriteStats(hostname, stats) + if err != nil { + break + } + err = c.WriteMessage(mt, []byte("")) + if err != nil { + break + } + } + + return nil + } +} + +func SetOffline(hostname string) error { + server, ok := Servers[hostname] + if ok { + server.Online = false + Servers[hostname] = server + } + + jsonServers, err := json.MarshalIndent(Servers, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile("stats.json", jsonServers, 0644) + if err != nil { + return err + } + + return nil +} + +func WriteStats(hostname string, stats UsageStats) error { + server, ok := Servers[hostname] + if ok { + server.Stats = stats + server.Online = true + } + if !ok { + server = Server{ + Stats: stats, + Online: true, + } + } + Servers[hostname] = server + + return nil +} + +// Close handler, begin chain of escalation. +func CloseHandler(code int, text string) error { + fmt.Println("ws closed") + if code != 1000 { + return errors.New("websocket closed badly") + } + return nil +} diff --git a/web/asset-manifest.json b/web/asset-manifest.json new file mode 100644 index 0000000..650f5e8 --- /dev/null +++ b/web/asset-manifest.json @@ -0,0 +1,13 @@ +{ + "main.css": "/static/css/main.0508ec29.chunk.css", + "main.js": "/static/js/main.cfe6a328.chunk.js", + "main.js.map": "/static/js/main.cfe6a328.chunk.js.map", + "runtime~main.js": "/static/js/runtime~main.a8a9905a.js", + "runtime~main.js.map": "/static/js/runtime~main.a8a9905a.js.map", + "static/js/2.0f58faaa.chunk.js": "/static/js/2.0f58faaa.chunk.js", + "static/js/2.0f58faaa.chunk.js.map": "/static/js/2.0f58faaa.chunk.js.map", + "index.html": "/index.html", + "precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js": "/precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js", + "service-worker.js": "/service-worker.js", + "static/css/main.0508ec29.chunk.css.map": "/static/css/main.0508ec29.chunk.css.map" +} \ No newline at end of file diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/web/favicon.ico differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..739656d --- /dev/null +++ b/web/index.html @@ -0,0 +1 @@ +Platypus

Platypus Server Status

\ No newline at end of file diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..fc69bb3 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "Platypus", + "name": "React Frontend for Platypus server monitoring", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/web/precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js b/web/precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js new file mode 100644 index 0000000..5ddfdf0 --- /dev/null +++ b/web/precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js @@ -0,0 +1,22 @@ +self.__precacheManifest = [ + { + "revision": "dd5b86ce236c1c4592a3", + "url": "/static/css/main.0508ec29.chunk.css" + }, + { + "revision": "dd5b86ce236c1c4592a3", + "url": "/static/js/main.cfe6a328.chunk.js" + }, + { + "revision": "42ac5946195a7306e2a5", + "url": "/static/js/runtime~main.a8a9905a.js" + }, + { + "revision": "baf961a1263ccd6a2d24", + "url": "/static/js/2.0f58faaa.chunk.js" + }, + { + "revision": "fd847b7f9f8c7d6f94b692d5690fd471", + "url": "/index.html" + } +]; \ No newline at end of file diff --git a/web/service-worker.js b/web/service-worker.js new file mode 100644 index 0000000..257e2f7 --- /dev/null +++ b/web/service-worker.js @@ -0,0 +1,34 @@ +/** + * Welcome to your Workbox-powered service worker! + * + * You'll need to register this file in your web app and you should + * disable HTTP caching for this file too. + * See https://goo.gl/nhQhGp + * + * The rest of the code is auto-generated. Please don't update this file + * directly; instead, make changes to your Workbox build configuration + * and re-run your build process. + * See https://goo.gl/2aRDsh + */ + +importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js"); + +importScripts( + "/precache-manifest.01aedb2ed2ef840338ddc85fd9afd892.js" +); + +workbox.clientsClaim(); + +/** + * The workboxSW.precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ +self.__precacheManifest = [].concat(self.__precacheManifest || []); +workbox.precaching.suppressWarnings(); +workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); + +workbox.routing.registerNavigationRoute("/index.html", { + + blacklist: [/^\/_/,/\/[^\/]+\.[^\/]+$/], +}); diff --git a/web/static/css/main.0508ec29.chunk.css b/web/static/css/main.0508ec29.chunk.css new file mode 100644 index 0000000..cd560df --- /dev/null +++ b/web/static/css/main.0508ec29.chunk.css @@ -0,0 +1,2 @@ +body{background-color:#1b2936;color:#fff;margin:0;padding:0;font-family:sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}h1{text-align:center}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{margin:20px 0;text-align:center;display:-webkit-flex;display:flex;-webkit-justify-content:center;justify-content:center}.Server,.ServersHeading{display:grid;grid-template-columns:repeat(4,1fr);padding:1em}.Server:nth-child(odd){background-color:#414f5c} +/*# sourceMappingURL=main.0508ec29.chunk.css.map */ \ No newline at end of file diff --git a/web/static/css/main.0508ec29.chunk.css.map b/web/static/css/main.0508ec29.chunk.css.map new file mode 100644 index 0000000..6d5aa8b --- /dev/null +++ b/web/static/css/main.0508ec29.chunk.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["/home/gabriel/Projects/platypus-react/src/index.css","main.0508ec29.chunk.css","/home/gabriel/Projects/platypus-react/src/App.css"],"names":[],"mappings":"AAAA,KACE,wBAAA,CACA,UAAA,CACA,QAAA,CACA,SAAA,CACA,sBAAA,CACA,kCAAA,CACA,iCCCF,CDEA,GACE,iBCCF,CDEA,KACE,uECEF,CCjBA,KACE,aAAA,CACA,iBAAA,CACA,oBAAA,CAAA,YAAA,CACA,8BAAA,CAAA,sBDsBF,CCnBA,wBACE,YAAA,CACA,mCAAA,CACA,WDsBF,CCnBA,uBACE,wBDsBF","file":"main.0508ec29.chunk.css","sourcesContent":["body {\n background-color: #1b2936;\n color: #ffffff;\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nh1 {\n text-align: center;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n","body {\n background-color: #1b2936;\n color: #ffffff;\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\nh1 {\n text-align: center;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace;\n}\n\n.App {\n margin: 20px 0;\n text-align: center;\n display: -webkit-flex;\n display: flex;\n -webkit-justify-content: center;\n justify-content: center;\n}\n\n.Server, .ServersHeading {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n padding: 1em;\n}\n\n.Server:nth-child(odd) {\n background-color: #414f5c;\n}\n",".App {\n margin: 20px 0;\n text-align: center;\n display: flex;\n justify-content: center;\n}\n\n.Server, .ServersHeading {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n padding: 1em;\n}\n\n.Server:nth-child(odd) {\n background-color: #414f5c;\n}"]} \ No newline at end of file diff --git a/web/static/js/2.0f58faaa.chunk.js b/web/static/js/2.0f58faaa.chunk.js new file mode 100644 index 0000000..f4c0e80 --- /dev/null +++ b/web/static/js/2.0f58faaa.chunk.js @@ -0,0 +1,2 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(10)},function(e,t,n){"use strict";function r(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}n.d(t,"a",function(){return r})},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}n.d(t,"a",function(){return r})},function(e,t,n){"use strict";function r(e,t){for(var n=0;nz.length&&z.push(e)}function U(e,t,n){return null==e?0:function e(t,n,r,l){var a=typeof t;"undefined"!==a&&"boolean"!==a||(t=null);var u=!1;if(null===t)u=!0;else switch(a){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case i:case o:u=!0}}if(u)return r(l,t,""===n?"."+D(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;cthis.eventPool.length&&this.eventPool.push(e)}function fe(e){e.eventPool=[],e.getPooled=ce,e.release=se}l(ue.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!==typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=oe)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!==typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=oe)},persist:function(){this.isPersistent=oe},isPersistent:ae,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=ae,this._dispatchInstances=this._dispatchListeners=null}}),ue.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},ue.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var i=new t;return l(i,n.prototype),n.prototype=i,n.prototype.constructor=n,n.Interface=l({},r.Interface,e),n.extend=r.extend,fe(n),n},fe(ue);var de=ue.extend({data:null}),pe=ue.extend({data:null}),me=[9,13,27,32],he=$&&"CompositionEvent"in window,ye=null;$&&"documentMode"in document&&(ye=document.documentMode);var ve=$&&"TextEvent"in window&&!ye,ge=$&&(!he||ye&&8=ye),be=String.fromCharCode(32),ke={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},xe=!1;function we(e,t){switch(e){case"keyup":return-1!==me.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Te(e){return"object"===typeof(e=e.detail)&&"data"in e?e.data:null}var _e=!1;var Se={eventTypes:ke,extractEvents:function(e,t,n,r){var l=void 0,i=void 0;if(he)e:{switch(e){case"compositionstart":l=ke.compositionStart;break e;case"compositionend":l=ke.compositionEnd;break e;case"compositionupdate":l=ke.compositionUpdate;break e}l=void 0}else _e?we(e,n)&&(l=ke.compositionEnd):"keydown"===e&&229===n.keyCode&&(l=ke.compositionStart);return l?(ge&&"ko"!==n.locale&&(_e||l!==ke.compositionStart?l===ke.compositionEnd&&_e&&(i=ie()):(re="value"in(ne=r)?ne.value:ne.textContent,_e=!0)),l=de.getPooled(l,t,n,r),i?l.data=i:null!==(i=Te(n))&&(l.data=i),H(l),i=l):i=null,(e=ve?function(e,t){switch(e){case"compositionend":return Te(t);case"keypress":return 32!==t.which?null:(xe=!0,be);case"textInput":return(e=t.data)===be&&xe?null:e;default:return null}}(e,n):function(e,t){if(_e)return"compositionend"===e||!he&&we(e,t)?(e=ie(),le=re=ne=null,_e=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1