From aede45a865d6005cdd256ad2e380a5370be270fe Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Sat, 13 Mar 2021 03:17:30 +0400 Subject: [PATCH] Initial commit. --- .gitignore | 2 + Makefile | 31 ++++++++ README.md | 12 +++ config.example.json | 4 + go.mod | 3 + httpprocwatchd.service | 12 +++ main.go | 177 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 241 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 config.example.json create mode 100644 go.mod create mode 100644 httpprocwatchd.service create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4def3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +http-nsupdater.json \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e79cd5c --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +TARGET=httpprocwatchd + +SYSCTL=$(shell which systemctl) + +CONFDIR=/etc/$(TARGET) +SYSDSERVICEDIR=/etc/systemd/system + +LDFLAGS=-ldflags "-s -w" + +all: $(TARGET) + +$(TARGET): main.go + go build $(LDFLAGS) + +install: + install -m 0755 $(TARGET) /usr/bin/$(TARGET) + mkdir -p -m 0755 $(CONFDIR) + install -m 0644 config.example.json $(CONFDIR)/config.example.json + +install-service: + install -m 0644 $(TARGET).service $(SYSDSERVICEDIR)/$(TARGET).service + $(SYSCTL) daemon-reload + +uninstall: + rm /usr/bin/$(TARGET) + +uninstall-service: + $(SYSCTL) stop $(TARGET).service + $(SYSCTL) disable $(TARGET).service + rm $(SYSDSERVICEDIR)/$(TARGET).service + $(SYSCTL) daemon-reload diff --git a/README.md b/README.md new file mode 100644 index 0000000..9936e18 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# httpprocwatchd + +It is a process watcher that gives processes' statuses in a JSON format via HTTP +GET request on /processes endpoint. + +A JSON object looks like this: `{ "":, ... }`. + +A configuration file is in JSON format as well. There are two options: +`listen_address` is a string that looks like `"[]:"` and an array +of process names. + +An example configuration is stored in `config.example.json` file. \ No newline at end of file diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..28b8309 --- /dev/null +++ b/config.example.json @@ -0,0 +1,4 @@ +{ + "listen_address": ":28010", + "processes": ["postfix", "dovecot"] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b944e96 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module httpprocwatchd + +go 1.15 diff --git a/httpprocwatchd.service b/httpprocwatchd.service new file mode 100644 index 0000000..ece7853 --- /dev/null +++ b/httpprocwatchd.service @@ -0,0 +1,12 @@ +[Unit] +Description=HTTPProcWatchD HTTP process watcher +After=network.target + +[Service] +Type=simple +User=nobody +Group=nobody +ExecStart=/usr/bin/httpprocwatchd --config /etc/httpprocwatchd/config.json + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..52becdb --- /dev/null +++ b/main.go @@ -0,0 +1,177 @@ +/* + httpprocwatchd provides HTTP interface to a list of process' statuses + formatted as JSON. + Copyright (c) 2021 Alexander "Arav" Andreev + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "syscall" + "time" +) + +// ErrPgrepNotFound occurs when pgrep program is not found. +var ErrPgrepNotFound = errors.New("pgrep not found") + +// Configuration holds a list of process names to be tracked and a listen address. +type Configuration struct { + ListenAddress string `json:"listen_address"` + Processes []string `json:"processes"` +} + +// LoadConfiguration loads configuration from a JSON file. +func LoadConfiguration(path string) (conf *Configuration, err error) { + conf = &Configuration{} + + file, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(file, conf); err != nil { + return nil, err + } + + return conf, nil +} + +// ProcessList is a map of processes' statuses. +type ProcessList map[string]bool + +// NewProcessList returns a ProcessList with initialised map. +func NewProcessList() *ProcessList { + pl := make(ProcessList) + return &pl +} + +// AddProcess appends a process to a ProcessList +func (pl *ProcessList) AddProcess(name string, isup bool) { + (*pl)[name] = isup +} + +// IsProcessUp returns true if process is up. Uses pgrep to get PID of a process +// and if a process is not working, then pgrep returns nothing. +func IsProcessUp(name string) (bool, error) { + pgrep := exec.Command("pgrep", "-nf", name) + + out, err := pgrep.CombinedOutput() + if err != nil { + return false, err + } + + if len(out) == 0 { + return false, nil + } + + return true, nil +} + +// AreProcessesUp handles a GET /processes request and sends back status of given +// processes. +func AreProcessesUp(processes *[]string) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + proclist := NewProcessList() + + for _, proc := range *processes { + isup, _ := IsProcessUp(proc) + + proclist.AddProcess(proc, isup) + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(proclist) + } else { + w.WriteHeader(http.StatusMethodNotAllowed) + w.Header().Add("Allow", "GET") + } + } +} + +func version() { + fmt.Println("httpprocwatchd ver. 1.0") + fmt.Println("Copyright (c) 2021 Alexander \"Arav\" Andreev ") + fmt.Println("This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.") + fmt.Println("This is free software, and you are welcome to redistribute it") + fmt.Println("under certain conditions; type `show c' for details.") +} + +func main() { + log.SetFlags(0) + + configPath := flag.String("config", "processes.json", "path to configuration file") + showVersion := flag.Bool("version", false, "show version") + + flag.Parse() + + if *showVersion { + version() + return + } + + if _, err := exec.LookPath("pgrep"); err != nil { + log.Fatalln(ErrPgrepNotFound) + } + + conf, err := LoadConfiguration(*configPath) + if err != nil { + log.Fatalf("Cannot load configuration file: %s\n", err) + } + + router := http.NewServeMux() + router.HandleFunc("/processes", AreProcessesUp(&conf.Processes)) + + srv := &http.Server{ + Addr: conf.ListenAddress, + Handler: router, + } + + done := make(chan os.Signal, 1) + signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("ListenAndServe: %s\n", err) + } + }() + + log.Printf("httpprocwatchd is running on \"%s\".", conf.ListenAddress) + + <-done + + log.Printf("Shutting down... ") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("%s\n", err) + } + + log.Println("Done.") +}