sermoni

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

commit 84673d881008684688113e514c8989244fc3a901
parent 6c553e5f99e22bbe1284396a6c268a7036fd6e6e
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Sat, 11 Apr 2020 12:37:20 +0200

Restructure for a more correct package structure

Diffstat:
Acmd/sermoni/main.go | 21+++++++++++++++++++++
Devents/events.go | 167-------------------------------------------------------------------------------
Devents/events_test.go | 97-------------------------------------------------------------------------------
Rdatabase/database.go -> internal/database/database.go | 0
Ainternal/events/events.go | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/events/events_test.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/services/services.go | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/services/services_test.go | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmain.go | 21---------------------
Dservices/services.go | 220-------------------------------------------------------------------------------
Dservices/services_test.go | 143-------------------------------------------------------------------------------
Rproto.html -> ui/proto.html | 0
Rstyle.css -> ui/style.css | 0
13 files changed, 651 insertions(+), 648 deletions(-)

diff --git a/cmd/sermoni/main.go b/cmd/sermoni/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "flag" + "fmt" + "log" + "sermoni/internal/database" +) + +func main() { + // TODO: Use getopt package instead of flags? + port := flag.Int("p", 8080, "Port") + dbFile := flag.String("d", "sermoni.db", "Database file") + //password := flag.String("w", "", "Password for the web interface") + flag.Parse() + if err := database.Init(*dbFile); err != nil { + log.Fatal(err) + } + defer database.Close() + fmt.Printf("Server running on port %v\n", *port) +} diff --git a/events/events.go b/events/events.go @@ -1,167 +0,0 @@ -package events - -import ( - "errors" - "fmt" - "log" - "sermoni/database" - "sermoni/services" - "strconv" - - "go.etcd.io/bbolt" -) - -var ( - keyEventID = []byte("id") - keyEventService = []byte("service") - keyEventTimestamp = []byte("timestamp") - keyEventStatus = []byte("status") - keyEventTitle = []byte("title") - keyEventDetails = []byte("details") -) - -// Event contains data sent to sermoni from a service -type Event struct { - ID uint64 `json:"id"` - Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side) - Timestamp uint64 `json:"timestamp"` - Status string `json:"status"` - Title string `json:"title"` - Details string `json:"details"` -} - -// GetAll returns all events in the database -func GetAll() (events []*Event) { - db := database.GetDB() - err := db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(database.BucketKeyEvents) - if b == nil { - log.Panic("The events bucket does not exist") - } - // ForEach doesn't return buckets (nil instead), so only the key is useful - return b.ForEach(func(id, _ []byte) error { - eb := b.Bucket(id) - event := new(Event) - if err := event.fromBucket(eb); err != nil { - return err - } - events = append(events, event) - return nil - }) - }) - if err != nil { - fmt.Println(err) - } - return -} - -// Delete event with the given ID. -// Returns error if no such event can be found -func Delete(idInt uint64) error { - db := database.GetDB() - id := database.Uint64ToBytes(idInt) - return db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(database.BucketKeyEvents) - if b == nil { - log.Panic("The events bucket does notexist") - } - return b.DeleteBucket(id) - }) -} - -// Add a new service event if the token matches any services in the database -// The event.Service will be set to the service ID automatically, given a valid token -func Add(serviceToken string, event *Event) error { - db := database.GetDB() - service := services.GetByToken(serviceToken) - if service == nil { - return errors.New("no service found for the given token") - } - return db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket(database.BucketKeyEvents) - if b == nil { - log.Panic("The events bucket does not exist") - } - - // Create a new event ID - idInt, err := b.NextSequence() - if err != nil { - return err - } - id := []byte(strconv.FormatUint(idInt, 10)) - event.ID = idInt - event.Service = service.ID - - // Create the event bucket and fill it with data from event - eb, err := b.CreateBucket(id) - if err != nil { - return err - } - return event.toBucket(eb) - }) -} - -// Writes the event data to the given bucket -func (event *Event) toBucket(eb *bbolt.Bucket) error { - var err error - id := database.Uint64ToBytes(event.ID) - serviceID := database.Uint64ToBytes(event.Service) - timestamp := database.Uint64ToBytes(event.Timestamp) - if err = eb.Put(keyEventID, id); err != nil { - return err - } - if eb.Put(keyEventService, serviceID); err != nil { - return err - } - if eb.Put(keyEventTimestamp, timestamp); err != nil { - return err - } - if eb.Put(keyEventStatus, []byte(event.Status)); err != nil { - return err - } - if eb.Put(keyEventTitle, []byte(event.Title)); err != nil { - return err - } - if eb.Put(keyEventDetails, []byte(event.Details)); err != nil { - return err - } - return nil -} - -// Reads data from the given bucket into the fields of event -// Returns error if any of the fields cannot be found -func (event *Event) fromBucket(eb *bbolt.Bucket) error { - var id, service, timestamp []byte - var status, title, details []byte - err := errors.New("missing field from database") - - // Get data from database - if id = eb.Get(keyEventID); id == nil { - return err - } - if service = eb.Get(keyEventService); service == nil { - return err - } - if timestamp = eb.Get(keyEventTimestamp); timestamp == nil { - return err - } - if status = eb.Get(keyEventStatus); status == nil { - return err - } - if title = eb.Get(keyEventTitle); title == nil { - return err - } - if details = eb.Get(keyEventDetails); details == nil { - return err - } - - // Format data and set fields of event - event.ID = database.BytesToUint64(id) - event.Service = database.BytesToUint64(service) - event.Timestamp = database.BytesToUint64(timestamp) - event.Status = string(status) - event.Title = string(title) - event.Details = string(details) - - return nil -} diff --git a/events/events_test.go b/events/events_test.go @@ -1,97 +0,0 @@ -package events - -import ( - "fmt" - "os" - "sermoni/database" - "sermoni/services" - "testing" -) - -const serviceToken = "test-service" - -func (e1 *Event) equals(e2 *Event) bool { - switch { - case e1.ID != e2.ID: - return false - case e1.Service != e2.Service: - return false - case e1.Status != e2.Status: - return false - case e1.Title != e2.Title: - return false - case e1.Details != e2.Details: - return false - default: - return true - } -} - -var testEvents = []*Event{ - { - Timestamp: 1586558825515, - Status: "ok", - Title: "Backup completed successfully", - }, - { - Timestamp: 1586558838488, - Status: "info", - Title: "SSH login for user vetle", - Details: "User vetle logged in from IP 192.168.10.110", - }, - { - Timestamp: 1586558848488, - Status: "ok", - }, - { - Timestamp: 1586558949488, - Status: "error", - Title: "Backup failed", - Details: "Backup couldn't complete because the disk is full", - }, -} - -func TestAddEvent(t *testing.T) { - for _, event := range testEvents { - if err := Add(serviceToken, event); err != nil { - fmt.Println(err) - t.Fatal("error returned when trying to add event") - } - } - - // Assumes that bbolt starts sequences on 1 - for i, event := range testEvents { - event.ID = uint64(i) + 1 - event.Service = 1 - } -} - -func TestGetAll(t *testing.T) { - events := GetAll() - for i, event := range events { - if !event.equals(testEvents[i]) { - t.Fatal("stored event does not match original") - } - } -} - -func TestMain(m *testing.M) { - // (Re)create the test database - testDB := "test.db" - os.Remove(testDB) - var err error - if err = database.Init(testDB); err != nil { - print("Couldn't initialize test database") - os.Exit(1) - } - err = services.Add(serviceToken, &services.Service{ - Name: "test @ dev-laptop", - Description: "Service used for testing only", - }) - if err != nil { - print("Couldn't add test service") - os.Exit(1) - } - defer database.Close() - os.Exit(m.Run()) -} diff --git a/database/database.go b/internal/database/database.go diff --git a/internal/events/events.go b/internal/events/events.go @@ -0,0 +1,167 @@ +package events + +import ( + "errors" + "fmt" + "log" + "sermoni/internal/database" + "sermoni/internal/services" + "strconv" + + "go.etcd.io/bbolt" +) + +var ( + keyEventID = []byte("id") + keyEventService = []byte("service") + keyEventTimestamp = []byte("timestamp") + keyEventStatus = []byte("status") + keyEventTitle = []byte("title") + keyEventDetails = []byte("details") +) + +// Event contains data sent to sermoni from a service +type Event struct { + ID uint64 `json:"id"` + Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side) + Timestamp uint64 `json:"timestamp"` + Status string `json:"status"` + Title string `json:"title"` + Details string `json:"details"` +} + +// GetAll returns all events in the database +func GetAll() (events []*Event) { + db := database.GetDB() + err := db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyEvents) + if b == nil { + log.Panic("The events bucket does not exist") + } + // ForEach doesn't return buckets (nil instead), so only the key is useful + return b.ForEach(func(id, _ []byte) error { + eb := b.Bucket(id) + event := new(Event) + if err := event.fromBucket(eb); err != nil { + return err + } + events = append(events, event) + return nil + }) + }) + if err != nil { + fmt.Println(err) + } + return +} + +// Delete event with the given ID. +// Returns error if no such event can be found +func Delete(idInt uint64) error { + db := database.GetDB() + id := database.Uint64ToBytes(idInt) + return db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyEvents) + if b == nil { + log.Panic("The events bucket does notexist") + } + return b.DeleteBucket(id) + }) +} + +// Add a new service event if the token matches any services in the database +// The event.Service will be set to the service ID automatically, given a valid token +func Add(serviceToken string, event *Event) error { + db := database.GetDB() + service := services.GetByToken(serviceToken) + if service == nil { + return errors.New("no service found for the given token") + } + return db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyEvents) + if b == nil { + log.Panic("The events bucket does not exist") + } + + // Create a new event ID + idInt, err := b.NextSequence() + if err != nil { + return err + } + id := []byte(strconv.FormatUint(idInt, 10)) + event.ID = idInt + event.Service = service.ID + + // Create the event bucket and fill it with data from event + eb, err := b.CreateBucket(id) + if err != nil { + return err + } + return event.toBucket(eb) + }) +} + +// Writes the event data to the given bucket +func (event *Event) toBucket(eb *bbolt.Bucket) error { + var err error + id := database.Uint64ToBytes(event.ID) + serviceID := database.Uint64ToBytes(event.Service) + timestamp := database.Uint64ToBytes(event.Timestamp) + if err = eb.Put(keyEventID, id); err != nil { + return err + } + if eb.Put(keyEventService, serviceID); err != nil { + return err + } + if eb.Put(keyEventTimestamp, timestamp); err != nil { + return err + } + if eb.Put(keyEventStatus, []byte(event.Status)); err != nil { + return err + } + if eb.Put(keyEventTitle, []byte(event.Title)); err != nil { + return err + } + if eb.Put(keyEventDetails, []byte(event.Details)); err != nil { + return err + } + return nil +} + +// Reads data from the given bucket into the fields of event +// Returns error if any of the fields cannot be found +func (event *Event) fromBucket(eb *bbolt.Bucket) error { + var id, service, timestamp []byte + var status, title, details []byte + err := errors.New("missing field from database") + + // Get data from database + if id = eb.Get(keyEventID); id == nil { + return err + } + if service = eb.Get(keyEventService); service == nil { + return err + } + if timestamp = eb.Get(keyEventTimestamp); timestamp == nil { + return err + } + if status = eb.Get(keyEventStatus); status == nil { + return err + } + if title = eb.Get(keyEventTitle); title == nil { + return err + } + if details = eb.Get(keyEventDetails); details == nil { + return err + } + + // Format data and set fields of event + event.ID = database.BytesToUint64(id) + event.Service = database.BytesToUint64(service) + event.Timestamp = database.BytesToUint64(timestamp) + event.Status = string(status) + event.Title = string(title) + event.Details = string(details) + + return nil +} diff --git a/internal/events/events_test.go b/internal/events/events_test.go @@ -0,0 +1,97 @@ +package events + +import ( + "fmt" + "os" + "sermoni/internal/database" + "sermoni/internal/services" + "testing" +) + +const serviceToken = "test-service" + +func (e1 *Event) equals(e2 *Event) bool { + switch { + case e1.ID != e2.ID: + return false + case e1.Service != e2.Service: + return false + case e1.Status != e2.Status: + return false + case e1.Title != e2.Title: + return false + case e1.Details != e2.Details: + return false + default: + return true + } +} + +var testEvents = []*Event{ + { + Timestamp: 1586558825515, + Status: "ok", + Title: "Backup completed successfully", + }, + { + Timestamp: 1586558838488, + Status: "info", + Title: "SSH login for user vetle", + Details: "User vetle logged in from IP 192.168.10.110", + }, + { + Timestamp: 1586558848488, + Status: "ok", + }, + { + Timestamp: 1586558949488, + Status: "error", + Title: "Backup failed", + Details: "Backup couldn't complete because the disk is full", + }, +} + +func TestAddEvent(t *testing.T) { + for _, event := range testEvents { + if err := Add(serviceToken, event); err != nil { + fmt.Println(err) + t.Fatal("error returned when trying to add event") + } + } + + // Assumes that bbolt starts sequences on 1 + for i, event := range testEvents { + event.ID = uint64(i) + 1 + event.Service = 1 + } +} + +func TestGetAll(t *testing.T) { + events := GetAll() + for i, event := range events { + if !event.equals(testEvents[i]) { + t.Fatal("stored event does not match original") + } + } +} + +func TestMain(m *testing.M) { + // (Re)create the test database + testDB := "test.db" + os.Remove(testDB) + var err error + if err = database.Init(testDB); err != nil { + print("Couldn't initialize test database") + os.Exit(1) + } + err = services.Add(serviceToken, &services.Service{ + Name: "test @ dev-laptop", + Description: "Service used for testing only", + }) + if err != nil { + print("Couldn't add test service") + os.Exit(1) + } + defer database.Close() + os.Exit(m.Run()) +} diff --git a/internal/services/services.go b/internal/services/services.go @@ -0,0 +1,223 @@ +package services + +import ( + "errors" + "log" + "sermoni/internal/database" + "strconv" + + "go.etcd.io/bbolt" +) + +var ( + keyServiceID = []byte("id") + keyServiceName = []byte("name") + keyServiceDescription = []byte("description") + keyServicePeriod = []byte("period") +) + +// Service describes a service that is expected to report +type Service struct { + ID uint64 `json:"id"` // service id, an integer that represents the service + Name string `json:"name"` // service name, usually on the format 'service @ server' + Description string `json:"description"` // more detailed description of the service + ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?) +} + +// GetByToken returns the service structure associated with the token string, if there +// are any matching entries in service-tokens bucket. Returns nil if there are no matches +func GetByToken(token string) *Service { + id := getIDFromToken(token) + if id == nil { + log.Printf("no service found for the token '%v'\n", token) + return nil + } + return get(id) +} + +// GetByID returns the service structure associated with the given uint64-formatted +// service ID, if that service exists. Otherwise returns nil +func GetByID(id uint64) *Service { + return get([]byte(strconv.FormatUint(id, 10))) +} + +// GetAll returns all services in the database (TODO) +func GetAll() (services []*Service) { + db := database.GetDB() + db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyServices) + stb := tx.Bucket(database.BucketKeyServiceTokens) + if b == nil { + log.Panic("The services bucket does not exist") + } + if stb == nil { + log.Panic("The service-tokens bucket does not exist") + } + + // Go through all k-v pairs in the service *tokens* bucket, in order to get service bucket IDs + // Use the ID to get the service bucket and create service fromBucket + return stb.ForEach(func(_, id []byte) error { + sb := b.Bucket(id) + service := new(Service) + service.fromBucket(id, sb) + services = append(services, service) + return nil + }) + }) + return +} + +// Delete deletes the given service if it exists +func Delete(intID uint64) error { + db := database.GetDB() + serviceID := []byte(strconv.FormatUint(intID, 10)) + return db.Update(func(tx *bbolt.Tx) error { + var b, stb *bbolt.Bucket + if b = tx.Bucket(database.BucketKeyServices); b == nil { + log.Panic("The services bucket does not exist") + } + if stb = tx.Bucket(database.BucketKeyServiceTokens); b == nil { + log.Panic("The service-tokens bucket does not exist") + } + + // Delete the entry from root services bucket + if b.Bucket(serviceID) == nil { + return errors.New("no service for the given id") + } + if err := b.DeleteBucket(serviceID); err != nil { + return err + } + + // Find the token entry and delete it from service-tokens bucket + c := stb.Cursor() + for token, id := c.First(); token != nil; token, id = c.Next() { + if string(id) == string(serviceID) { + return stb.Delete(token) + } + } + return errors.New("service id not found in the service-tokens bucket") + + // TODO: Cascade, i.e. delete all events for the given service + // Maybe this should be done in the HTTP request handler, though? + }) +} + +// Add adds a new service to monitor +// Returns error if the token is unavailable and if the transaction fails in any way +func Add(token string, service *Service) error { + db := database.GetDB() + return db.Update(func(tx *bbolt.Tx) error { + var err error + var b, sb, stb *bbolt.Bucket + var serviceIDint uint64 + var serviceID []byte + + // Get the services root bucket + if b = tx.Bucket(database.BucketKeyServices); b == nil { + log.Panic("The services bucket does not exist") + } + // Get the service-tokens root bucket + if stb = tx.Bucket(database.BucketKeyServiceTokens); stb == nil { + log.Panic("The service-tokens bucket does not exist") + } + + // Check if the service token is available, return error otherwise + serviceToken := []byte(token) + if serviceID = stb.Get(serviceToken); serviceID != nil { + return errors.New("a service has already been registered for the given token") + } + + // Create a new service bucket, sb, and populate it with data from service + if serviceIDint, err = b.NextSequence(); err != nil { + return err + } + serviceID = []byte(strconv.FormatUint(serviceIDint, 10)) + if sb, err = b.CreateBucket(serviceID); err != nil { + return err + } + if err = sb.Put(keyServiceName, []byte(service.Name)); err != nil { + return err + } + if err = sb.Put(keyServiceDescription, []byte(service.Description)); err != nil { + return err + } + periodStr := strconv.FormatUint(service.ExpectationPeriod, 10) + if err = sb.Put(keyServicePeriod, []byte(periodStr)); err != nil { + return err + } + + // Put an entry in the service-tokens bucket to map the token to the service + return stb.Put([]byte(token), serviceID) + }) +} + +// +// Package-local helpers +// + +// fromBucket populates the service struct with data from the given service bucket +// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket +func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) { + // Ignoring this error, because it shouldn't be possible + idInt := database.BytesToUint64(id) + service.ID = idInt + if name := sb.Get(keyServiceName); name != nil { + service.Name = string(name) + } + if description := sb.Get(keyServiceDescription); description != nil { + service.Description = string(description) + } + if period := sb.Get(keyServicePeriod); period != nil { + // Quick fix: Convert to string, then int + // Uses default value 0 if an error occurs + intPeriod, err := strconv.ParseUint(string(period), 10, 64) + if err == nil { + service.ExpectationPeriod = intPeriod + } else { + log.Println("Couldn't convert period to int for service") + service.ExpectationPeriod = 0 + } + } +} + +// get returns the service structure associated with the []byte-formatted service ID +func get(id []byte) *Service { + var service Service + db := database.GetDB() + err := db.View(func(tx *bbolt.Tx) error { + + // Get the root services bucket and the requested service bucket + var b, sb *bbolt.Bucket + if b = tx.Bucket(database.BucketKeyServices); b == nil { + log.Panic("The services bucket does not exist") + } + if sb = b.Bucket(id); sb == nil { + return errors.New("no service found for the given id") + } + + // Get service information from the bucket + service.fromBucket(id, sb) + return nil + }) + if err != nil { + log.Println(err) + return nil + } + return &service +} + +// getIDFromToken looks up the given token in the service-tokens bucket and returns +// the ID if it's found, otherwise returning nil +func getIDFromToken(token string) []byte { + var id []byte + db := database.GetDB() + db.View(func(tx *bbolt.Tx) error { + stb := tx.Bucket(database.BucketKeyServiceTokens) + if stb == nil { + log.Panic("The service-tokens bucket does not exist") + } + id = stb.Get([]byte(token)) + return nil + }) + return id +} diff --git a/internal/services/services_test.go b/internal/services/services_test.go @@ -0,0 +1,143 @@ +package services + +import ( + "fmt" + "os" + "sermoni/internal/database" + "strconv" + "testing" +) + +// intID gets uint64 from bytes +func intID(id []byte) uint64 { + idInt, err := strconv.ParseUint(string(id), 10, 64) + if err != nil { + return 0 + } + return idInt +} + +func (s1 *Service) equals(s2 *Service) bool { + switch { + case s1.ID != s2.ID: + return false + case s1.Name != s2.Name: + return false + case s1.Description != s2.Description: + return false + case s1.ExpectationPeriod != s2.ExpectationPeriod: + return false + default: + return true + } +} + +var ( + token1 = "my-great-token" + token2 = "my-other-token" + token3 = "my-third-token" +) + +var testServices = []*Service{ + { + Name: "tester @ dev-computer", + Description: "This describes the service in more detail", + ExpectationPeriod: 282342, + }, + {Name: "tester2", ExpectationPeriod: 300003}, + {Name: "third @ tester"}, +} + +func TestAddService(t *testing.T) { + err := Add(token1, testServices[0]) + if err != nil { + fmt.Println(err) + t.Fatal("unexpected error when adding service") + } + if err = Add(token2, testServices[1]); err != nil { + fmt.Println(err) + t.Fatal("unexpected error when adding second service") + } + if err = Add(token2, testServices[1]); err == nil { + t.Fatal("no error returned when trying to re-use a service token") + } + err = Add(token3, testServices[2]) + if err != nil { + t.Fatal("unexpected error when adding third service") + } + + // Simulate ID generation for testServices after adding them to DB, to avoid + // possible interferrence (shouldn't be a problem, but doesn't hurt to be sure). + // bbolt should always start ID sequences on 1, so this assumes that the service ID + // equals the testService index + 1 + for i, service := range testServices { + service.ID = uint64(i) + 1 + } +} + +func TestDeleteService(t *testing.T) { + var di uint64 = 1 // Deletion index + err := Delete(di + 1) + if err != nil { + fmt.Println(err) + t.Fatal("unexpected error when trying to delete service") + } + if err = Delete(di + 1); err == nil { + t.Fatal("no error returned when trying to delete non-existing service") + } + + // Assert that the service token is deleted too + if service := GetByToken(token2); service != nil { + t.Fatal("the service token was not deleted") + } + + // Delete from testServices too + testServices = append(testServices[:di], testServices[di+1:]...) +} + +func TestGetByID(t *testing.T) { + var gi uint64 = 0 // Get index + testService := testServices[gi] + service := GetByID(gi + 1) + if !service.equals(testService) { + t.Fatal("stored service doesn't match original") + } + if service = GetByID(23423); service != nil { + t.Fatal("returned service for invalid id") + } +} + +func TestGetByToken(t *testing.T) { + testService := testServices[1] + service := GetByToken(token3) + if !service.equals(testService) { + t.Fatal("stored service doesn't match original") + } + if service = GetByToken("not-a-token"); service != nil { + t.Fatal("returned service for invalid token") + } +} + +func TestGetAll(t *testing.T) { + services := GetAll() + for i, service := range services { + if !service.equals(testServices[i]) { + t.Fatal("stored service doesn't match original") + } + } + if services[0].equals(testServices[1]) { + t.Fatal("unexpected match between two services") + } +} + +func TestMain(m *testing.M) { + // (Re)create the test database + testDB := "test.db" + os.Remove(testDB) + if err := database.Init(testDB); err != nil { + print("Couldn't initialize test database") + os.Exit(1) + } + defer database.Close() + os.Exit(m.Run()) +} diff --git a/main.go b/main.go @@ -1,21 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "sermoni/database" -) - -func main() { - // TODO: Use getopt package instead of flags? - port := flag.Int("p", 8080, "Port") - dbFile := flag.String("d", "sermoni.db", "Database file") - //password := flag.String("w", "", "Password for the web interface") - flag.Parse() - if err := database.Init(*dbFile); err != nil { - log.Fatal(err) - } - defer database.Close() - fmt.Printf("Server running on port %v\n", *port) -} diff --git a/services/services.go b/services/services.go @@ -1,220 +0,0 @@ -package services - -import ( - "errors" - "log" - "sermoni/database" - "strconv" - - "go.etcd.io/bbolt" -) - -var ( - keyServiceID = []byte("id") - keyServiceName = []byte("name") - keyServiceDescription = []byte("description") - keyServicePeriod = []byte("period") -) - -// Service describes a service that is expected to report -type Service struct { - ID uint64 `json:"id"` // service id, an integer that represents the service - Name string `json:"name"` // service name, usually on the format 'service @ server' - Description string `json:"description"` // more detailed description of the service - ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?) -} - -// GetByToken returns the service structure associated with the token string, if there -// are any matching entries in service-tokens bucket. Returns nil if there are no matches -func GetByToken(token string) *Service { - id := getIDFromToken(token) - if id == nil { - log.Printf("no service found for the token '%v'\n", token) - return nil - } - return get(id) -} - -// GetByID returns the service structure associated with the given uint64-formatted -// service ID, if that service exists. Otherwise returns nil -func GetByID(id uint64) *Service { - return get([]byte(strconv.FormatUint(id, 10))) -} - -// GetAll returns all services in the database (TODO) -func GetAll() (services []*Service) { - db := database.GetDB() - db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(database.BucketKeyServices) - stb := tx.Bucket(database.BucketKeyServiceTokens) - if b == nil { - log.Panic("The services bucket does not exist") - } - if stb == nil { - log.Panic("The service-tokens bucket does not exist") - } - - // Go through all k-v pairs in the service *tokens* bucket, in order to get service bucket IDs - // Use the ID to get the service bucket and create service fromBucket - return stb.ForEach(func(_, id []byte) error { - sb := b.Bucket(id) - service := new(Service) - service.fromBucket(id, sb) - services = append(services, service) - return nil - }) - }) - return -} - -// Delete deletes the given service if it exists -func Delete(intID uint64) error { - db := database.GetDB() - serviceID := []byte(strconv.FormatUint(intID, 10)) - return db.Update(func(tx *bbolt.Tx) error { - var b, stb *bbolt.Bucket - if b = tx.Bucket(database.BucketKeyServices); b == nil { - log.Panic("The services bucket does not exist") - } - if stb = tx.Bucket(database.BucketKeyServiceTokens); b == nil { - log.Panic("The service-tokens bucket does not exist") - } - - // Delete the entry from root services bucket - if b.Bucket(serviceID) == nil { - return errors.New("no service for the given id") - } - if err := b.DeleteBucket(serviceID); err != nil { - return err - } - - // Find the token entry and delete it from service-tokens bucket - c := stb.Cursor() - for token, id := c.First(); token != nil; token, id = c.Next() { - if string(id) == string(serviceID) { - return stb.Delete(token) - } - } - return errors.New("service id not found in the service-tokens bucket") - }) -} - -// Add adds a new service to monitor -// Returns error if the token is unavailable and if the transaction fails in any way -func Add(token string, service *Service) error { - db := database.GetDB() - return db.Update(func(tx *bbolt.Tx) error { - var err error - var b, sb, stb *bbolt.Bucket - var serviceIDint uint64 - var serviceID []byte - - // Get the services root bucket - if b = tx.Bucket(database.BucketKeyServices); b == nil { - log.Panic("The services bucket does not exist") - } - // Get the service-tokens root bucket - if stb = tx.Bucket(database.BucketKeyServiceTokens); stb == nil { - log.Panic("The service-tokens bucket does not exist") - } - - // Check if the service token is available, return error otherwise - serviceToken := []byte(token) - if serviceID = stb.Get(serviceToken); serviceID != nil { - return errors.New("a service has already been registered for the given token") - } - - // Create a new service bucket, sb, and populate it with data from service - if serviceIDint, err = b.NextSequence(); err != nil { - return err - } - serviceID = []byte(strconv.FormatUint(serviceIDint, 10)) - if sb, err = b.CreateBucket(serviceID); err != nil { - return err - } - if err = sb.Put(keyServiceName, []byte(service.Name)); err != nil { - return err - } - if err = sb.Put(keyServiceDescription, []byte(service.Description)); err != nil { - return err - } - periodStr := strconv.FormatUint(service.ExpectationPeriod, 10) - if err = sb.Put(keyServicePeriod, []byte(periodStr)); err != nil { - return err - } - - // Put an entry in the service-tokens bucket to map the token to the service - return stb.Put([]byte(token), serviceID) - }) -} - -// -// Package-local helpers -// - -// fromBucket populates the service struct with data from the given service bucket -// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket -func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) { - // Ignoring this error, because it shouldn't be possible - idInt := database.BytesToUint64(id) - service.ID = idInt - if name := sb.Get(keyServiceName); name != nil { - service.Name = string(name) - } - if description := sb.Get(keyServiceDescription); description != nil { - service.Description = string(description) - } - if period := sb.Get(keyServicePeriod); period != nil { - // Quick fix: Convert to string, then int - // Uses default value 0 if an error occurs - intPeriod, err := strconv.ParseUint(string(period), 10, 64) - if err == nil { - service.ExpectationPeriod = intPeriod - } else { - log.Println("Couldn't convert period to int for service") - service.ExpectationPeriod = 0 - } - } -} - -// get returns the service structure associated with the []byte-formatted service ID -func get(id []byte) *Service { - var service Service - db := database.GetDB() - err := db.View(func(tx *bbolt.Tx) error { - - // Get the root services bucket and the requested service bucket - var b, sb *bbolt.Bucket - if b = tx.Bucket(database.BucketKeyServices); b == nil { - log.Panic("The services bucket does not exist") - } - if sb = b.Bucket(id); sb == nil { - return errors.New("no service found for the given id") - } - - // Get service information from the bucket - service.fromBucket(id, sb) - return nil - }) - if err != nil { - log.Println(err) - return nil - } - return &service -} - -// getIDFromToken looks up the given token in the service-tokens bucket and returns -// the ID if it's found, otherwise returning nil -func getIDFromToken(token string) []byte { - var id []byte - db := database.GetDB() - db.View(func(tx *bbolt.Tx) error { - stb := tx.Bucket(database.BucketKeyServiceTokens) - if stb == nil { - log.Panic("The service-tokens bucket does not exist") - } - id = stb.Get([]byte(token)) - return nil - }) - return id -} diff --git a/services/services_test.go b/services/services_test.go @@ -1,143 +0,0 @@ -package services - -import ( - "fmt" - "os" - "sermoni/database" - "strconv" - "testing" -) - -// intID gets uint64 from bytes -func intID(id []byte) uint64 { - idInt, err := strconv.ParseUint(string(id), 10, 64) - if err != nil { - return 0 - } - return idInt -} - -func (s1 *Service) equals(s2 *Service) bool { - switch { - case s1.ID != s2.ID: - return false - case s1.Name != s2.Name: - return false - case s1.Description != s2.Description: - return false - case s1.ExpectationPeriod != s2.ExpectationPeriod: - return false - default: - return true - } -} - -var ( - token1 = "my-great-token" - token2 = "my-other-token" - token3 = "my-third-token" -) - -var testServices = []*Service{ - { - Name: "tester @ dev-computer", - Description: "This describes the service in more detail", - ExpectationPeriod: 282342, - }, - {Name: "tester2", ExpectationPeriod: 300003}, - {Name: "third @ tester"}, -} - -func TestAddService(t *testing.T) { - err := Add(token1, testServices[0]) - if err != nil { - fmt.Println(err) - t.Fatal("unexpected error when adding service") - } - if err = Add(token2, testServices[1]); err != nil { - fmt.Println(err) - t.Fatal("unexpected error when adding second service") - } - if err = Add(token2, testServices[1]); err == nil { - t.Fatal("no error returned when trying to re-use a service token") - } - err = Add(token3, testServices[2]) - if err != nil { - t.Fatal("unexpected error when adding third service") - } - - // Simulate ID generation for testServices after adding them to DB, to avoid - // possible interferrence (shouldn't be a problem, but doesn't hurt to be sure). - // bbolt should always start ID sequences on 1, so this assumes that the service ID - // equals the testService index + 1 - for i, service := range testServices { - service.ID = uint64(i) + 1 - } -} - -func TestDeleteService(t *testing.T) { - var di uint64 = 1 // Deletion index - err := Delete(di + 1) - if err != nil { - fmt.Println(err) - t.Fatal("unexpected error when trying to delete service") - } - if err = Delete(di + 1); err == nil { - t.Fatal("no error returned when trying to delete non-existing service") - } - - // Assert that the service token is deleted too - if service := GetByToken(token2); service != nil { - t.Fatal("the service token was not deleted") - } - - // Delete from testServices too - testServices = append(testServices[:di], testServices[di+1:]...) -} - -func TestGetByID(t *testing.T) { - var gi uint64 = 0 // Get index - testService := testServices[gi] - service := GetByID(gi + 1) - if !service.equals(testService) { - t.Fatal("stored service doesn't match original") - } - if service = GetByID(23423); service != nil { - t.Fatal("returned service for invalid id") - } -} - -func TestGetByToken(t *testing.T) { - testService := testServices[1] - service := GetByToken(token3) - if !service.equals(testService) { - t.Fatal("stored service doesn't match original") - } - if service = GetByToken("not-a-token"); service != nil { - t.Fatal("returned service for invalid token") - } -} - -func TestGetAll(t *testing.T) { - services := GetAll() - for i, service := range services { - if !service.equals(testServices[i]) { - t.Fatal("stored service doesn't match original") - } - } - if services[0].equals(testServices[1]) { - t.Fatal("unexpected match between two services") - } -} - -func TestMain(m *testing.M) { - // (Re)create the test database - testDB := "test.db" - os.Remove(testDB) - if err := database.Init(testDB); err != nil { - print("Couldn't initialize test database") - os.Exit(1) - } - defer database.Close() - os.Exit(m.Run()) -} diff --git a/proto.html b/ui/proto.html diff --git a/style.css b/ui/style.css