commit f0aa76eaf2ee2c5a8fc782898668687d84a5f318
parent 6bee8c9c07faf1f1a60ff4305d60d5e305db4f8c
Author: Vetle Haflan <vetle@haflan.dev>
Date: Fri, 10 Apr 2020 15:37:34 +0200
A bunch of work
Diffstat:
A | database/database.go | | | 125 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | db.go | | | 110 | ------------------------------------------------------------------------------- |
M | main.go | | | 14 | +++++++++++--- |
A | services/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