From 1bbd8ae59ce31bf37e8e3dc478c7c240094c811e Mon Sep 17 00:00:00 2001 From: Tordarus Date: Tue, 21 Dec 2021 20:51:12 +0100 Subject: [PATCH] initial commit --- .Trash-1000/files/Dockerfile | 0 .Trash-1000/info/Dockerfile.trashinfo | 3 + .gitignore | 1 + filter_changed_states.go | 24 ++++++++ go.mod | 8 +++ go.sum | 4 ++ main.go | 55 ++++++++++++++++++ mdstat.go | 82 +++++++++++++++++++++++++++ state.go | 30 ++++++++++ 9 files changed, 207 insertions(+) create mode 100644 .Trash-1000/files/Dockerfile create mode 100644 .Trash-1000/info/Dockerfile.trashinfo create mode 100644 .gitignore create mode 100644 filter_changed_states.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 mdstat.go create mode 100644 state.go diff --git a/.Trash-1000/files/Dockerfile b/.Trash-1000/files/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/.Trash-1000/info/Dockerfile.trashinfo b/.Trash-1000/info/Dockerfile.trashinfo new file mode 100644 index 0000000..8a86802 --- /dev/null +++ b/.Trash-1000/info/Dockerfile.trashinfo @@ -0,0 +1,3 @@ +[Trash Info] +Path=Dockerfile +DeletionDate=2021-12-21T20:49:43 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f868c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +raidcheck diff --git a/filter_changed_states.go b/filter_changed_states.go new file mode 100644 index 0000000..edf4851 --- /dev/null +++ b/filter_changed_states.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..04b962a --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..49f673c --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f14f4c7 --- /dev/null +++ b/main.go @@ -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) + } + } +} diff --git a/mdstat.go b/mdstat.go new file mode 100644 index 0000000..119b5c8 --- /dev/null +++ b/mdstat.go @@ -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 +} diff --git a/state.go b/state.go new file mode 100644 index 0000000..8db827a --- /dev/null +++ b/state.go @@ -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) +}