11 Commits
Author SHA1 Message Date
aziis98 c86785ecdf feat: added temperature command 2023-12-27 23:30:47 +01:00
aziis98 7ff45091c7 fix: added attribution to various stuff 2023-12-25 10:23:18 +01:00
aziis98 994cff1190 chore: updated github workflow 2023-12-25 10:10:45 +01:00
aziis98 c85ec78793 chore: some updates 2023-12-25 10:05:22 +01:00
aziis98 10938c1d98 chore: updated readme 2023-12-25 01:06:37 +01:00
aziis98 631af1cda6 chore: updated readme 2023-12-25 01:01:37 +01:00
aziis98 c488d41ac0 chore: updated readme 2023-12-25 00:41:27 +01:00
aziis98 0a375c0990 fix: setup also updates binary 2023-12-25 00:26:22 +01:00
aziis98 8069de0cf8 feat: bootstapping and systemd 2023-12-25 00:10:53 +01:00
aziis98 30b92345b0 fix: random changes 2023-12-24 23:41:28 +01:00
aziis98 8ebdb0854d chore: better io handling and minor style enhancements 2023-12-24 23:34:29 +01:00
3 changed files with 266 additions and 63 deletions
+3 -3
View File
@@ -12,12 +12,12 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v4
with: with:
go-version: 1.21 go-version-file: './go.mod'
- name: Build - name: Build
run: | run: |
+55 -32
View File
@@ -1,64 +1,87 @@
# Go Stats Server # Go Stats Server
This Go project implements a simple TCP server that responds to custom commands over the network. The server performs various system-related tasks and provides This Go project implements a simple TCP server that responds to custom commands over an internal network. The server performs various system-related tasks and provides information such as CPU usage, memory status, network statistics, storage details, and system uptime.
information such as CPU usage, memory status, network statistics, storage details, and system uptime.
## Commands
The protocol supports the following commands:
- `cpu` — returns the CPU usage percentage
- `memory` — returns the memory usage in MB
- `network` — returns the network usage in bytes
- `storage` — returns the storage usage in GB
- `uptime` — returns the system uptime in seconds
- `temperature` — returns the CPU temperature of Big and Little cores in Celsius
[TODO: check if the command docs are correct]
## Usage ## Usage
1. **Build the Server:** 1. **Build the Server:**
```bash ```bash shell
CGO_ENABLED=0 GOARCH=<arch> go build -a -ldflags '-s -w' -o ./out/stats-server main.go $ CGO_ENABLED=0 GOARCH=<arch> go build -a -ldflags '-s -w' -o ./out/stats-server main.go
``` ```
2. **Run the Server:** 2. **Run the Server:**
```bash ```bash shell
./out/stats-server $ ./out/stats-server serve
``` ```
3. **Connect to the Server:** Use a TCP client to connect to the server on port 12345. You can send commands like "cpu," "memory," "network," "storage," 3. **Connect to the Server:** Use a TCP client to connect to the server on port 12345. You can send commands like "cpu", "memory", "network", "storage" or
"uptime," and "exit." "uptime".
Example using `nc`: Example using `nc`:
```bash ```bash shell
echo "cpu" | nc localhost 12345 $ time ( echo "cpu" | nc <hostname> 12345 )
``` ```
or using golang or directly using golang:
```go ```go
import "net" conn, _ := net.Dial("tcp", "<hostname>:12345")
conn.Write([]byte("cpu\n"))
func main() { data, _ := io.RealAll(conn)
conn, err := net.Dial("tcp", "localhost:12345") log.Printf("data: %s", data)
if err != nil {
// handle error
}
defer conn.Close()
conn.Write([]byte("cpu"))
}
``` ```
## GitHub Actions Workflow ## GitHub Actions Workflow
The included GitHub Actions workflow automates the build and release process. On each push to the main branch, the workflow builds the Go program, creates a The included GitHub Actions workflow automates the build and release process. On each push to the main branch, the workflow builds the Go program, creates a GitHub release, and uploads the compiled binary as an artifact.
GitHub release, and uploads the compiled binary as an artifact.
## Downloading the Artifact ## Self-Installing Binary
TODO Assuming you have a cluster of machines, you can use the following commands to download, install and setup systemd services for the latest version of the binary on all nodes. You must have root access to all nodes.
<!--
To download the compiled binary on multiple machines using GNU Parallel, use the following `wget` command:
```bash ```bash
cat machines.txt | parallel --slf - 'wget -qO- https://github.com/aziis98/go-stats-server/releases/latest/download/stats-server | tar -xz -C /path/to/destination/' $ parallel --nonall --slf nodes.txt 'mkdir -p cluster'
$ parallel --nonall --slf nodes.txt 'wget -qO- https://github.com/aziis98/go-stats-server/releases/latest/download/stats-server > ./cluster/stats-server'
$ parallel --nonall --slf nodes.txt 'chmod -v +x ./cluster/stats-server'
# setup systemd services on all nodes
$ parallel --nonall --slf nodes.txt './cluster/stats-server setup'
# setup systemd service on a single node
$ ssh root@<node>
$ cd cluster
$ ./stats-server setup
# trying out the new binary using netcat
$ time ( echo '<command>' | nc <node> 12345 )
# stop and disable the systemd service on all nodes
$ parallel --nonall --slf nodes.txt 'systemctl disable --now stats-server.service'
``` ```
Replace "your-username" and "your-repo" with your GitHub username and repository name. Adjust "/path/to/destination/" to the desired destination on the target machines. ## Credits
Feel free to explore and customize the server to suit your needs. If you encounter any issues or have suggestions for improvements, please open an issue on this repository. Contributions are welcome! - The shell code for the commands was originally made by [@BachoSeven](https://github.com/bachoseven)
-->
<https://git.phc.dm.unipi.it/phc/cluster-dashboard/src/branch/main/backend/scripts>
- Most of the code was initially made with the help of ChatGPT in this conversation
<https://chat.openai.com/share/40247aa0-76e9-4d9a-b0fa-356a5f51f208>
+208 -28
View File
@@ -3,62 +3,242 @@ package main
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"log"
"net" "net"
"os" "os"
"os/exec" "os/exec"
"strings"
) )
// ExecuteCommand runs a system command and returns its output var systemdServiceUnit = strings.TrimSpace(`
func ExecuteCommand(command string) string { [Unit]
Description=Stats Server
After=network.target
[Service]
Type=simple
Restart=always
RestartSec=15
ExecStart=/usr/local/bin/stats-server serve
[Install]
WantedBy=default.target
`)
// cmdStrHelper removes indentation and newlines from a shell command string for
// better readability.
//
// Precisely, it first trims the string, then splits it by newlines, then trims
// each line, then joins them back with a space.
func cmdStrHelper(command string) string {
lines := strings.Split(strings.TrimSpace(command), "\n")
for i, line := range lines {
lines[i] = strings.TrimSpace(line)
}
return strings.Join(lines, " ")
}
// commands is a map of commands that can be run on the server, originally made
// by @BachoSeven
var commands = map[string]string{
"cpu": cmdStrHelper(`
top -bn1
| grep "Cpu(s)"
| sed "s/.*, *\([0-9.]*\)%* id.*/\1/"
| sed "s/^/100 - /"
| bc
`),
"memory": cmdStrHelper(`
free -m
| awk '/Mem/{print $3 " " $2}'
`),
"network": cmdStrHelper(`
cat /sys/class/net/[e]*/statistics/{r,t}x_bytes
`),
"storage": cmdStrHelper(`
df -Ph
| grep mmcblk0p5
| awk '{print $2 " " $3}'
| sed 's/G//g'
`),
"uptime": cmdStrHelper(`
cut -f1 -d. /proc/uptime
`),
"temperature": cmdStrHelper(`
paste
<(cat /sys/class/thermal/thermal_zone*/type)
<(awk
'{printf "%.3f\n", $1/1000}'
/sys/class/thermal/thermal_zone*/temp
)
`),
}
func init() {
log.SetFlags(0)
}
// runShellCommand runs a system command and returns its output
func runShellCommand(command string) (string, error) {
cmd := exec.Command("bash", "-c", command) cmd := exec.Command("bash", "-c", command)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return fmt.Sprintf("Error: %s", err) return string(output), err
} }
return string(output)
return string(output), nil
} }
// handleConnection handles one command per connection
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer conn.Close()
scanner := bufio.NewScanner(conn) scanner := bufio.NewScanner(conn)
for scanner.Scan() { if !scanner.Scan() {
command := scanner.Text() log.Printf("error reading from %s, %s", conn.RemoteAddr(), scanner.Err())
return
switch command {
case "cpu":
fmt.Fprintln(conn, ExecuteCommand("top -bn1 | grep \"Cpu(s)\" | sed \"s/.*, *\\([0-9.]*\\)%* id.*/\\1/\" | sed \"s/^/100 - /\" | bc"))
case "memory":
fmt.Fprintln(conn, ExecuteCommand("free -m | awk '/Mem/{print $3\" \"$2}'"))
case "network":
fmt.Fprintln(conn, ExecuteCommand("cat /sys/class/net/[e]*/statistics/{r,t}x_bytes"))
case "storage":
fmt.Fprintln(conn, ExecuteCommand("df -Ph | grep mmcblk0p5 | awk '{print $2\" \"$3}' | sed 's/G//g'"))
case "uptime":
fmt.Fprintln(conn, ExecuteCommand("cut -f1 -d. /proc/uptime"))
case "exit":
return
default:
fmt.Fprintln(conn, "Invalid command")
}
} }
command := scanner.Text()
log.Printf("received command %s from %s", command, conn.RemoteAddr())
shellCmd, valid := commands[strings.TrimSpace(string(command))]
if !valid {
fmt.Fprintln(conn, "invalid command")
return
}
output, err := runShellCommand(shellCmd)
if err != nil {
log.Fatal(err)
return
}
fmt.Fprintln(conn, output)
} }
func main() { func runCommand(args ...string) error {
ln, err := net.Listen("tcp", ":12345") cmd := exec.Command(args[0], args[1:]...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("%w: %s", err, output)
}
return nil
}
func setupSystemdService() error {
if _, err := os.Stat("/usr/local/bin/stats-server"); err == nil {
log.Println("binary already exists in /usr/local/bin, updating...")
}
log.Println("copying itself to /usr/local/bin/stats-server")
if err := runCommand("cp", "-f", os.Args[0], "/usr/local/bin/stats-server"); err != nil {
return err
}
log.Println("writing unit to /etc/systemd/system/stats-server.service")
if err := os.WriteFile(
"/etc/systemd/system/stats-server.service",
[]byte(systemdServiceUnit),
os.ModePerm,
); err != nil {
return err
}
log.Println("running systemctl daemon-reload")
if err := runCommand("systemctl", "daemon-reload"); err != nil {
return err
}
log.Println("running systemctl enable stats-server.service")
if err := runCommand("systemctl", "enable", "stats-server.service"); err != nil {
return err
}
log.Println("running systemctl (re)start stats-server.service")
if err := runCommand("systemctl", "restart", "stats-server.service"); err != nil {
return err
}
return nil
}
func startTCPServer() error {
host, ok := os.LookupEnv("HOST")
if !ok {
host = ":12345"
}
ln, err := net.Listen("tcp", host)
if err != nil { if err != nil {
fmt.Println("Error:", err) return err
os.Exit(1)
} }
defer ln.Close() defer ln.Close()
log.Printf("listening on %s...", host)
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
if err != nil { if err != nil {
fmt.Println("Error:", err) log.Printf("error accepting connection: %s", err)
continue continue
} }
log.Printf("connection from %s", conn.RemoteAddr())
go handleConnection(conn) go handleConnection(conn)
} }
} }
var helpText = strings.TrimSpace(`
usage: stats-server [setup|serve]
This is a simple tcp server that returns system stats, it can be run as a
systemd service or as a standalone server.
protocol commands:
cpu returns cpu usage
memory returns memory usage
network returns network usage
storage returns storage usage
uptime returns uptime
subcommands:
setup auto-install and setup systemd service
serve start tcp server
config, environment variables:
HOST tcp host to bind to (default: :12345)
`)
func showHelp() {
fmt.Println(helpText)
os.Exit(1)
}
func main() {
if len(os.Args) != 2 {
showHelp()
}
switch os.Args[1] {
case "setup":
log.Println("starting setup")
if err := setupSystemdService(); err != nil {
log.Fatal(err)
}
log.Println("setup completed")
case "serve":
log.Println("starting server")
if err := startTCPServer(); err != nil {
log.Fatal(err)
}
log.Println("server exited")
default:
showHelp()
}
}