sermoni

"Service monitor" / cronjob status service
Log | Files | Refs

commit f0aa76eaf2ee2c5a8fc782898668687d84a5f318
parent 6bee8c9c07faf1f1a60ff4305d60d5e305db4f8c
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Fri, 10 Apr 2020 15:37:34 +0200

A bunch of work

Diffstat:
Adatabase/database.go | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddb.go | 110-------------------------------------------------------------------------------
Mmain.go | 14+++++++++++---
Aservices/services.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 233 insertions(+), 113 deletions(-)

diff --git a/database/database.go b/database/database.go @@ -0,0 +1,125 @@ +package database + +import ( + "errors" + "fmt" + "time" + + "golang.org/x/crypto/bcrypt" + + "go.etcd.io/bbolt" +) + +const ( + defaultPassPhrase = "admin" + defaultPageTitle = "sermoni" +) + +// bbolt keys +var ( + BucketKeyConfig = []byte("config") // bucket key for config bucket key + BucketKeyServices = []byte("services") // bucket key for services bucket + BucketKeyEvents = []byte("events") // bucket key for events bucket + + keyPassHash = []byte("passhash") + keyPageTitle = []byte("pagetitle") +) + +// ErrConfigBucket is returned when bbolt is unable to open the config bucket +// TODO: I'm not sure if this is the idiomatic way to use errors +var ErrConfigBucket = errors.New("unable to open config bucket") + +var db *bbolt.DB + +// Init opens the database for the given file name or creates it if it doesn't exist. +// It also populates it with essential configuration data if required. +func Init(dbFileName string) error { + fmt.Printf("Init db '%v'\n", dbFileName) + var err error + db, err = bbolt.Open(dbFileName, 0600, &bbolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return err + } + // Create the necessary bbolt buckets if they don't exist + err = db.Update(func(tx *bbolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists(BucketKeyConfig); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(BucketKeyServices); err != nil { + return err + } + if _, err := tx.CreateBucketIfNotExists(BucketKeyEvents); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + // Check if the config is initialized - configure if not + var configured bool + err = db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(BucketKeyConfig) + if b == nil { + return ErrConfigBucket + } + passhash := b.Get(keyPassHash) + configured = passhash != nil + return nil + }) + if !configured { + return Reconfigure(defaultPassPhrase, defaultPageTitle) + } + return nil +} + +// Reconfigure takes a passphrase and a page title for the web page, +// generates hash for the password and updates the database with this +// new configuration. +func Reconfigure(passphrase string, pageTitle string) error { + + /* TODO: Generate a random _readable_ password if none is given + var passphraseBytes []byte + if passphrase == "" { + passphraseBytes = make([]byte, 24) + rand.Read(passphraseBytes) + passphrase = string(passphraseBytes) + fmt.Printf("Generated passphrase: %v\n", []rune(passphrase)) + } else { + passphraseBytes = []byte(passphrase) + }*/ + //sha256.Sum256([]byte(passphraseBytes)) + + // TODO: Maybe bcrypt is overkill for such a small project? Consider later + passhash, err := bcrypt.GenerateFromPassword([]byte(passphrase), bcrypt.DefaultCost) + if err != nil { + return err + } + + return db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(BucketKeyConfig) + if b == nil { + return ErrConfigBucket + } + var err error + if err = b.Put(keyPassHash, passhash); err != nil { + return err + } + if err = b.Put(keyPageTitle, []byte(pageTitle)); err != nil { + return err + } + return nil + }) +} + +// Close is just a wrapper around db.Close() in order to keep all database +// management in one file +func Close() { + db.Close() +} + +// GetDB returns the bbolt database struct +// TODO: Not sure how if this is a good way to do it, although it does seem to work +func GetDB() *bbolt.DB { + return db +} diff --git a/db.go b/db.go @@ -1,110 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/sha256" - "errors" - "fmt" - "time" - - "go.etcd.io/bbolt" -) - -const defaultPageTitle = "sermoni" - -// bbolt keys -var ( - bucketKeyConfig = []byte("config") - bucketKeyServices = []byte("services") - bucketKeyEvents = []byte("events") - keyPassHash = []byte("passhash") - keyPageTitle = []byte("pagetitle") - keyServiceName = []byte("name") - keyServiceDescription = []byte("description") - keyServicePeriod = []byte("period") -) - -// ErrConfigBucket is returned when bbolt is unable to open the config bucket -// TODO: I'm not sure if this is the idiomatic way to use errors -var ErrConfigBucket = errors.New("unable to open config bucket") - -// Global bbolt DB struct -var db *bbolt.DB - -// openDB opens the database for the given file name or creates it if it doesn't exist -func initDB(dbFileName string) error { - fmt.Printf("Init db '%v'\n", dbFileName) - var err error - db, err = bbolt.Open(dbFileName, 0600, &bbolt.Options{Timeout: 1 * time.Second}) - if err != nil { - return err - } - // Create the necessary bbolt buckets if they don't exist - err = db.Update(func(tx *bbolt.Tx) error { - if _, err := tx.CreateBucketIfNotExists(bucketKeyConfig); err != nil { - return err - } - if _, err := tx.CreateBucketIfNotExists(bucketKeyServices); err != nil { - return err - } - if _, err := tx.CreateBucketIfNotExists(bucketKeyEvents); err != nil { - return err - } - return nil - }) - if err != nil { - return err - } - // Check if the config is initialized - configure if not - var configured bool - err = db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(bucketKeyConfig) - if b == nil { - return ErrConfigBucket - } - passhash := b.Get(keyPassHash) - configured = passhash != nil - return nil - }) - if !configured { - return reconfigure("", defaultPageTitle) - } - return nil -} - -// reconfigure takes a passphrase and a page title for the web page -// and updates the database with this new configuration. -// If passphrase is the empty string, a random phrase will be generated. -func reconfigure(passphrase string, pageTitle string) error { - var passphraseBytes []byte - if passphrase == "" { - passphraseBytes = make([]byte, 24) - rand.Read(passphraseBytes) - passphrase = string(passphraseBytes) - fmt.Printf("Generated passphrase: %v\n", passphrase) - } else { - passphraseBytes = []byte(passphrase) - } - passhash := sha256.Sum256(passphraseBytes) - return db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket(bucketKeyConfig) - if b == nil { - return ErrConfigBucket - } - var err error - // [:] is needed to get a slice from the [32]byte array - if err = b.Put(keyPassHash, passhash[:]); err != nil { - return err - } - if err = b.Put(keyPageTitle, []byte(pageTitle)); err != nil { - return err - } - return nil - }) -} - -// closeDB is just a wrapper around db.Close() in order to keep all database -// management in one file -func closeDB() { - db.Close() -} diff --git a/main.go b/main.go @@ -4,6 +4,8 @@ import ( "flag" "fmt" "log" + "sermoni/database" + "sermoni/services" ) func main() { @@ -12,9 +14,15 @@ func main() { dbFile := flag.String("d", "sermoni.db", "Database file") //password := flag.String("w", "", "Password for the web interface") flag.Parse() - if err := initDB(*dbFile); err != nil { + if err := database.Init(*dbFile); err != nil { log.Fatal(err) } - defer closeDB() - fmt.Printf("Server running on port %v", *port) + defer database.Close() + fmt.Printf("Server running on port %v\n", *port) + services.Add("testing", services.Service{ + Name: "Test name", + Description: "This is the description, yay", + }) + testService := services.Get("testing") + fmt.Printf("test service: %+v\n", testService) } diff --git a/services/services.go b/services/services.go @@ -0,0 +1,97 @@ +package services + +import ( + "errors" + "log" + "sermoni/database" + "strconv" + + "go.etcd.io/bbolt" +) + +var ( + keyServiceName = []byte("name") + keyServiceDescription = []byte("description") + keyServicePeriod = []byte("period") +) + +// Service describes a service that is expected to report +type Service struct { + Name string `json:"name"` // service name, usually on the format 'service @ server' + Description string `json:"description"` // more detailed description of the service + ExpectationPeriod int `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?) +} + +// Get returns a service struct if the identifier matches any +// keys in the services bucket. Returns nil if there are no matching buckets +func Get(identifier string) (service Service) { + db := database.GetDB() + err := db.View(func(tx *bbolt.Tx) error { + serviceBucket := tx.Bucket(database.BucketKeyServices) + if serviceBucket == nil { + log.Fatal("No services bucket found") + } + b := serviceBucket.Bucket([]byte(identifier)) + if b == nil { + return errors.New("no bucket found for the given id") + } + if name := b.Get(keyServiceName); name != nil { + service.Name = string(name) + } + if description := b.Get(keyServiceDescription); description != nil { + service.Description = string(description) + } + if period := b.Get(keyServicePeriod); period != nil { + // Quick fix: Convert to string, then int + // If an error occurs (it tho) + if intPeriod, err := strconv.Atoi(string(period)); err != nil { + service.ExpectationPeriod = intPeriod + log.Printf("Couldn't convert period to int for service with id '%v'\n", identifier) + } else { + service.ExpectationPeriod = 0 + } + } + return nil + }) + if err != nil { + log.Println(err) + } + return service +} + +// Delete deletes the given service if it exists +func Delete(identifier string) { + //db := database.GetDB() +} + +// Add adds a new service to monitor +func Add(identifier string, service Service) error { + db := database.GetDB() + return db.Update(func(tx *bbolt.Tx) error { + serviceBucket := tx.Bucket(database.BucketKeyServices) + if serviceBucket == nil { + log.Fatal("No services bucket found") + } + serviceKey := []byte(identifier) + if serviceBucket.Bucket(serviceKey) != nil { + return errors.New("a service has already been registered for the given id") + } + b, err := serviceBucket.CreateBucket(serviceKey) + if err != nil { + return err + } + if err = b.Put(keyServiceName, []byte(service.Name)); err != nil { + return err + } + if err = b.Put(keyServiceDescription, []byte(service.Description)); err != nil { + return err + } + periodStr := strconv.Itoa(service.ExpectationPeriod) + if err = b.Put(keyServicePeriod, []byte(periodStr)); err != nil { + return err + } + return nil + }) +} + +// TODO: Consider a wrapper that gets the services bucket and operates on it