first version of client

This commit is contained in:
Mans Ziesel 2024-12-05 19:50:21 +01:00
parent 8bcb0dd653
commit 5a6ccbc588
9 changed files with 291 additions and 20 deletions

View File

@ -1,13 +1,16 @@
GO_FILES := $(shell find . -name '*.go' -not -path './build/*')
build: $(GO_FILES)
build-server: $(GO_FILES)
mkdir -p build
go build -o build/zadmin-server ./cmd/server/main.go
build-client: $(GO_FILES)
mkdir -p build
go build -o build/zadmin-client ./cmd/client/main.go
docker-up:
mkdir -p ./tmp
docker compose -f ./deployments/compose-dev.yaml up
docker compose -f ./deployments/compose-dev.yaml up -d
docker-down:
docker compose -f ./deployments/compose-dev.yaml down
@ -15,10 +18,10 @@ docker-down:
docker-clean: docker-down
rm -rf ./tmp
server: build
server: build-server
./build/zadmin-server
client: build
client: build-client
./build/zadmin-client
clean:

View File

@ -1,8 +1,56 @@
package main
import "fmt"
import (
"log"
"sync"
"time"
func main() {
fmt.Println("Hello, World!")
"git.mziesel.nl/mans/zadmin/internal/agent"
"git.mziesel.nl/mans/zadmin/internal/models"
"github.com/nats-io/nats.go"
)
var DefaultAgentConfig models.MachineAgentConfig = models.MachineAgentConfig{
RESTServerHostname: "localhost",
RESTServerPort: 443,
NATSServerUrl: nats.DefaultURL,
HelloInterval: 30,
MetricsInterval: 30,
GetIPv4Endpoint: "https://ip4.mziesel.nl/json",
GetIPv6Endpoint: "https://ip6.mziesel.nl/json",
}
func main() {
agentConfig := DefaultAgentConfig
nc, err := nats.Connect(agentConfig.NATSServerUrl)
if err != nil {
log.Fatalf("failed to connect to nats-server: %s\n", err)
}
log.Println("connected to nats-server")
defer func() {
nc.Publish("client/disconnected", []byte("Goodbye!"))
nc.Close()
}()
nc.Publish("client/connected", []byte("Hello!"))
nc.Publish("client/hello", []byte("Hello!"))
var wg sync.WaitGroup
shouldExit := false
wg.Add(1)
go func() {
defer wg.Done()
for !shouldExit {
agent.PublishMachineData(agentConfig, nc)
time.Sleep(30 * time.Second)
}
}()
wg.Wait()
nc.Publish("client/disconnected", []byte("Goodbye!"))
nc.Close()
}

View File

@ -1,8 +1,10 @@
package main
import (
"encoding/json"
"log"
"git.mziesel.nl/mans/zadmin/internal/models"
"github.com/nats-io/nats.go"
)
@ -16,8 +18,23 @@ func main() {
log.Println("connected to nats-server")
// simply print to the console for now
nc.Subscribe("*", func(msg *nats.Msg) {
log.Printf("got message: %s\n", msg.Data)
nc.Subscribe(".", func(msg *nats.Msg) {
log.Printf("Msg received on [%s] : %s\n", msg.Subject, string(msg.Data))
})
nc.Subscribe("client/disconnected", func(msg *nats.Msg) {
log.Printf("Msg received on [%s] : %s\n", msg.Subject, string(msg.Data))
})
nc.Subscribe("client/connected", func(msg *nats.Msg) {
log.Printf("Msg received on [%s] : %s\n", msg.Subject, string(msg.Data))
})
nc.Subscribe("client/machinedata", func(msg *nats.Msg) {
data := models.MachineData{}
err = json.Unmarshal(msg.Data, &data)
if err != nil {
log.Println("failed to Unmarshal MachineData: ", err)
} else {
log.Printf("Msg received on [%s]: %s\n", msg.Subject, models.PrettyFormatData(data))
}
})
shouldExit := false

View File

@ -3,3 +3,4 @@ services:
image: nats:latest
ports:
- 4222:4222
command: ["-DV"]

7
go.mod
View File

@ -2,12 +2,15 @@ module git.mziesel.nl/mans/zadmin
go 1.23.0
require github.com/nats-io/nats.go v1.37.0
require (
github.com/mackerelio/go-osstat v0.2.5
github.com/nats-io/nats.go v1.37.0
golang.org/x/sys v0.20.0
)
require (
github.com/klauspost/compress v1.17.2 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.16.0 // indirect
)

6
go.sum
View File

@ -1,5 +1,7 @@
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o=
github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
@ -8,5 +10,5 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

148
internal/agent/collect.go Normal file
View File

@ -0,0 +1,148 @@
package agent
import (
"encoding/json"
"log"
"net/http"
"os"
"time"
"github.com/mackerelio/go-osstat/cpu"
"github.com/mackerelio/go-osstat/loadavg"
"github.com/mackerelio/go-osstat/memory"
"github.com/mackerelio/go-osstat/uptime"
"git.mziesel.nl/mans/zadmin/internal/models"
"github.com/nats-io/nats.go"
)
func PublishMachineData(ac models.MachineAgentConfig, nc *nats.Conn) {
machineData, err := GetMachineData(ac)
if err != nil {
LogError("failed to get MachineData", err)
return
}
machineDataJSON, err := json.Marshal(machineData)
if err != nil {
LogError("failed to marshal MachineData", err)
return
}
if err := nc.Publish("client/machinedata", machineDataJSON); err != nil {
LogError("failed to publish MachineData", err)
}
log.Println("sent data to nats-server")
}
func GetMachineData(ac models.MachineAgentConfig) (models.MachineData, error) {
data := models.MachineData{}
// get hostname
hostname, err := os.Hostname()
if err != nil {
LogError("failed to get system hostname", err)
return data, err
}
data.Hostname = hostname
data.AgentVersion = "TODO"
type IPResponse struct {
Address string `json:"address"`
}
// get public ipv4
ip4resp, err := http.Get(ac.GetIPv4Endpoint)
if err != nil {
LogError("failed to get IPv4 address", err)
return data, err
}
ipResponseBody := IPResponse{}
if err := json.NewDecoder(ip4resp.Body).Decode(&ipResponseBody); err != nil {
LogError("failed to decode IPv4 address", err)
return data, err
}
data.PublicIPv4Address = ipResponseBody.Address
// get public ipv6
ip6resp, err := http.Get(ac.GetIPv6Endpoint)
if err != nil {
LogError("failed to get IPv6 address", err)
return data, err
}
if err := json.NewDecoder(ip6resp.Body).Decode(&ipResponseBody); err != nil {
LogError("failed to decode IPv6 address", err)
return data, err
}
data.PublicIPv6Address = ipResponseBody.Address
// get machine uptime
uptime, err := uptime.GetUptime()
if err != nil {
LogError("failed to get machine uptime", err)
return data, nil
}
data.UptimeSeconds = int(uptime.Seconds())
// get useage statistics
useageStatistics, err := CollectUsageStatistics(ac)
if err != nil {
LogError("failed to get useage statistics", err)
return data, nil
}
data.UsageStatistics = useageStatistics
return data, nil
}
func LogError(msg string, err error) {
log.Printf("ERROR: %s: %v", msg, err)
}
func CollectUsageStatistics(ac models.MachineAgentConfig) (models.UsageStatistics, error) {
statistics := models.UsageStatistics{}
statistics.TimeCollected = time.Now()
memory, err := memory.Get()
if err != nil {
LogError("failed to collect memory statistics", err)
return statistics, err
}
statistics.MemoryTotal = memory.Total
statistics.MemoryUsed = memory.Used
statistics.MemoryCached = memory.Cached
statistics.MemoryFree = memory.Free
statistics.MemoryAvailable = memory.Available
cpu, err := cpu.Get()
if err != nil {
LogError("failed to collect cpu statistics", err)
return statistics, err
}
statistics.CpuUser = cpu.User
statistics.CpuSystem = cpu.System
statistics.CpuIdle = cpu.Idle
statistics.CpuIowait = cpu.Iowait
statistics.CpuSteal = cpu.Steal
statistics.CpuCount = cpu.CPUCount
loadAvg, err := loadavg.Get()
if err != nil {
LogError("failed to collect loadavg statistics", err)
return statistics, err
}
statistics.LoadAVG1 = loadAvg.Loadavg1
statistics.LoadAVG5 = loadAvg.Loadavg5
statistics.LoadAVG15 = loadAvg.Loadavg15
return statistics, nil
}

View File

@ -0,0 +1,14 @@
package models
import (
"encoding/json"
"log"
)
func PrettyFormatData(s any) string {
prettyData, err := json.MarshalIndent(s, "", "\t")
if err != nil {
log.Panic(err)
}
return string(prettyData)
}

View File

@ -1,6 +1,10 @@
package models
import "time"
import (
"fmt"
"strings"
"time"
)
// Model for the user accounts
type AccountModel struct {
@ -29,6 +33,7 @@ type MachineModel struct {
GoArch string `json:"go_arch"` // Go arch
}
// Data of the machine
type MachineData struct {
FirstSeen time.Time `json:"first_seen"` // Time of first contact with zadmin
LastSeen time.Time `json:"last_seen"` // Time of last contact with zadmin
@ -45,6 +50,23 @@ type MachineData struct {
OSVersion string `json:"os_version"` // OS version
}
// String method for MachineData
func (m MachineData) String() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("MachineData:\n"))
sb.WriteString(fmt.Sprintf(" FirstSeen: %s\n", m.FirstSeen))
sb.WriteString(fmt.Sprintf(" LastSeen: %s\n", m.LastSeen))
sb.WriteString(fmt.Sprintf(" Hostname: %s\n", m.Hostname))
sb.WriteString(fmt.Sprintf(" PublicIPv4Address: %s\n", m.PublicIPv4Address))
sb.WriteString(fmt.Sprintf(" PublicIPv6Address: %s\n", m.PublicIPv6Address))
sb.WriteString(fmt.Sprintf(" AgentVersion: %s\n", m.AgentVersion))
sb.WriteString(fmt.Sprintf(" UptimeSeconds: %d\n", m.UptimeSeconds))
sb.WriteString(fmt.Sprintf(" OSVersion: %s\n", m.OSVersion))
sb.WriteString(fmt.Sprintf(" LoggedOnUsers: %v\n", m.LoggedOnUsers))
return sb.String()
}
// Software installed on a Machine
type Software struct {
Name string
@ -57,10 +79,21 @@ type AntivirusInfo struct {
}
type UsageStatistics struct {
AvailCPU int `json:"avail_cpu"` // Available mCPU
UsedCPU int `json:"used_cpu"` // Used mCPU
AvailRAM int `json:"avail_ram"` // Available RAM in MB
UsedRAM int `json:"used_ram"` // Used RAM in MB
TimeCollected time.Time `json:"time_collected"` // Time metrics were collected at
CpuUser uint64 `json:"cpu_user"` // The amount of time the CPU spends executing user-level processes
CpuSystem uint64 `json:"cpu_system"` // The time the CPU spends executing kernel-level processes
CpuIdle uint64 `json:"cpu_idle"` // The percentage of time the CPU is idle and not doing any work
CpuIowait uint64 `json:"cpu_iowait"` // The time the CPU spends waiting for I/O operations to complete
CpuSteal uint64 `json:"cpu_steal"` // The amount of time a virtual CPU waits for the hypervisor to service its requests
CpuCount int `json:"cpu_count"` // Amount of CPUs present in machine
MemoryTotal uint64 `json:"memory_total"` // Total memory
MemoryUsed uint64 `json:"memory_used"` // Used memory
MemoryCached uint64 `json:"memory_cached"` // Cached memory
MemoryFree uint64 `json:"memory_free"` // Free memory
MemoryAvailable uint64 `json:"memory_available"` // Available memory
LoadAVG1 float64 `json:"loadavg_1"` // 1-minute Load Average
LoadAVG5 float64 `json:"loadavg_5"` // 5-minute Load Average
LoadAVG15 float64 `json:"loadavg_15"` // 15-minute Load Average
}
type DiskInformation struct {
@ -81,7 +114,9 @@ type MachineInterfaceDetails struct {
type MachineAgentConfig struct {
RESTServerHostname string `json:"rest_server_hostname"` // Hostname used for REST requests
RESTServerPort int `json:"rest_server_port"` // Port used for REST requests
NATSServerHostname string `json:"nats_server_hostname"` // Hostname used to contact nats.io
NATSServerPort int `json:"nats_server_port"` // Port used for REST requests
NATSServerUrl string `json:"nats_server_url"` // URL used to contact nats.io
HelloInterval int `json:"checkin_interval"` // interval of sending hello message, indicating host is online
MetricsInterval int `json:"metrics_interval"` // interval of sending hello message, indicating host is online
GetIPv4Endpoint string `json:"get_ipv4_endpoint"` // endpoint that returns a json doc with the IPv4 address
GetIPv6Endpoint string `json:"get_ipv6_endpoint"` // endpoint that returns a json doc with the IPv6 address
}