initial commit
This commit is contained in:
commit
1bbd8ae59c
0
.Trash-1000/files/Dockerfile
Normal file
0
.Trash-1000/files/Dockerfile
Normal file
3
.Trash-1000/info/Dockerfile.trashinfo
Normal file
3
.Trash-1000/info/Dockerfile.trashinfo
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[Trash Info]
|
||||||
|
Path=Dockerfile
|
||||||
|
DeletionDate=2021-12-21T20:49:43
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
raidcheck
|
24
filter_changed_states.go
Normal file
24
filter_changed_states.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func filterChanges(in <-chan RaidState) <-chan RaidState {
|
||||||
|
out := make(chan RaidState, 4)
|
||||||
|
|
||||||
|
go func(in <-chan RaidState, out chan<- RaidState) {
|
||||||
|
defer close(out)
|
||||||
|
|
||||||
|
currentStates := map[string]RaidState{}
|
||||||
|
|
||||||
|
for state := range in {
|
||||||
|
if oldState, ok := currentStates[state.Name]; !ok || !reflect.DeepEqual(oldState.DevicesUp, state.DevicesUp) {
|
||||||
|
currentStates[state.Name] = state
|
||||||
|
out <- state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(in, out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module git.tordarus.net/Tordarus/raidcheck
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.tordarus.net/Tordarus/adverr v0.2.0
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
|
)
|
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
git.tordarus.net/Tordarus/adverr v0.2.0 h1:kLYjR2/Vb2GHiSAMvAv+WPNaHR9BRphKanf8H/pCZdA=
|
||||||
|
git.tordarus.net/Tordarus/adverr v0.2.0/go.mod h1:XRf0+7nhOkIEr0gi9DUG4RvV2KaOFB0fYPDaR1KLenw=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||||
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
55
main.go
Normal file
55
main.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.tordarus.net/Tordarus/adverr"
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
interval = 1 * time.Minute
|
||||||
|
telegramToken string
|
||||||
|
chatID int64
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if i, ok := os.LookupEnv("POLL_INTERVAL"); ok {
|
||||||
|
if interval2, err := time.ParseDuration(i); err == nil {
|
||||||
|
interval = interval2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt, ok := os.LookupEnv("TELEGRAM_API_TOKEN"); ok {
|
||||||
|
telegramToken = tt
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, ok := os.LookupEnv("TELEGRAM_CHAT_ID"); ok {
|
||||||
|
if chatId2, err := strconv.Atoi(id); err == nil {
|
||||||
|
chatID = int64(chatId2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot, err := tgbotapi.NewBotAPI(telegramToken)
|
||||||
|
if err != nil {
|
||||||
|
adverr.Fatalln(err, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for state := range filterChanges(mdstat()) {
|
||||||
|
str := state.String()
|
||||||
|
fmt.Println(str)
|
||||||
|
message := tgbotapi.NewMessage(chatID, fmt.Sprintf("Der Zustand des RAID Systems %s hat sich verändert:\n\n%s", state.Name, str))
|
||||||
|
message.Entities = append(message.Entities, tgbotapi.MessageEntity{
|
||||||
|
Type: "code",
|
||||||
|
Offset: len("Der Zustand des RAID Systems %s hat sich verändert:\n\n"),
|
||||||
|
Length: len(str),
|
||||||
|
})
|
||||||
|
_, err := bot.Send(message)
|
||||||
|
if err != nil {
|
||||||
|
adverr.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
mdstat.go
Normal file
82
mdstat.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
firstLineRegex = regexp.MustCompile(`^(.*?)\s:\s(\w+)?\sraid(\d{1,2})\s((?:[\w\d]+\[\d+\](\s|$))+)`)
|
||||||
|
secondLineRegex = regexp.MustCompile(`^\s{6}\d+\sblocks\s(?:super\s\d\.\d\s)?(?:level\s\d,\s)?(?:\d+\w\s\w+, )?(?:\w+\s\d\s)?\[(\d+)\/(\d+)\]\s\[([U_]+)\]`)
|
||||||
|
actionLineRegex = regexp.MustCompile(`^\s{6}\[[=>.]{21}\]\s\s(\w+?)\s=\s(\d+?\.\d+?)%\s\(\d+?\/\d+?\)\sfinish=(\d+?\.\d+?)(\w+?)\sspeed=(\d+?)([BKMG])\/sec`)
|
||||||
|
|
||||||
|
deviceRegex = regexp.MustCompile(`^([\w\d]+)\[(\d+)\]$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func mdstat() <-chan RaidState {
|
||||||
|
ch := make(chan RaidState, 4)
|
||||||
|
|
||||||
|
go func(out chan<- RaidState) {
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, err := os.ReadFile("/proc/mdstat")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
state := RaidState{}
|
||||||
|
for _, line := range lines {
|
||||||
|
if match := firstLineRegex.FindStringSubmatch(line); match != nil {
|
||||||
|
state.Name = match[1]
|
||||||
|
state.State = match[2]
|
||||||
|
state.Level = match[3]
|
||||||
|
|
||||||
|
devices := strings.Split(match[4], " ")
|
||||||
|
state.Devices = make([]string, len(devices))
|
||||||
|
for _, device := range devices {
|
||||||
|
if match := deviceRegex.FindStringSubmatch(device); match != nil {
|
||||||
|
deviceIndex, _ := strconv.Atoi(match[2])
|
||||||
|
state.Devices[deviceIndex] = match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if match := secondLineRegex.FindStringSubmatch(line); match != nil {
|
||||||
|
state.RegisteredDeviceCount, _ = strconv.Atoi(match[1])
|
||||||
|
state.UsedDeviceCount, _ = strconv.Atoi(match[2])
|
||||||
|
state.DevicesUp = map[string]bool{}
|
||||||
|
for i, rn := range match[3] {
|
||||||
|
state.DevicesUp[state.Devices[i]] = rn == 'U'
|
||||||
|
}
|
||||||
|
} else if match := actionLineRegex.FindStringSubmatch(line); match != nil {
|
||||||
|
state.Action = new(RaidAction)
|
||||||
|
state.Action.Name = match[1]
|
||||||
|
state.Action.Progress, _ = strconv.ParseFloat(match[2], 64)
|
||||||
|
state.Action.Duration = parseFinished(match[3], match[4])
|
||||||
|
state.Action.DurationFormatted = state.Action.Duration.String()
|
||||||
|
state.Action.Finished = time.Now().Add(state.Action.Duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out <- state
|
||||||
|
time.Sleep(time.Minute)
|
||||||
|
}
|
||||||
|
}(ch)
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFinished(value, unit string) time.Duration {
|
||||||
|
switch unit {
|
||||||
|
case "sec":
|
||||||
|
unit = "s"
|
||||||
|
case "min":
|
||||||
|
unit = "m"
|
||||||
|
}
|
||||||
|
|
||||||
|
dur, _ := time.ParseDuration(value + unit)
|
||||||
|
return dur
|
||||||
|
}
|
30
state.go
Normal file
30
state.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RaidState struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Devices []string `json:"devices"`
|
||||||
|
RegisteredDeviceCount int `json:"registered_device_count"`
|
||||||
|
UsedDeviceCount int `json:"used_device_count"`
|
||||||
|
DevicesUp map[string]bool `json:"devices_up"`
|
||||||
|
Action *RaidAction `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RaidAction struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Progress float64 `json:"progress"`
|
||||||
|
Duration time.Duration `json:"-"`
|
||||||
|
DurationFormatted string `json:"duration"`
|
||||||
|
Finished time.Time `json:"finished"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s RaidState) String() string {
|
||||||
|
data, _ := json.MarshalIndent(s, "", "\t")
|
||||||
|
return string(data)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user