diff --git a/internal/fleetdb/attributes.go b/internal/fleetdb/attributes.go new file mode 100644 index 0000000..410c37d --- /dev/null +++ b/internal/fleetdb/attributes.go @@ -0,0 +1,52 @@ +package internalfleetdb + +import ( + "encoding/json" + + "github.com/metal-toolbox/component-inventory/pkg/api/constants" + "github.com/metal-toolbox/component-inventory/pkg/api/types" + fleetdb "github.com/metal-toolbox/fleetdb/pkg/api/v1" +) + +func deviceVendorAttributes(cid *types.ComponentInventoryDevice) (map[string]string, *fleetdb.Attributes, error) { + deviceVendorData := map[string]string{ + constants.ServerSerialAttributeKey: "unknown", + constants.ServerVendorAttributeKey: "unknown", + constants.ServerModelAttributeKey: "unknown", + } + + if cid.Inv != nil { + if cid.Inv.Serial != "" { + deviceVendorData[constants.ServerSerialAttributeKey] = cid.Inv.Serial + } + + if cid.Inv.Model != "" { + deviceVendorData[constants.ServerModelAttributeKey] = cid.Inv.Model + } + + if cid.Inv.Vendor != "" { + deviceVendorData[constants.ServerVendorAttributeKey] = cid.Inv.Vendor + } + } + + deviceVendorDataBytes, err := json.Marshal(deviceVendorData) + if err != nil { + return nil, nil, err + } + + return deviceVendorData, &fleetdb.Attributes{ + Namespace: constants.ServerVendorAttributeNS, + Data: deviceVendorDataBytes, + }, nil +} + +// attributeByNamespace returns the attribute in the slice that matches the namespace +func attributeByNamespace(ns string, attributes []fleetdb.Attributes) *fleetdb.Attributes { + for _, attribute := range attributes { + if attribute.Namespace == ns { + return &attribute + } + } + + return nil +} diff --git a/internal/fleetdb/fleetdb.go b/internal/fleetdb/fleetdb.go new file mode 100644 index 0000000..05d42c7 --- /dev/null +++ b/internal/fleetdb/fleetdb.go @@ -0,0 +1,116 @@ +package internalfleetdb + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/uuid" + "github.com/metal-toolbox/component-inventory/internal/app" + "github.com/metal-toolbox/component-inventory/pkg/api/constants" + "github.com/metal-toolbox/component-inventory/pkg/api/types" + fleetdb "github.com/metal-toolbox/fleetdb/pkg/api/v1" + "go.uber.org/zap" +) + +type Client interface { + GetServer(context.Context, uuid.UUID) (*fleetdb.Server, *fleetdb.ServerResponse, error) + GetComponents(context.Context, uuid.UUID, *fleetdb.PaginationParams) (fleetdb.ServerComponentSlice, *fleetdb.ServerResponse, error) + UpdateAttributes(context.Context, *fleetdb.Server, *types.ComponentInventoryDevice, *zap.Logger) error + UpdateServerBIOSConfig() error +} + +// Creates a new Client, with reasonable defaults +func NewFleetDBClient(cfg *app.Configuration) (Client, error) { + client, err := fleetdb.NewClient(cfg.FleetDBAddress, nil) + if err != nil { + return nil, err + } + + if cfg.FleetDBToken != "" { + client.SetToken(cfg.FleetDBToken) + } + + return &fleetDBClient{ + client: client, + }, nil +} + +type fleetDBClient struct { + client *fleetdb.Client +} + +func (fc fleetDBClient) GetServer(ctx context.Context, id uuid.UUID) (*fleetdb.Server, *fleetdb.ServerResponse, error) { + return fc.client.Get(ctx, id) +} + +func (fc fleetDBClient) GetComponents(ctx context.Context, id uuid.UUID, params *fleetdb.PaginationParams) (fleetdb.ServerComponentSlice, *fleetdb.ServerResponse, error) { + return fc.client.GetComponents(ctx, id, params) +} + +func (fc fleetDBClient) UpdateAttributes(ctx context.Context, server *fleetdb.Server, dev *types.ComponentInventoryDevice, log *zap.Logger) error { + return createUpdateServerAttributes(ctx, fc.client, server, dev, log) +} + +// Functions below may be refactored in the near future. +func createUpdateServerAttributes(ctx context.Context, c *fleetdb.Client, server *fleetdb.Server, dev *types.ComponentInventoryDevice, log *zap.Logger) error { + newVendorData, newVendorAttrs, err := deviceVendorAttributes(dev) + if err != nil { + return err + } + + // identify current vendor data in the inventory + existingVendorAttrs := attributeByNamespace(constants.ServerVendorAttributeNS, server.Attributes) + if existingVendorAttrs == nil { + // create if none exists + _, err = c.CreateAttributes(ctx, server.UUID, *newVendorAttrs) + return err + } + + // unpack vendor data from inventory + existingVendorData := map[string]string{} + if err := json.Unmarshal(existingVendorAttrs.Data, &existingVendorData); err != nil { + // update vendor data since it seems to be invalid + log.Warn("server vendor attributes data invalid, updating..") + + _, err = c.UpdateAttributes(ctx, server.UUID, constants.ServerVendorAttributeNS, newVendorAttrs.Data) + + return err + } + + updatedVendorData := existingVendorData + var changes bool + for key := range newVendorData { + if updatedVendorData[key] == "" || updatedVendorData[key] == "unknown" { + if newVendorData[key] != "unknown" { + changes = true + updatedVendorData[key] = newVendorData[key] + } + } + } + + if !changes { + return nil + } + + if len(updatedVendorData) > 0 { + updateBytes, err := json.Marshal(updatedVendorData) + if err != nil { + return err + } + + _, err = c.UpdateAttributes(ctx, server.UUID, constants.ServerVendorAttributeNS, updateBytes) + + return err + } + + return nil +} + +func (fc fleetDBClient) UpdateServerBIOSConfig() error { + return createUpdateServerBIOSConfig() +} + +func createUpdateServerBIOSConfig() error { + return fmt.Errorf("unimplemented") +} diff --git a/pkg/api/client/client.go b/pkg/api/client/client.go index 743dead..a76d7b7 100644 --- a/pkg/api/client/client.go +++ b/pkg/api/client/client.go @@ -6,8 +6,9 @@ import ( "fmt" "net/http" + "github.com/metal-toolbox/component-inventory/pkg/api/constants" + "github.com/bmc-toolbox/common" - "github.com/metal-toolbox/component-inventory/pkg/api/routes" rivets "github.com/metal-toolbox/rivets/types" ) @@ -51,7 +52,7 @@ func NewClient(serverAddress string, opts ...Option) (Client, error) { } func (c componentInventoryClient) GetServerComponents(ctx context.Context, serverID string) (ServerComponents, error) { - path := fmt.Sprintf("%v/%v", routes.ComponentsEndpoint, serverID) + path := fmt.Sprintf("%v/%v", constants.ComponentsEndpoint, serverID) resp, err := c.get(ctx, path) if err != nil { return nil, err @@ -66,7 +67,7 @@ func (c componentInventoryClient) GetServerComponents(ctx context.Context, serve } func (c componentInventoryClient) Version(ctx context.Context) (string, error) { - resp, err := c.get(ctx, routes.VersionEndpoint) + resp, err := c.get(ctx, constants.VersionEndpoint) if err != nil { return "", err } @@ -75,7 +76,7 @@ func (c componentInventoryClient) Version(ctx context.Context) (string, error) { } func (c componentInventoryClient) UpdateInbandInventory(ctx context.Context, serverID string, device *common.Device) (string, error) { - path := fmt.Sprintf("%v/%v", routes.InbandInventoryEndpoint, serverID) + path := fmt.Sprintf("%v/%v", constants.InbandInventoryEndpoint, serverID) body, err := json.Marshal(device) if err != nil { return "", fmt.Errorf("failed to parse device: %v", err) @@ -90,7 +91,7 @@ func (c componentInventoryClient) UpdateInbandInventory(ctx context.Context, ser } func (c componentInventoryClient) UpdateOutOfbandInventory(ctx context.Context, serverID string, device *common.Device) (string, error) { - path := fmt.Sprintf("%v/%v", routes.OutofbandInventoryEndpoint, serverID) + path := fmt.Sprintf("%v/%v", constants.OutofbandInventoryEndpoint, serverID) body, err := json.Marshal(device) if err != nil { return "", fmt.Errorf("failed to parse device: %v", err) diff --git a/pkg/api/constants/constants.go b/pkg/api/constants/constants.go new file mode 100644 index 0000000..71ef474 --- /dev/null +++ b/pkg/api/constants/constants.go @@ -0,0 +1,36 @@ +package constants + +const ( + LivenessEndpoint = "/_health/liveness" + VersionEndpoint = "/api/version" + ComponentsEndpoint = "/components" + InbandInventoryEndpoint = "/inventory/in-band" + OutofbandInventoryEndpoint = "/inventory/out-of-band" + + // server service attribute to look up the BMC IP Address in + BmcAttributeNamespace = "sh.hollow.bmc_info" + + // server server service BMC address attribute key found under the bmcAttributeNamespace + BmcIPAddressAttributeKey = "address" + + // serverservice namespace prefix the data is stored in. + ServerServiceNSPrefix = "sh.hollow.alloy" + + // server vendor, model attributes are stored in this namespace. + ServerVendorAttributeNS = ServerServiceNSPrefix + ".server_vendor_attributes" + + // additional server metadata are stored in this namespace. + ServerMetadataAttributeNS = ServerServiceNSPrefix + ".server_metadata_attributes" + + // errors that occurred when connecting/collecting inventory from the bmc are stored here. + ServerBMCErrorsAttributeNS = ServerServiceNSPrefix + ".server_bmc_errors" + + // server service server serial attribute key + ServerSerialAttributeKey = "serial" + + // server service server model attribute key + ServerModelAttributeKey = "model" + + // server service server vendor attribute key + ServerVendorAttributeKey = "vendor" +) diff --git a/pkg/api/routes/components.go b/pkg/api/routes/components.go index c6b66db..f7068fc 100644 --- a/pkg/api/routes/components.go +++ b/pkg/api/routes/components.go @@ -5,6 +5,7 @@ import ( "time" "github.com/google/uuid" + internalfleetdb "github.com/metal-toolbox/component-inventory/internal/fleetdb" fleetdb "github.com/metal-toolbox/fleetdb/pkg/api/v1" rdb "github.com/metal-toolbox/rivets/fleetdb" rivets "github.com/metal-toolbox/rivets/types" @@ -16,7 +17,7 @@ var fleetDBTimeout = 3 * time.Minute // this is a map of "component_type_name" to the actual inventory data for each component type serverComponents map[string][]*rivets.Component -func fetchServerComponents(client *fleetdb.Client, srvid uuid.UUID, l *zap.Logger) (serverComponents, error) { +func fetchServerComponents(client internalfleetdb.Client, srvid uuid.UUID, l *zap.Logger) (serverComponents, error) { ctx, cancel := context.WithTimeout(context.Background(), fleetDBTimeout) defer cancel() diff --git a/pkg/api/routes/components_test.go b/pkg/api/routes/components_test.go index 96e42a8..c250943 100644 --- a/pkg/api/routes/components_test.go +++ b/pkg/api/routes/components_test.go @@ -2,6 +2,7 @@ package routes import ( "github.com/metal-toolbox/component-inventory/internal/app" + internalfleetdb "github.com/metal-toolbox/component-inventory/internal/fleetdb" "encoding/json" "fmt" @@ -90,9 +91,10 @@ func TestFetchServerComponents(t *testing.T) { logger := app.GetLogger(true) - client := getFleetDBClient(&app.Configuration{ + client, err := internalfleetdb.NewFleetDBClient(&app.Configuration{ FleetDBAddress: ts.URL, }) + require.NoError(t, err) result, err := fetchServerComponents(client, serverUUID, logger) require.NoError(t, err) @@ -118,11 +120,12 @@ func TestFetchServerComponents(t *testing.T) { logger := app.GetLogger(true) - client := getFleetDBClient(&app.Configuration{ + client, err := internalfleetdb.NewFleetDBClient(&app.Configuration{ FleetDBAddress: ts.URL, }) + require.NoError(t, err) - _, err := fetchServerComponents(client, serverUUID, logger) + _, err = fetchServerComponents(client, serverUUID, logger) require.Error(t, err) var srvErr fleetdb.ServerError require.ErrorAs(t, err, &srvErr, "unexpected error type") diff --git a/pkg/api/routes/endpoints.go b/pkg/api/routes/endpoints.go deleted file mode 100644 index 3b94ff0..0000000 --- a/pkg/api/routes/endpoints.go +++ /dev/null @@ -1,9 +0,0 @@ -package routes - -const ( - LivenessEndpoint = "/_health/liveness" - VersionEndpoint = "/api/version" - ComponentsEndpoint = "/components" - InbandInventoryEndpoint = "/inventory/in-band" - OutofbandInventoryEndpoint = "/inventory/out-of-band" -) diff --git a/pkg/api/routes/inventory.go b/pkg/api/routes/inventory.go index 8445ab9..91023c7 100644 --- a/pkg/api/routes/inventory.go +++ b/pkg/api/routes/inventory.go @@ -1,20 +1,24 @@ package routes import ( + "context" "errors" - "github.com/bmc-toolbox/common" - "github.com/google/uuid" + internalfleetdb "github.com/metal-toolbox/component-inventory/internal/fleetdb" + "github.com/metal-toolbox/component-inventory/pkg/api/types" fleetdb "github.com/metal-toolbox/fleetdb/pkg/api/v1" "go.uber.org/zap" ) -func processInband(c *fleetdb.Client, srvID uuid.UUID, dev *common.Device, log *zap.Logger) error { //nolint - log.Info("processing", zap.String("server id", srvID.String()), zap.String("device", dev.Serial)) +func processInband(ctx context.Context, c internalfleetdb.Client, server *fleetdb.Server, dev *types.ComponentInventoryDevice, log *zap.Logger) error { //nolint + log.Info("processing", zap.String("server", server.Name), zap.String("device", dev.Inv.Serial)) + if err := c.UpdateAttributes(ctx, server, dev, log); err != nil { + return err + } return errors.New("not implemented") } -func processOutofband(c *fleetdb.Client, srvID uuid.UUID, dev *common.Device, log *zap.Logger) error { //nolint - log.Info("processing", zap.String("server id", srvID.String()), zap.String("device", dev.Serial)) +func processOutofband(ctx context.Context, c internalfleetdb.Client, server *fleetdb.Server, dev *types.ComponentInventoryDevice, log *zap.Logger) error { //nolint + log.Info("processing", zap.String("server", server.Name), zap.String("device", dev.Inv.Serial)) return errors.New("not implemented") } diff --git a/pkg/api/routes/routes.go b/pkg/api/routes/routes.go index d50260e..428f019 100644 --- a/pkg/api/routes/routes.go +++ b/pkg/api/routes/routes.go @@ -1,16 +1,19 @@ package routes import ( + "context" "fmt" "net/http" "time" - "github.com/bmc-toolbox/common" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/metal-toolbox/component-inventory/internal/app" + internalfleetdb "github.com/metal-toolbox/component-inventory/internal/fleetdb" "github.com/metal-toolbox/component-inventory/internal/metrics" "github.com/metal-toolbox/component-inventory/internal/version" + "github.com/metal-toolbox/component-inventory/pkg/api/constants" + "github.com/metal-toolbox/component-inventory/pkg/api/types" fleetdb "github.com/metal-toolbox/fleetdb/pkg/api/v1" "go.hollow.sh/toolbox/ginauth" "go.hollow.sh/toolbox/ginjwt" @@ -86,7 +89,7 @@ func ComposeHTTPServer(theApp *app.App) *http.Server { } // set up common middleware for logging and metrics - g.Use(composeAppLogging(theApp.Log, LivenessEndpoint), gin.Recovery()) + g.Use(composeAppLogging(theApp.Log, constants.LivenessEndpoint), gin.Recovery()) // some boilerplate setup g.NoRoute(func(c *gin.Context) { @@ -98,11 +101,11 @@ func ComposeHTTPServer(theApp *app.App) *http.Server { }) // a liveness endpoint - g.GET(LivenessEndpoint, func(c *gin.Context) { + g.GET(constants.LivenessEndpoint, func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"time": time.Now()}) }) - g.GET(VersionEndpoint, func(c *gin.Context) { + g.GET(constants.VersionEndpoint, func(c *gin.Context) { c.JSON(http.StatusOK, version.Current()) }) @@ -117,7 +120,7 @@ func ComposeHTTPServer(theApp *app.App) *http.Server { // add other API endpoints to the gin Engine as required // get the components associated with a server - g.GET(ComponentsEndpoint+"/:server", + g.GET(constants.ComponentsEndpoint+"/:server", composeAuthHandler(readScopes("server:component")), func(ctx *gin.Context) { serverID, err := uuid.Parse(ctx.Param("server")) @@ -129,7 +132,15 @@ func ComposeHTTPServer(theApp *app.App) *http.Server { return } - client := getFleetDBClient(theApp.Cfg) + client, err := internalfleetdb.NewFleetDBClient(theApp.Cfg) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]any{ + "message": "failed to connect to fleetdb", + "error": err.Error(), + }) + return + } + comps, err := fetchServerComponents(client, serverID, theApp.Log) if err != nil { ctx.JSON(http.StatusInternalServerError, map[string]any{ @@ -142,12 +153,12 @@ func ComposeHTTPServer(theApp *app.App) *http.Server { }) // add an API to ingest inventory data - g.POST(InbandInventoryEndpoint+"/:server", + g.POST(constants.InbandInventoryEndpoint+"/:server", composeAuthHandler(updateScopes("server:component")), composeInventoryHandler(theApp, processInband), ) - g.POST(OutofbandInventoryEndpoint+"/:server", + g.POST(constants.OutofbandInventoryEndpoint+"/:server", composeAuthHandler(updateScopes("server:component")), composeInventoryHandler(theApp, processOutofband), ) @@ -190,7 +201,7 @@ func wrapAPICall(fn apiHandler) gin.HandlerFunc { } } -type inventoryHandler func(*fleetdb.Client, uuid.UUID, *common.Device, *zap.Logger) error +type inventoryHandler func(context.Context, internalfleetdb.Client, *fleetdb.Server, *types.ComponentInventoryDevice, *zap.Logger) error func reject(ctx *gin.Context, code int, msg, err string) { ctx.JSON(code, map[string]any{ @@ -207,15 +218,30 @@ func composeInventoryHandler(theApp *app.App, fn inventoryHandler) gin.HandlerFu return } - var dev common.Device - if err := ctx.BindJSON(&dev); err != nil { + var dev types.ComponentInventoryDevice + if err = ctx.BindJSON(&dev); err != nil { reject(ctx, http.StatusBadRequest, "invalid server inventory", err.Error()) return } + fleetDBClient, err := internalfleetdb.NewFleetDBClient(theApp.Cfg) + if err != nil { + ctx.JSON(http.StatusInternalServerError, map[string]any{ + "message": "failed to connect to fleetdb", + "error": err.Error(), + }) + return + } + + server, _, err := fleetDBClient.GetServer(ctx, serverID) + if err != nil { + reject(ctx, http.StatusBadRequest, "server not exisit", err.Error()) + } + if err := fn( - getFleetDBClient(theApp.Cfg), - serverID, + ctx, + fleetDBClient, + server, &dev, theApp.Log, ); err != nil { @@ -227,14 +253,6 @@ func composeInventoryHandler(theApp *app.App, fn inventoryHandler) gin.HandlerFu } } -func getFleetDBClient(cfg *app.Configuration) *fleetdb.Client { - client, _ := fleetdb.NewClient(cfg.FleetDBAddress, nil) - if cfg.FleetDBToken != "" { - client.SetToken(cfg.FleetDBToken) - } - return client -} - func composeAuthHandler(scopes []string) gin.HandlerFunc { if authMiddleWare == nil { return ginNoOp diff --git a/pkg/api/types/types.go b/pkg/api/types/types.go new file mode 100644 index 0000000..c1fa55b --- /dev/null +++ b/pkg/api/types/types.go @@ -0,0 +1,13 @@ +package types + +import ( + "github.com/bmc-toolbox/common" +) + +type BiosConfig map[string]string + +type ComponentInventoryDevice struct { + ID string `json:"id,omitempty"` + Inv *common.Device `json:"inventory,omitempty"` + BiosCfg *BiosConfig `json:"biosconfig,omitempty"` +}