From dfa112c933a67bb53f22c45033683d7f1e1c19d6 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 3 Apr 2025 12:07:29 +0200 Subject: [PATCH 01/10] feat: node last seen --- node-registrar/pkg/db/db.go | 26 ++++++++++++++++++++ node-registrar/pkg/db/models.go | 8 +++--- node-registrar/pkg/db/nodes.go | 15 ++++++++++++ node-registrar/pkg/server/handlers.go | 35 ++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/node-registrar/pkg/db/db.go b/node-registrar/pkg/db/db.go index 6e32ca9..648f6d5 100644 --- a/node-registrar/pkg/db/db.go +++ b/node-registrar/pkg/db/db.go @@ -54,6 +54,12 @@ func NewDB(c Config) (Database, error) { return Database{}, err } + // Run the data migration for LastSeen field + err = db.MigrateNodeLastSeen() + if err != nil { + return Database{}, errors.Wrap(err, "failed to migrate node last seen data") + } + return db, sql.Ping() } @@ -134,3 +140,23 @@ func (db Database) Close() error { } return nil } + +// MigrateNodeLastSeen updates the LastSeen field for existing nodes +func (db Database) MigrateNodeLastSeen() error { + // Updates all nodes with the latest timestamp from their uptime reports + query := ` + UPDATE nodes n + SET last_seen = ( + SELECT MAX(timestamp) + FROM uptime_reports ur + WHERE ur.node_id = n.node_id + ) + WHERE EXISTS ( + SELECT 1 + FROM uptime_reports ur + WHERE ur.node_id = n.node_id + ) + ` + + return db.gormDB.Exec(query).Error +} diff --git a/node-registrar/pkg/db/models.go b/node-registrar/pkg/db/models.go index 5c3a815..8149afc 100644 --- a/node-registrar/pkg/db/models.go +++ b/node-registrar/pkg/db/models.go @@ -48,10 +48,10 @@ type Node struct { SerialNumber string `json:"serial_number"` UptimeReports []UptimeReport `json:"uptime" gorm:"foreignKey:NodeID;references:NodeID;constraint:OnDelete:CASCADE"` - - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approved bool `json:"approved"` + LastSeen time.Time `json:"last_seen" gorm:"index"` // Last time the node sent Uptime report + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approved bool `json:"approved"` } type UptimeReport struct { diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index aa52f5d..fd44def 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -77,6 +77,21 @@ func (db *Database) GetUptimeReports(nodeID uint64, start, end time.Time) ([]Upt return reports, result.Error } +// Update Node Last Seen +func (db *Database) UpdateNodeLastSeen(nodeID uint64, lastSeen time.Time) error { + result := db.gormDB.Model(&Node{}). + Where("node_id = ?", nodeID). + Update("last_seen", lastSeen) + + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrRecordNotFound + } + return nil +} + func (db *Database) CreateUptimeReport(report *UptimeReport) error { return db.gormDB.Create(report).Error } diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 0e2534e..d372dd4 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -15,7 +15,8 @@ import ( ) const ( - MaxTimestampDelta = 2 * time.Second + MaxTimestampDelta = 2 * time.Second + UptimeReportTimestampHintDrift int64 = 60 ) // @title Node Registrar API @@ -457,6 +458,13 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { // Maybe aggregate reports here and store total uptime? // The total uptime should accumulate unless the node restarts, which is detected when the reported uptime is less than the previous value. + // Ensuring the timestamp_hint is within an Acceptable Range + err = validateTimestampHint(req.Timestamp.Unix()) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid timestamp hint"}) + return + } + // Create report record report := &db.UptimeReport{ NodeID: id, @@ -469,6 +477,14 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save report"}) return } + // Update node LastSeen + // We only store the timestamp of the last report + // It's up to the clients to determine if the node is online based on the reporting interval and allowable window. + err = s.db.UpdateNodeLastSeen(id, req.Timestamp) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update node last seen"}) + return + } c.JSON(http.StatusCreated, gin.H{"message": "uptime reported successfully"}) } @@ -783,3 +799,20 @@ func ensureOwner(c *gin.Context, twinID uint64) { return } } + +// Helper function to validate timestamp hint +func validateTimestampHint(timestampHint int64) error { + // Get the current timestamp in seconds + now := time.Now().Unix() + + // Calculate acceptable range + lowerBound := now - min(now, UptimeReportTimestampHintDrift) + upperBound := now + UptimeReportTimestampHintDrift + + // Ensure timestampHint is within the range + if timestampHint < lowerBound || timestampHint > upperBound { + return errors.New("InvalidTimestampHint") + } + + return nil +} From 1931b84f737ee31f0b6542a986eb6c05c1abd35e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 3 Apr 2025 12:31:57 +0200 Subject: [PATCH 02/10] feat: node last seen --- node-registrar/pkg/db/db.go | 20 ++++++++++++++------ node-registrar/pkg/db/nodes.go | 17 ++++++++++++++++- node-registrar/pkg/server/handlers.go | 12 +++--------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/node-registrar/pkg/db/db.go b/node-registrar/pkg/db/db.go index 648f6d5..b7d7518 100644 --- a/node-registrar/pkg/db/db.go +++ b/node-registrar/pkg/db/db.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -55,10 +56,11 @@ func NewDB(c Config) (Database, error) { } // Run the data migration for LastSeen field - err = db.MigrateNodeLastSeen() + count, err := db.MigrateNodeLastSeen() if err != nil { return Database{}, errors.Wrap(err, "failed to migrate node last seen data") } + log.Info().Msgf("Migration: Updated LastSeen for %d nodes", count) return db, sql.Ping() } @@ -141,9 +143,13 @@ func (db Database) Close() error { return nil } -// MigrateNodeLastSeen updates the LastSeen field for existing nodes -func (db Database) MigrateNodeLastSeen() error { - // Updates all nodes with the latest timestamp from their uptime reports +// Transaction executes operations within a database transaction +func (db *Database) Transaction(fn func(tx *gorm.DB) error) error { + return db.gormDB.Transaction(fn) +} + +// MigrateNodeLastSeen updates the LastSeen field for existing nodes that don't have it set +func (db Database) MigrateNodeLastSeen() (int64, error) { query := ` UPDATE nodes n SET last_seen = ( @@ -151,12 +157,14 @@ func (db Database) MigrateNodeLastSeen() error { FROM uptime_reports ur WHERE ur.node_id = n.node_id ) - WHERE EXISTS ( + WHERE (last_seen IS NULL OR last_seen = '0001-01-01 00:00:00+00') + AND EXISTS ( SELECT 1 FROM uptime_reports ur WHERE ur.node_id = n.node_id ) ` - return db.gormDB.Exec(query).Error + result := db.gormDB.Exec(query) + return result.RowsAffected, result.Error } diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index fd44def..46d1dd5 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -92,8 +92,23 @@ func (db *Database) UpdateNodeLastSeen(nodeID uint64, lastSeen time.Time) error return nil } +// CreateUptimeReportAndUpdateLastSeen creates an uptime report and updates the node's LastSeen field in a single transaction func (db *Database) CreateUptimeReport(report *UptimeReport) error { - return db.gormDB.Create(report).Error + return db.gormDB.Transaction(func(tx *gorm.DB) error { + // Create the uptime report + if err := tx.Create(report).Error; err != nil { + return err + } + + // Update the node's LastSeen field + if err := tx.Model(&Node{}). + Where("node_id = ?", report.NodeID). + Update("last_seen", report.Timestamp).Error; err != nil { + return err + } + + return nil + }) } func (db *Database) SetZOSVersion(version string) error { diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index d372dd4..63f22c0 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -472,17 +472,11 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { Timestamp: req.Timestamp, } - err = s.db.CreateUptimeReport(report) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save report"}) - return - } - // Update node LastSeen - // We only store the timestamp of the last report + // Create report record and Update node LastSeen(the timestamp of the last report) // It's up to the clients to determine if the node is online based on the reporting interval and allowable window. - err = s.db.UpdateNodeLastSeen(id, req.Timestamp) + err = s.db.CreateUptimeReport(report) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update node last seen"}) + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to process uptime report"}) return } From 8d6ea6af517599f1f5d1a9718b984c8885bf27ce Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 3 Apr 2025 12:44:41 +0200 Subject: [PATCH 03/10] update swagger files --- node-registrar/docs/docs.go | 166 ++++++++++++++++++++++--------- node-registrar/docs/swagger.json | 164 +++++++++++++++++++++--------- node-registrar/docs/swagger.yaml | 143 ++++++++++++++++++-------- 3 files changed, 339 insertions(+), 134 deletions(-) diff --git a/node-registrar/docs/docs.go b/node-registrar/docs/docs.go index c46e734..d2b3d9d 100644 --- a/node-registrar/docs/docs.go +++ b/node-registrar/docs/docs.go @@ -46,7 +46,7 @@ const docTemplate = `{ "200": { "description": "Account details", "schema": { - "$ref": "#/definitions/db.Account" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account" } }, "400": { @@ -84,7 +84,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.AccountCreationRequest" + "$ref": "#/definitions/pkg_server.AccountCreationRequest" } } ], @@ -92,7 +92,7 @@ const docTemplate = `{ "201": { "description": "Created account details", "schema": { - "$ref": "#/definitions/db.Account" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account" } }, "400": { @@ -146,7 +146,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateAccountRequest" + "$ref": "#/definitions/pkg_server.UpdateAccountRequest" } } ], @@ -235,7 +235,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } } }, @@ -274,7 +274,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } } ], @@ -338,7 +338,7 @@ const docTemplate = `{ "200": { "description": "Farm details", "schema": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } }, "400": { @@ -390,7 +390,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateFarmRequest" + "$ref": "#/definitions/pkg_server.UpdateFarmRequest" } } ], @@ -491,7 +491,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } } }, @@ -530,7 +530,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.NodeRegistrationRequest" + "$ref": "#/definitions/pkg_server.NodeRegistrationRequest" } } ], @@ -594,7 +594,7 @@ const docTemplate = `{ "200": { "description": "Node details", "schema": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } }, "400": { @@ -646,7 +646,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateNodeRequest" + "$ref": "#/definitions/pkg_server.UpdateNodeRequest" } } ], @@ -716,7 +716,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UptimeReportRequest" + "$ref": "#/definitions/pkg_server.UptimeReportRequest" } } ], @@ -811,7 +811,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.ZOSVersionRequest" + "$ref": "#/definitions/pkg_server.ZOSVersionRequest" } } ], @@ -856,7 +856,7 @@ const docTemplate = `{ } }, "definitions": { - "db.Account": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account": { "type": "object", "properties": { "created_at": { @@ -866,7 +866,7 @@ const docTemplate = `{ "description": "Relations | likely we need to use OnDelete:RESTRICT (Prevent Twin deletion if farms exist)\n@swagger:ignore", "type": "array", "items": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } }, "public_key": { @@ -892,8 +892,13 @@ const docTemplate = `{ } } }, - "db.Farm": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm": { "type": "object", + "required": [ + "farm_name", + "stellar_address", + "twin_id" + ], "properties": { "created_at": { "type": "string" @@ -911,7 +916,7 @@ const docTemplate = `{ "description": "@swagger:ignore", "type": "array", "items": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } }, "stellar_address": { @@ -926,7 +931,7 @@ const docTemplate = `{ } } }, - "db.Interface": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface": { "type": "object", "properties": { "ips": { @@ -940,7 +945,7 @@ const docTemplate = `{ } } }, - "db.Location": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location": { "type": "object", "properties": { "city": { @@ -957,7 +962,7 @@ const docTemplate = `{ } } }, - "db.Node": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node": { "type": "object", "properties": { "approved": { @@ -973,11 +978,15 @@ const docTemplate = `{ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, + "last_seen": { + "description": "Last time the node sent Uptime report", + "type": "string" + }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "node_id": { "type": "integer" @@ -986,7 +995,7 @@ const docTemplate = `{ "description": "PublicConfig PublicConfig ` + "`" + `json:\"public_config\" gorm:\"type:json\"` + "`" + `", "allOf": [ { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" } ] }, @@ -1006,7 +1015,7 @@ const docTemplate = `{ "uptime": { "type": "array", "items": { - "$ref": "#/definitions/db.UptimeReport" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport" } }, "virtualized": { @@ -1014,7 +1023,7 @@ const docTemplate = `{ } } }, - "db.Resources": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources": { "type": "object", "properties": { "cru": { @@ -1031,7 +1040,7 @@ const docTemplate = `{ } } }, - "db.UptimeReport": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport": { "type": "object", "properties": { "created_at": { @@ -1056,7 +1065,7 @@ const docTemplate = `{ } } }, - "server.AccountCreationRequest": { + "pkg_server.AccountCreationRequest": { "type": "object", "required": [ "public_key", @@ -1086,7 +1095,7 @@ const docTemplate = `{ } } }, - "server.NodeRegistrationRequest": { + "pkg_server.NodeRegistrationRequest": { "type": "object", "required": [ "farm_id", @@ -1104,14 +1113,14 @@ const docTemplate = `{ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "resources": { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" }, "secure_boot": { "type": "boolean" @@ -1128,10 +1137,21 @@ const docTemplate = `{ } } }, - "server.UpdateAccountRequest": { - "type": "object" + "pkg_server.UpdateAccountRequest": { + "type": "object", + "properties": { + "relays": { + "type": "array", + "items": { + "type": "string" + } + }, + "rmb_enc_key": { + "type": "string" + } + } }, - "server.UpdateFarmRequest": { + "pkg_server.UpdateFarmRequest": { "type": "object", "properties": { "farm_name": { @@ -1139,12 +1159,11 @@ const docTemplate = `{ "maxLength": 40 }, "stellar_address": { - "type": "string", - "maxLength": 56 + "type": "string" } } }, - "server.UpdateNodeRequest": { + "pkg_server.UpdateNodeRequest": { "type": "object", "required": [ "farm_id", @@ -1161,14 +1180,14 @@ const docTemplate = `{ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "resources": { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" }, "secure_boot": { "type": "boolean" @@ -1181,10 +1200,22 @@ const docTemplate = `{ } } }, - "server.UptimeReportRequest": { - "type": "object" + "pkg_server.UptimeReportRequest": { + "type": "object", + "required": [ + "timestamp", + "uptime" + ], + "properties": { + "timestamp": { + "type": "string" + }, + "uptime": { + "$ref": "#/definitions/time.Duration" + } + } }, - "server.ZOSVersionRequest": { + "pkg_server.ZOSVersionRequest": { "type": "object", "required": [ "version" @@ -1194,18 +1225,57 @@ const docTemplate = `{ "type": "string" } } + }, + "time.Duration": { + "type": "integer", + "enum": [ + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000, + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000 + ], + "x-enum-varnames": [ + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour", + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour" + ] } } }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "", + Version: "1.0", Host: "", - BasePath: "", + BasePath: "/v1", Schemes: []string{}, - Title: "", - Description: "", + Title: "Node Registrar API", + Description: "API for managing TFGrid node registration", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/node-registrar/docs/swagger.json b/node-registrar/docs/swagger.json index d05ca7f..adb9a75 100644 --- a/node-registrar/docs/swagger.json +++ b/node-registrar/docs/swagger.json @@ -1,8 +1,12 @@ { "swagger": "2.0", "info": { - "contact": {} + "description": "API for managing TFGrid node registration", + "title": "Node Registrar API", + "contact": {}, + "version": "1.0" }, + "basePath": "/v1", "paths": { "/accounts": { "get": { @@ -35,7 +39,7 @@ "200": { "description": "Account details", "schema": { - "$ref": "#/definitions/db.Account" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account" } }, "400": { @@ -73,7 +77,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.AccountCreationRequest" + "$ref": "#/definitions/pkg_server.AccountCreationRequest" } } ], @@ -81,7 +85,7 @@ "201": { "description": "Created account details", "schema": { - "$ref": "#/definitions/db.Account" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account" } }, "400": { @@ -135,7 +139,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateAccountRequest" + "$ref": "#/definitions/pkg_server.UpdateAccountRequest" } } ], @@ -224,7 +228,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } } }, @@ -263,7 +267,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } } ], @@ -327,7 +331,7 @@ "200": { "description": "Farm details", "schema": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } }, "400": { @@ -379,7 +383,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateFarmRequest" + "$ref": "#/definitions/pkg_server.UpdateFarmRequest" } } ], @@ -480,7 +484,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } } }, @@ -519,7 +523,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.NodeRegistrationRequest" + "$ref": "#/definitions/pkg_server.NodeRegistrationRequest" } } ], @@ -583,7 +587,7 @@ "200": { "description": "Node details", "schema": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } }, "400": { @@ -635,7 +639,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UpdateNodeRequest" + "$ref": "#/definitions/pkg_server.UpdateNodeRequest" } } ], @@ -705,7 +709,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.UptimeReportRequest" + "$ref": "#/definitions/pkg_server.UptimeReportRequest" } } ], @@ -800,7 +804,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/server.ZOSVersionRequest" + "$ref": "#/definitions/pkg_server.ZOSVersionRequest" } } ], @@ -845,7 +849,7 @@ } }, "definitions": { - "db.Account": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account": { "type": "object", "properties": { "created_at": { @@ -855,7 +859,7 @@ "description": "Relations | likely we need to use OnDelete:RESTRICT (Prevent Twin deletion if farms exist)\n@swagger:ignore", "type": "array", "items": { - "$ref": "#/definitions/db.Farm" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm" } }, "public_key": { @@ -881,8 +885,13 @@ } } }, - "db.Farm": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm": { "type": "object", + "required": [ + "farm_name", + "stellar_address", + "twin_id" + ], "properties": { "created_at": { "type": "string" @@ -900,7 +909,7 @@ "description": "@swagger:ignore", "type": "array", "items": { - "$ref": "#/definitions/db.Node" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node" } }, "stellar_address": { @@ -915,7 +924,7 @@ } } }, - "db.Interface": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface": { "type": "object", "properties": { "ips": { @@ -929,7 +938,7 @@ } } }, - "db.Location": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location": { "type": "object", "properties": { "city": { @@ -946,7 +955,7 @@ } } }, - "db.Node": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node": { "type": "object", "properties": { "approved": { @@ -962,11 +971,15 @@ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, + "last_seen": { + "description": "Last time the node sent Uptime report", + "type": "string" + }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "node_id": { "type": "integer" @@ -975,7 +988,7 @@ "description": "PublicConfig PublicConfig `json:\"public_config\" gorm:\"type:json\"`", "allOf": [ { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" } ] }, @@ -995,7 +1008,7 @@ "uptime": { "type": "array", "items": { - "$ref": "#/definitions/db.UptimeReport" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport" } }, "virtualized": { @@ -1003,7 +1016,7 @@ } } }, - "db.Resources": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources": { "type": "object", "properties": { "cru": { @@ -1020,7 +1033,7 @@ } } }, - "db.UptimeReport": { + "github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport": { "type": "object", "properties": { "created_at": { @@ -1045,7 +1058,7 @@ } } }, - "server.AccountCreationRequest": { + "pkg_server.AccountCreationRequest": { "type": "object", "required": [ "public_key", @@ -1075,7 +1088,7 @@ } } }, - "server.NodeRegistrationRequest": { + "pkg_server.NodeRegistrationRequest": { "type": "object", "required": [ "farm_id", @@ -1093,14 +1106,14 @@ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "resources": { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" }, "secure_boot": { "type": "boolean" @@ -1117,10 +1130,21 @@ } } }, - "server.UpdateAccountRequest": { - "type": "object" + "pkg_server.UpdateAccountRequest": { + "type": "object", + "properties": { + "relays": { + "type": "array", + "items": { + "type": "string" + } + }, + "rmb_enc_key": { + "type": "string" + } + } }, - "server.UpdateFarmRequest": { + "pkg_server.UpdateFarmRequest": { "type": "object", "properties": { "farm_name": { @@ -1128,12 +1152,11 @@ "maxLength": 40 }, "stellar_address": { - "type": "string", - "maxLength": 56 + "type": "string" } } }, - "server.UpdateNodeRequest": { + "pkg_server.UpdateNodeRequest": { "type": "object", "required": [ "farm_id", @@ -1150,14 +1173,14 @@ "interfaces": { "type": "array", "items": { - "$ref": "#/definitions/db.Interface" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface" } }, "location": { - "$ref": "#/definitions/db.Location" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location" }, "resources": { - "$ref": "#/definitions/db.Resources" + "$ref": "#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources" }, "secure_boot": { "type": "boolean" @@ -1170,10 +1193,22 @@ } } }, - "server.UptimeReportRequest": { - "type": "object" + "pkg_server.UptimeReportRequest": { + "type": "object", + "required": [ + "timestamp", + "uptime" + ], + "properties": { + "timestamp": { + "type": "string" + }, + "uptime": { + "$ref": "#/definitions/time.Duration" + } + } }, - "server.ZOSVersionRequest": { + "pkg_server.ZOSVersionRequest": { "type": "object", "required": [ "version" @@ -1183,6 +1218,45 @@ "type": "string" } } + }, + "time.Duration": { + "type": "integer", + "enum": [ + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000, + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000 + ], + "x-enum-varnames": [ + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour", + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour" + ] } } } \ No newline at end of file diff --git a/node-registrar/docs/swagger.yaml b/node-registrar/docs/swagger.yaml index d2b3482..a152472 100644 --- a/node-registrar/docs/swagger.yaml +++ b/node-registrar/docs/swagger.yaml @@ -1,5 +1,6 @@ +basePath: /v1 definitions: - db.Account: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account: properties: created_at: type: string @@ -8,7 +9,7 @@ definitions: Relations | likely we need to use OnDelete:RESTRICT (Prevent Twin deletion if farms exist) @swagger:ignore items: - $ref: '#/definitions/db.Farm' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm' type: array public_key: description: |- @@ -28,7 +29,7 @@ definitions: updated_at: type: string type: object - db.Farm: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm: properties: created_at: type: string @@ -41,7 +42,7 @@ definitions: nodes: description: '@swagger:ignore' items: - $ref: '#/definitions/db.Node' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node' type: array stellar_address: type: string @@ -50,8 +51,12 @@ definitions: type: integer updated_at: type: string + required: + - farm_name + - stellar_address + - twin_id type: object - db.Interface: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface: properties: ips: type: string @@ -60,7 +65,7 @@ definitions: name: type: string type: object - db.Location: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location: properties: city: type: string @@ -71,7 +76,7 @@ definitions: longitude: type: string type: object - db.Node: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node: properties: approved: type: boolean @@ -83,15 +88,18 @@ definitions: type: integer interfaces: items: - $ref: '#/definitions/db.Interface' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface' type: array + last_seen: + description: Last time the node sent Uptime report + type: string location: - $ref: '#/definitions/db.Location' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location' node_id: type: integer resources: allOf: - - $ref: '#/definitions/db.Resources' + - $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources' description: PublicConfig PublicConfig `json:"public_config" gorm:"type:json"` secure_boot: type: boolean @@ -104,12 +112,12 @@ definitions: type: string uptime: items: - $ref: '#/definitions/db.UptimeReport' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport' type: array virtualized: type: boolean type: object - db.Resources: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources: properties: cru: type: integer @@ -120,7 +128,7 @@ definitions: sru: type: integer type: object - db.UptimeReport: + github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.UptimeReport: properties: created_at: type: string @@ -137,7 +145,7 @@ definitions: description: True if this report followed a restart type: boolean type: object - server.AccountCreationRequest: + pkg_server.AccountCreationRequest: properties: public_key: description: base64 encoded @@ -160,19 +168,19 @@ definitions: - signature - timestamp type: object - server.NodeRegistrationRequest: + pkg_server.NodeRegistrationRequest: properties: farm_id: minimum: 1 type: integer interfaces: items: - $ref: '#/definitions/db.Interface' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface' type: array location: - $ref: '#/definitions/db.Location' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location' resources: - $ref: '#/definitions/db.Resources' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources' secure_boot: type: boolean serial_number: @@ -190,30 +198,36 @@ definitions: - serial_number - twin_id type: object - server.UpdateAccountRequest: + pkg_server.UpdateAccountRequest: + properties: + relays: + items: + type: string + type: array + rmb_enc_key: + type: string type: object - server.UpdateFarmRequest: + pkg_server.UpdateFarmRequest: properties: farm_name: maxLength: 40 type: string stellar_address: - maxLength: 56 type: string type: object - server.UpdateNodeRequest: + pkg_server.UpdateNodeRequest: properties: farm_id: minimum: 1 type: integer interfaces: items: - $ref: '#/definitions/db.Interface' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Interface' type: array location: - $ref: '#/definitions/db.Location' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Location' resources: - $ref: '#/definitions/db.Resources' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Resources' secure_boot: type: boolean serial_number: @@ -227,17 +241,64 @@ definitions: - resources - serial_number type: object - server.UptimeReportRequest: + pkg_server.UptimeReportRequest: + properties: + timestamp: + type: string + uptime: + $ref: '#/definitions/time.Duration' + required: + - timestamp + - uptime type: object - server.ZOSVersionRequest: + pkg_server.ZOSVersionRequest: properties: version: type: string required: - version type: object + time.Duration: + enum: + - -9223372036854775808 + - 9223372036854775807 + - 1 + - 1000 + - 1000000 + - 1000000000 + - 60000000000 + - 3600000000000 + - -9223372036854775808 + - 9223372036854775807 + - 1 + - 1000 + - 1000000 + - 1000000000 + - 60000000000 + - 3600000000000 + type: integer + x-enum-varnames: + - minDuration + - maxDuration + - Nanosecond + - Microsecond + - Millisecond + - Second + - Minute + - Hour + - minDuration + - maxDuration + - Nanosecond + - Microsecond + - Millisecond + - Second + - Minute + - Hour info: contact: {} + description: API for managing TFGrid node registration + title: Node Registrar API + version: "1.0" paths: /accounts: get: @@ -259,7 +320,7 @@ paths: "200": description: Account details schema: - $ref: '#/definitions/db.Account' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account' "400": description: Invalid request schema: @@ -283,14 +344,14 @@ paths: name: request required: true schema: - $ref: '#/definitions/server.AccountCreationRequest' + $ref: '#/definitions/pkg_server.AccountCreationRequest' produces: - application/json responses: "201": description: Created account details schema: - $ref: '#/definitions/db.Account' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Account' "400": description: Invalid request schema: @@ -325,7 +386,7 @@ paths: name: account required: true schema: - $ref: '#/definitions/server.UpdateAccountRequest' + $ref: '#/definitions/pkg_server.UpdateAccountRequest' produces: - application/json responses: @@ -387,7 +448,7 @@ paths: description: List of farms schema: items: - $ref: '#/definitions/db.Farm' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm' type: array "400": description: Bad request @@ -412,7 +473,7 @@ paths: name: farm required: true schema: - $ref: '#/definitions/db.Farm' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm' produces: - application/json responses: @@ -457,7 +518,7 @@ paths: "200": description: Farm details schema: - $ref: '#/definitions/db.Farm' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Farm' "400": description: Invalid farm ID schema: @@ -491,7 +552,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/server.UpdateFarmRequest' + $ref: '#/definitions/pkg_server.UpdateFarmRequest' produces: - application/json responses: @@ -561,7 +622,7 @@ paths: description: List of nodes schema: items: - $ref: '#/definitions/db.Node' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node' type: array "400": description: Bad request @@ -586,7 +647,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/server.NodeRegistrationRequest' + $ref: '#/definitions/pkg_server.NodeRegistrationRequest' produces: - application/json responses: @@ -631,7 +692,7 @@ paths: "200": description: Node details schema: - $ref: '#/definitions/db.Node' + $ref: '#/definitions/github_com_threefoldtech_tfgrid4-sdk-go_node-registrar_pkg_db.Node' "400": description: Invalid node ID schema: @@ -665,7 +726,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/server.UpdateNodeRequest' + $ref: '#/definitions/pkg_server.UpdateNodeRequest' produces: - application/json responses: @@ -713,7 +774,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/server.UptimeReportRequest' + $ref: '#/definitions/pkg_server.UptimeReportRequest' produces: - application/json responses: @@ -778,7 +839,7 @@ paths: name: body required: true schema: - $ref: '#/definitions/server.ZOSVersionRequest' + $ref: '#/definitions/pkg_server.ZOSVersionRequest' produces: - application/json responses: From 233b8f208076070301b48ef4acf738b08f4bb27b Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 3 Apr 2025 12:45:54 +0200 Subject: [PATCH 04/10] clean up unused UpdateNodeLastSeen method --- node-registrar/pkg/db/nodes.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index 46d1dd5..e96e632 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -77,21 +77,6 @@ func (db *Database) GetUptimeReports(nodeID uint64, start, end time.Time) ([]Upt return reports, result.Error } -// Update Node Last Seen -func (db *Database) UpdateNodeLastSeen(nodeID uint64, lastSeen time.Time) error { - result := db.gormDB.Model(&Node{}). - Where("node_id = ?", nodeID). - Update("last_seen", lastSeen) - - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrRecordNotFound - } - return nil -} - // CreateUptimeReportAndUpdateLastSeen creates an uptime report and updates the node's LastSeen field in a single transaction func (db *Database) CreateUptimeReport(report *UptimeReport) error { return db.gormDB.Transaction(func(tx *gorm.DB) error { From e233a6e432c5732f3fbdf700d2b83a5c895d2ef9 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 3 Apr 2025 15:17:57 +0200 Subject: [PATCH 05/10] ensure UTC in in uptimeReportHandler --- node-registrar/pkg/server/handlers.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 63f22c0..bc80ec2 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -410,8 +410,8 @@ func (s *Server) updateNodeHandler(c *gin.Context) { } type UptimeReportRequest struct { - Uptime time.Duration `json:"uptime" binding:"required"` - Timestamp time.Time `json:"timestamp" binding:"required"` + Uptime uint64 `json:"uptime" binding:"required"` + Timestamp uint64 `json:"timestamp" binding:"required"` } // @Summary Report node uptime @@ -459,7 +459,7 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { // The total uptime should accumulate unless the node restarts, which is detected when the reported uptime is less than the previous value. // Ensuring the timestamp_hint is within an Acceptable Range - err = validateTimestampHint(req.Timestamp.Unix()) + err = validateTimestampHint(req.Timestamp) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid timestamp hint"}) return @@ -468,8 +468,8 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { // Create report record report := &db.UptimeReport{ NodeID: id, - Duration: req.Uptime, - Timestamp: req.Timestamp, + Duration: time.Duration(req.Uptime) * time.Second, + Timestamp: time.Unix(int64(req.Timestamp), 0).UTC(), } // Create report record and Update node LastSeen(the timestamp of the last report) @@ -795,16 +795,18 @@ func ensureOwner(c *gin.Context, twinID uint64) { } // Helper function to validate timestamp hint -func validateTimestampHint(timestampHint int64) error { - // Get the current timestamp in seconds - now := time.Now().Unix() +func validateTimestampHint(timestampHint uint64) error { + hintTime := time.Unix(int64(timestampHint), 0) + + now := time.Now() // Calculate acceptable range - lowerBound := now - min(now, UptimeReportTimestampHintDrift) - upperBound := now + UptimeReportTimestampHintDrift + maxDrift := time.Duration(UptimeReportTimestampHintDrift) * time.Second + earliestAllowed := now.Add(-maxDrift) + latestAllowed := now.Add(maxDrift) - // Ensure timestampHint is within the range - if timestampHint < lowerBound || timestampHint > upperBound { + // Check if the hint is within the acceptable range + if hintTime.Before(earliestAllowed) || hintTime.After(latestAllowed) { return errors.New("InvalidTimestampHint") } From e0458efad2cd726fb2ea9b64cbdf26acce6f223c Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Wed, 30 Apr 2025 17:18:00 +0300 Subject: [PATCH 06/10] Add online and last seen filters Co-authored-by: xmonader --- node-registrar/client/node.go | 38 ++++++++++++++++++++++----- node-registrar/client/types.go | 2 ++ node-registrar/pkg/db/models.go | 13 +++++---- node-registrar/pkg/db/nodes.go | 22 ++++++++++++++++ node-registrar/pkg/server/handlers.go | 16 +++++++++-- 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index b2ba5a8..82a575d 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -59,6 +59,8 @@ type nodeCfg struct { twinID uint64 status string healthy bool + online *bool + lastSeen *int64 Location Location Resources Resources Interfaces []Interface @@ -100,6 +102,18 @@ func ListNodesWithHealthy() ListNodeOpts { } } +func ListNodesWithOnline(online bool) ListNodeOpts { + return func(n *nodeCfg) { + n.online = &online + } +} + +func ListNodesWithLastSeen(minutes int64) ListNodeOpts { + return func(n *nodeCfg) { + n.lastSeen = &minutes + } +} + func ListNodesWithTwinID(id uint64) ListNodeOpts { return func(n *nodeCfg) { n.twinID = id @@ -483,13 +497,15 @@ func (c *RegistrarClient) parseUpdateNodeOpts(node Node, opts []UpdateNodeOpts) func parseListNodeOpts(opts []ListNodeOpts) map[string]any { cfg := nodeCfg{ - nodeID: 0, - twinID: 0, - farmID: 0, - status: "", - healthy: false, - size: 50, - page: 1, + nodeID: 0, + twinID: 0, + farmID: 0, + status: "", + healthy: false, + online: nil, + lastSeen: nil, + size: 50, + page: 1, } for _, opt := range opts { @@ -518,6 +534,14 @@ func parseListNodeOpts(opts []ListNodeOpts) map[string]any { data["healthy"] = cfg.healthy } + if cfg.online != nil { + data["online"] = *cfg.online + } + + if cfg.lastSeen != nil { + data["last_seen"] = *cfg.lastSeen + } + data["size"] = cfg.size data["page"] = cfg.page diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index 2007195..bbc0bfd 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -30,6 +30,8 @@ type Node struct { Virtualized bool `json:"virtualized"` SerialNumber string `json:"serial_number"` UptimeReports []UptimeReport `json:"uptime"` + LastSeen *time.Time `json:"last_seen"` + Online bool `json:"online"` Approved bool } diff --git a/node-registrar/pkg/db/models.go b/node-registrar/pkg/db/models.go index 8149afc..87262e9 100644 --- a/node-registrar/pkg/db/models.go +++ b/node-registrar/pkg/db/models.go @@ -49,6 +49,7 @@ type Node struct { UptimeReports []UptimeReport `json:"uptime" gorm:"foreignKey:NodeID;references:NodeID;constraint:OnDelete:CASCADE"` LastSeen time.Time `json:"last_seen" gorm:"index"` // Last time the node sent Uptime report + Online bool `json:"online" gorm:"-"` // Computed field, not stored in database CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` Approved bool `json:"approved"` @@ -89,11 +90,13 @@ type Location struct { } type NodeFilter struct { - NodeID *uint64 `form:"node_id"` - FarmID *uint64 `form:"farm_id"` - TwinID *uint64 `form:"twin_id"` - Status string `form:"status"` - Healthy bool `form:"healthy"` + NodeID *uint64 `form:"node_id"` + FarmID *uint64 `form:"farm_id"` + TwinID *uint64 `form:"twin_id"` + Status string `form:"status"` + Healthy bool `form:"healthy"` + Online *bool `form:"online"` // Filter by online status (true = online, false = offline, nil = both) + LastSeen *int64 `form:"last_seen"` // Filter nodes last seen within this many minutes } type FarmFilter struct { diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index e96e632..43ed737 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -23,6 +23,28 @@ func (db *Database) ListNodes(filter NodeFilter, limit Limit) (nodes []Node, err query = query.Where("twin_id = ?", *filter.TwinID) } + // Filter by online status (node sent an uptime report in the last 30 minutes) + if filter.Online != nil { + // Calculate the cutoff time (30 minutes ago by default) + cutoffMinutes := int64(30) // Default to 30 minutes + if filter.LastSeen != nil { + cutoffMinutes = *filter.LastSeen + } + cutoffTime := time.Now().Add(-time.Duration(cutoffMinutes) * time.Minute) + + if *filter.Online { + // Online nodes: last_seen is not null and more recent than cutoff time + query = query.Where("last_seen IS NOT NULL AND last_seen > ?", cutoffTime) + } else { + // Offline nodes: last_seen is null or older than cutoff time + query = query.Where("last_seen IS NULL OR last_seen <= ?", cutoffTime) + } + } else if filter.LastSeen != nil { + // If only LastSeen is provided without Online flag, show nodes active within that period + cutoffTime := time.Now().Add(-time.Duration(*filter.LastSeen) * time.Minute) + query = query.Where("last_seen IS NOT NULL AND last_seen > ?", cutoffTime) + } + offset := (limit.Page - 1) * limit.Size query = query.Offset(int(offset)).Limit(int(limit.Size)) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index bc80ec2..4ba3b02 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -212,9 +212,11 @@ func (s Server) updateFarmHandler(c *gin.Context) { // @Param twin_id query int false "Filter by twin ID" // @Param status query string false "Filter by status" // @Param healthy query bool false "Filter by health status" +// @Param online query bool false "Filter by online status (true = online, false = offline)" +// @Param last_seen query int false "Filter nodes last seen within this many minutes" // @Param page query int false "Page number" default(1) // @Param size query int false "Results per page" default(10) -// @Success 200 {object} []db.Node "List of nodes" +// @Success 200 {object} []db.Node "List of nodes with online status" // @Failure 400 {object} map[string]any "Bad request" // @Router /nodes [get] func (s Server) listNodesHandler(c *gin.Context) { @@ -233,6 +235,12 @@ func (s Server) listNodesHandler(c *gin.Context) { return } + // Set online status for each node + cutoffTime := time.Now().Add(-30 * time.Minute) + for i := range nodes { + nodes[i].Online = !nodes[i].LastSeen.IsZero() && nodes[i].LastSeen.After(cutoffTime) + } + c.JSON(http.StatusOK, nodes) } @@ -242,7 +250,7 @@ func (s Server) listNodesHandler(c *gin.Context) { // @Accept json // @Produce json // @Param node_id path int true "Node ID" -// @Success 200 {object} db.Node "Node details" +// @Success 200 {object} db.Node "Node details with online status and last_seen information" // @Failure 400 {object} map[string]any "Invalid node ID" // @Failure 404 {object} map[string]any "Node not found" // @Router /nodes/{node_id} [get] @@ -266,6 +274,10 @@ func (s Server) getNodeHandler(c *gin.Context) { return } + // Determine if the node is online (has sent an uptime report in the last 30 minutes) + cutoffTime := time.Now().Add(-30 * time.Minute) + node.Online = !node.LastSeen.IsZero() && node.LastSeen.After(cutoffTime) + c.JSON(http.StatusOK, node) } From bcd5350dae38ec993d88d513755af7a23dcb592e Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 1 May 2025 13:41:05 +0300 Subject: [PATCH 07/10] handle uptime report correctly --- node-registrar/client/node.go | 4 +++- node-registrar/client/node_test.go | 4 ++-- node-registrar/client/types.go | 4 ++-- node-registrar/pkg/db/farms.go | 5 ++++- node-registrar/pkg/server/handlers.go | 11 ++++++----- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/node-registrar/client/node.go b/node-registrar/client/node.go index 82a575d..0763976 100644 --- a/node-registrar/client/node.go +++ b/node-registrar/client/node.go @@ -254,7 +254,9 @@ func (c *RegistrarClient) registerNode( }{} err = json.NewDecoder(resp.Body).Decode(&result) - + if err != nil { + return 0, errors.Wrap(err, "failed to decode response body") + } c.nodeID = result.NodeID return result.NodeID, err } diff --git a/node-registrar/client/node_test.go b/node-registrar/client/node_test.go index 1028054..4d539a2 100644 --- a/node-registrar/client/node_test.go +++ b/node-registrar/client/node_test.go @@ -99,8 +99,8 @@ func TestUpdateNode(t *testing.T) { request = updateNodeSendUptimeReport report := UptimeReport{ - Uptime: 40 * time.Minute, - Timestamp: time.Now(), + Uptime: 40 * 60, + Timestamp: time.Now().Unix(), } err = c.ReportUptime(report) require.NoError(err) diff --git a/node-registrar/client/types.go b/node-registrar/client/types.go index bbc0bfd..94e98e5 100644 --- a/node-registrar/client/types.go +++ b/node-registrar/client/types.go @@ -36,8 +36,8 @@ type Node struct { } type UptimeReport struct { - Uptime time.Duration `json:"uptime"` - Timestamp time.Time `json:"timestamp"` + Uptime uint64 `json:"uptime"` // in seconds + Timestamp int64 `json:"timestamp"` // in seconds since epoch } type ZosVersion struct { diff --git a/node-registrar/pkg/db/farms.go b/node-registrar/pkg/db/farms.go index 5653231..21e97e6 100644 --- a/node-registrar/pkg/db/farms.go +++ b/node-registrar/pkg/db/farms.go @@ -1,6 +1,8 @@ package db import ( + "strings" + "github.com/pkg/errors" "gorm.io/gorm" ) @@ -41,9 +43,10 @@ func (db *Database) GetFarm(farmID uint64) (farm Farm, err error) { func (db *Database) CreateFarm(farm Farm) (uint64, error) { if err := db.gormDB.Create(&farm).Error; err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { + if strings.Contains(err.Error(), "duplicate key value") { return 0, ErrRecordAlreadyExists } + return 0, err } return farm.FarmID, nil diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 4ba3b02..591e870 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -423,7 +423,7 @@ func (s *Server) updateNodeHandler(c *gin.Context) { type UptimeReportRequest struct { Uptime uint64 `json:"uptime" binding:"required"` - Timestamp uint64 `json:"timestamp" binding:"required"` + Timestamp int64 `json:"timestamp" binding:"required"` } // @Summary Report node uptime @@ -473,7 +473,8 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { // Ensuring the timestamp_hint is within an Acceptable Range err = validateTimestampHint(req.Timestamp) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid timestamp hint"}) + // include the error message + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -807,8 +808,8 @@ func ensureOwner(c *gin.Context, twinID uint64) { } // Helper function to validate timestamp hint -func validateTimestampHint(timestampHint uint64) error { - hintTime := time.Unix(int64(timestampHint), 0) +func validateTimestampHint(timestampHint int64) error { + hintTime := time.Unix(timestampHint, 0) now := time.Now() @@ -819,7 +820,7 @@ func validateTimestampHint(timestampHint uint64) error { // Check if the hint is within the acceptable range if hintTime.Before(earliestAllowed) || hintTime.After(latestAllowed) { - return errors.New("InvalidTimestampHint") + return fmt.Errorf("invalid timestamp hint: must be within ±%d seconds of the current time (%s)", UptimeReportTimestampHintDrift, now) } return nil From 4bc197e9a5f957644b3610044821af52cf15337d Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 1 May 2025 14:16:29 +0300 Subject: [PATCH 08/10] fix lint --- node-registrar/pkg/server/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 591e870..4849e93 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -482,7 +482,7 @@ func (s *Server) uptimeReportHandler(c *gin.Context) { report := &db.UptimeReport{ NodeID: id, Duration: time.Duration(req.Uptime) * time.Second, - Timestamp: time.Unix(int64(req.Timestamp), 0).UTC(), + Timestamp: time.Unix(req.Timestamp, 0).UTC(), } // Create report record and Update node LastSeen(the timestamp of the last report) From fb8a3630f331d1caf93c6d31256cd7b1956b63ec Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 1 May 2025 16:14:12 +0300 Subject: [PATCH 09/10] update migartion and default cutoff time for online filter --- node-registrar/pkg/db/db.go | 29 --------- node-registrar/pkg/db/migrate.go | 101 ++++++++++++++++++------------- node-registrar/pkg/db/nodes.go | 4 +- 3 files changed, 60 insertions(+), 74 deletions(-) diff --git a/node-registrar/pkg/db/db.go b/node-registrar/pkg/db/db.go index afbd1e9..ab1d148 100644 --- a/node-registrar/pkg/db/db.go +++ b/node-registrar/pkg/db/db.go @@ -7,7 +7,6 @@ import ( "strings" "github.com/pkg/errors" - "github.com/rs/zerolog/log" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -55,13 +54,6 @@ func NewDB(c Config) (Database, error) { return Database{}, err } - // Run the data migration for LastSeen field - count, err := db.MigrateNodeLastSeen() - if err != nil { - return Database{}, errors.Wrap(err, "failed to migrate node last seen data") - } - log.Info().Msgf("Migration: Updated LastSeen for %d nodes", count) - return db, sql.Ping() } @@ -134,24 +126,3 @@ func (db Database) Close() error { func (db *Database) Transaction(fn func(tx *gorm.DB) error) error { return db.gormDB.Transaction(fn) } - -// MigrateNodeLastSeen updates the LastSeen field for existing nodes that don't have it set -func (db Database) MigrateNodeLastSeen() (int64, error) { - query := ` - UPDATE nodes n - SET last_seen = ( - SELECT MAX(timestamp) - FROM uptime_reports ur - WHERE ur.node_id = n.node_id - ) - WHERE (last_seen IS NULL OR last_seen = '0001-01-01 00:00:00+00') - AND EXISTS ( - SELECT 1 - FROM uptime_reports ur - WHERE ur.node_id = n.node_id - ) - ` - - result := db.gormDB.Exec(query) - return result.RowsAffected, result.Error -} diff --git a/node-registrar/pkg/db/migrate.go b/node-registrar/pkg/db/migrate.go index 9e6d121..d3f7231 100644 --- a/node-registrar/pkg/db/migrate.go +++ b/node-registrar/pkg/db/migrate.go @@ -4,6 +4,8 @@ import ( "strings" "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "gorm.io/gorm" ) func (db Database) autoMigrate() error { @@ -21,12 +23,18 @@ func (db Database) autoMigrate() error { return err } + err := db.MigrateNodeLastSeen() + if err != nil { + return errors.Wrap(err, "failed to migrate node last seen data") + } + return nil } func (db Database) migrateNodes() error { // if nodes are already migrated skip migration if result := db.gormDB.First(&Node{}); result.Error == nil { + log.Info().Msg("nodes Interfaces are already migrated") return nil } @@ -36,56 +44,63 @@ func (db Database) migrateNodes() error { IPs string `json:"ips"` } + // we'd only load the data we actually need from the database type nodeType struct { - NodeID uint64 `json:"node_id" gorm:"primaryKey;autoIncrement"` - FarmID uint64 `json:"farm_id" gorm:"not null;check:farm_id> 0;foreignKey:FarmID;references:FarmID;constraint:OnDelete:RESTRICT"` - TwinID uint64 `json:"twin_id" gorm:"not null;unique;check:twin_id > 0;foreignKey:TwinID;references:TwinID;constraint:OnDelete:RESTRICT"` - - Location Location `json:"location" gorm:"not null;type:json;serializer:json"` - - Resources Resources `json:"resources" gorm:"not null;type:json;serializer:json"` - Interfaces []oldInterface `gorm:"not null;type:json;serializer:json"` - SecureBoot bool `json:"secure_boot"` - Virtualized bool `json:"virtualized"` - SerialNumber string `json:"serial_number"` - - Approved bool `json:"approved"` + NodeID uint64 `json:"node_id" gorm:"primaryKey"` + Interfaces []oldInterface `gorm:"not null;type:json;serializer:json"` } - var nodes []nodeType - result := db.gormDB.Model(&Node{}).Find(&nodes) - if result.Error != nil { - return result.Error - } + // Use a single transaction for all updates to ensure atomicity + return db.Transaction(func(tx *gorm.DB) error { + var nodes []nodeType + if err := tx.Model(&Node{}).Find(&nodes).Error; err != nil { + return err + } - for _, node := range nodes { - var interfaces []Interface - for _, i := range node.Interfaces { - ips := strings.Split(i.IPs, "/") - newInterface := Interface{ - Name: i.Name, - Mac: i.Mac, - IPs: ips, + for _, node := range nodes { + var interfaces []Interface + for _, i := range node.Interfaces { + ips := strings.Split(i.IPs, "/") + newInterface := Interface{ + Name: i.Name, + Mac: i.Mac, + IPs: ips, + } + interfaces = append(interfaces, newInterface) } - interfaces = append(interfaces, newInterface) - } - updatedNode := Node{ - NodeID: node.NodeID, - FarmID: node.FarmID, - TwinID: node.TwinID, - Location: node.Location, - Resources: node.Resources, - Interfaces: interfaces, - SecureBoot: node.SecureBoot, - Virtualized: node.Virtualized, - Approved: node.Approved, + // Update only the interfaces field + if err := tx.Model(&Node{}). + Where("node_id = ?", node.NodeID). + Update("interfaces", interfaces).Error; err != nil { + return err // This will roll back the entire transaction + } } - err := db.gormDB.Model(&Node{}).Where("node_id = ?", node.NodeID).Updates(updatedNode).Error - if err != nil { - return err - } + return nil + }) +} + +// MigrateNodeLastSeen updates the LastSeen field for existing nodes that don't have it set +func (db Database) MigrateNodeLastSeen() error { + query := ` + UPDATE nodes n + SET last_seen = ( + SELECT MAX(timestamp) + FROM uptime_reports ur + WHERE ur.node_id = n.node_id + ) + WHERE (last_seen IS NULL OR last_seen = '0001-01-01 00:00:00+00') + AND EXISTS ( + SELECT 1 + FROM uptime_reports ur + WHERE ur.node_id = n.node_id + ) + ` + + result := db.gormDB.Exec(query) + if result.Error == nil { + log.Info().Msgf("Migration: Updated LastSeen for %d nodes", result.RowsAffected) } - return nil + return result.Error } diff --git a/node-registrar/pkg/db/nodes.go b/node-registrar/pkg/db/nodes.go index 43ed737..abb6ee1 100644 --- a/node-registrar/pkg/db/nodes.go +++ b/node-registrar/pkg/db/nodes.go @@ -25,8 +25,8 @@ func (db *Database) ListNodes(filter NodeFilter, limit Limit) (nodes []Node, err // Filter by online status (node sent an uptime report in the last 30 minutes) if filter.Online != nil { - // Calculate the cutoff time (30 minutes ago by default) - cutoffMinutes := int64(30) // Default to 30 minutes + // Calculate the cutoff time (40 minutes ago by default) + cutoffMinutes := int64(40) // Default to 40 minutes if filter.LastSeen != nil { cutoffMinutes = *filter.LastSeen } From c8e8840e33f41b356f114121f50669c1dc6d8189 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 1 May 2025 16:58:53 +0300 Subject: [PATCH 10/10] Update online cutoff time --- node-registrar/pkg/server/handlers.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/node-registrar/pkg/server/handlers.go b/node-registrar/pkg/server/handlers.go index 4849e93..55032f6 100644 --- a/node-registrar/pkg/server/handlers.go +++ b/node-registrar/pkg/server/handlers.go @@ -17,6 +17,7 @@ import ( const ( MaxTimestampDelta = 2 * time.Second UptimeReportTimestampHintDrift int64 = 60 + OnlineCutoffTime = 40 * time.Minute ) // @title Node Registrar API @@ -236,7 +237,7 @@ func (s Server) listNodesHandler(c *gin.Context) { } // Set online status for each node - cutoffTime := time.Now().Add(-30 * time.Minute) + cutoffTime := time.Now().Add(-OnlineCutoffTime) for i := range nodes { nodes[i].Online = !nodes[i].LastSeen.IsZero() && nodes[i].LastSeen.After(cutoffTime) } @@ -275,7 +276,7 @@ func (s Server) getNodeHandler(c *gin.Context) { } // Determine if the node is online (has sent an uptime report in the last 30 minutes) - cutoffTime := time.Now().Add(-30 * time.Minute) + cutoffTime := time.Now().Add(-OnlineCutoffTime) node.Online = !node.LastSeen.IsZero() && node.LastSeen.After(cutoffTime) c.JSON(http.StatusOK, node)