diff --git a/cmd/cabinet/add_cabinet.go b/cmd/cabinet/add_cabinet.go index 1c6683ef..3404ef2d 100644 --- a/cmd/cabinet/add_cabinet.go +++ b/cmd/cabinet/add_cabinet.go @@ -67,7 +67,7 @@ func addCabinet(cmd *cobra.Command, args []string) error { log.Info().Msgf("Querying inventory to suggest cabinet number and VLAN ID") // set the vars to the recommendations cabinetNumber = recommendations.LocationOrdinal - vlanId = recommendations.ProviderMetadata[csm.ProviderPropertyVlanId].(int) + vlanId = recommendations.ProviderMetadata[csm.ProviderMetadataVlanId].(int) log.Debug().Msgf("Provider recommendations: %+v", recommendations) log.Info().Msgf("Suggested cabinet number: %d", cabinetNumber) log.Info().Msgf("Suggested VLAN ID: %d", vlanId) @@ -97,7 +97,7 @@ func addCabinet(cmd *cobra.Command, args []string) error { // Right now the build metadata function in the CSM provider will // unset options if nil is passed in. cabinetMetadata := map[string]interface{}{ - csm.ProviderPropertyVlanId: vlanId, + csm.ProviderMetadataVlanId: vlanId, } // Add the cabinet to the inventory using domain methods diff --git a/cmd/cabinet/list_cabinet.go b/cmd/cabinet/list_cabinet.go index b16a4877..648a76a7 100644 --- a/cmd/cabinet/list_cabinet.go +++ b/cmd/cabinet/list_cabinet.go @@ -38,8 +38,8 @@ import ( "github.com/Cray-HPE/cani/internal/inventory" "github.com/Cray-HPE/cani/internal/provider/csm" "github.com/Cray-HPE/cani/pkg/hardwaretypes" + "github.com/Cray-HPE/cani/pkg/pointers" "github.com/google/uuid" - "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" ) @@ -126,17 +126,26 @@ func listCabinet(cmd *cobra.Command, args []string) error { return filtered[keys[i]].LocationPath.String() < filtered[keys[j]].LocationPath.String() }) - properties := csm.CabinetMetadata{} for _, hw := range keys { - if _, exists := filtered[hw].ProviderProperties[string(inventory.CSMProvider)]; exists { - if err := mapstructure.Decode(filtered[hw].ProviderProperties["csm"], &properties); err != nil { + // Start with an empty cabinet metadata struct, just in case if this cabinet doesn't have any + // metadata set + cabinetMetadata := csm.CabinetMetadata{} + + if _, exists := filtered[hw].ProviderMetadata[inventory.CSMProvider]; exists { + csmMetadata, err := csm.DecodeProviderMetadata(filtered[hw]) + if err != nil { return err } + + if csmMetadata.Cabinet != nil { + cabinetMetadata = *csmMetadata.Cabinet + } } + fmt.Fprintf(w, "%s\t%s\t%v\t%s\n", filtered[hw].ID.String(), filtered[hw].DeviceTypeSlug, - *properties.HMNVlan, + pointers.IntPtrToStr(cabinetMetadata.HMNVlan), filtered[hw].LocationPath.String()) } diff --git a/cmd/node/list_node.go b/cmd/node/list_node.go index fe108f24..1557f67d 100644 --- a/cmd/node/list_node.go +++ b/cmd/node/list_node.go @@ -39,7 +39,6 @@ import ( "github.com/Cray-HPE/cani/internal/provider/csm" "github.com/Cray-HPE/cani/pkg/hardwaretypes" "github.com/google/uuid" - "github.com/mitchellh/mapstructure" "github.com/spf13/cobra" ) @@ -126,15 +125,25 @@ func listNode(cmd *cobra.Command, args []string) error { return filtered[keys[i]].LocationPath.String() < filtered[keys[j]].LocationPath.String() }) - properties := csm.NodeMetadata{} for _, hw := range keys { - if _, exists := filtered[hw].ProviderProperties[string(inventory.CSMProvider)]; exists { - if err := mapstructure.Decode(filtered[hw].ProviderProperties["csm"], &properties); err != nil { + // Start with an empty Node metadata struct, just in case if this node doesn't have any + // metadata set + var nodeMetadata csm.NodeMetadata + + // If metadata exists decode it + if _, exists := filtered[hw].ProviderMetadata[inventory.CSMProvider]; exists { + csmMetadata, err := csm.DecodeProviderMetadata(filtered[hw]) + if err != nil { return err } + + if csmMetadata.Node != nil { + nodeMetadata = *csmMetadata.Node + } } + // convert properties to strings and set nil values for easy printing - pp := properties.Pretty() + pp := nodeMetadata.Pretty() fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", filtered[hw].ID.String(), diff --git a/cmd/node/update_node.go b/cmd/node/update_node.go index f16bbf0a..490334d5 100644 --- a/cmd/node/update_node.go +++ b/cmd/node/update_node.go @@ -32,6 +32,7 @@ import ( root "github.com/Cray-HPE/cani/cmd" "github.com/Cray-HPE/cani/internal/domain" "github.com/Cray-HPE/cani/internal/provider" + "github.com/Cray-HPE/cani/internal/provider/csm" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -60,16 +61,16 @@ func updateNode(cmd *cobra.Command, args []string) error { // unset options if nil is passed in. nodeMeta := map[string]interface{}{} if cmd.Flags().Changed("role") { - nodeMeta["role"] = role + nodeMeta[csm.ProviderMetadataRole] = role } if cmd.Flags().Changed("subrole") { - nodeMeta["subrole"] = subrole + nodeMeta[csm.ProviderMetadataSubRole] = subrole } if cmd.Flags().Changed("alias") { - nodeMeta["alias"] = alias + nodeMeta[csm.ProviderMetadataAlias] = alias } if cmd.Flags().Changed("alias") { - nodeMeta["nid"] = nid + nodeMeta[csm.ProviderMetadataNID] = nid } // Remove the node from the inventory using domain methods diff --git a/internal/domain/node.go b/internal/domain/node.go index ef53889e..87a18613 100644 --- a/internal/domain/node.go +++ b/internal/domain/node.go @@ -58,7 +58,7 @@ func (d *Domain) UpdateNode(ctx context.Context, cabinet, chassis, slot, bmc, no return AddHardwareResult{}, err } - log.Debug().Any("metadata", hw.ProviderProperties).Msg("Provider Properties") + log.Debug().Any("metadata", hw.ProviderMetadata).Msg("Provider Properties") // Push it back into the data store if err := d.datastore.Update(&hw); err != nil { diff --git a/internal/inventory/model.go b/internal/inventory/model.go index 55d6b79f..b8daeadb 100644 --- a/internal/inventory/model.go +++ b/internal/inventory/model.go @@ -88,19 +88,16 @@ func (i *Inventory) FilterHardwareByTypeStatus(status HardwareStatus, types ...h // Hardware is the smallest unit of inventory // It has all the potential fields that hardware can have type Hardware struct { - ID uuid.UUID - Name string `json:"Name,omitempty" yaml:"Name,omitempty" default:"" usage:"Friendly name"` - Type hardwaretypes.HardwareType `json:"Type,omitempty" yaml:"Type,omitempty" default:"" usage:"Type"` - DeviceTypeSlug string `json:"DeviceTypeSlug,omitempty" yaml:"DeviceTypeSlug,omitempty" default:"" usage:"Hardware Type Library Device slug"` - Vendor string `json:"Vendor,omitempty" yaml:"Vendor,omitempty" default:"" usage:"Vendor"` - Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty" default:"" usage:"Architecture"` - Model string `json:"Model,omitempty" yaml:"Model,omitempty" default:"" usage:"Model"` - Status HardwareStatus `json:"Status,omitempty" yaml:"Status,omitempty" default:"Staged" usage:"Hardware can be [staged, provisioned, decomissioned]"` - Properties map[string]interface{} `json:"Properties,omitempty" yaml:"Properties,omitempty" default:"" usage:"Properties"` - Role string `json:"Role,omitempty" yaml:"Role,omitempty" default:"" usage:"Role"` - SubRole string `json:"SubRole,omitempty" yaml:"SubRole,omitempty" default:"" usage:"SubRole"` - Alias string `json:"Alias,omitempty" yaml:"Alias,omitempty" default:"" usage:"Alias"` - ProviderProperties map[string]interface{} `json:"ProviderProperties,omitempty" yaml:"ProviderProperties,omitempty" default:"" usage:"ProviderProperties"` + ID uuid.UUID + Name string `json:"Name,omitempty" yaml:"Name,omitempty" default:"" usage:"Friendly name"` + Type hardwaretypes.HardwareType `json:"Type,omitempty" yaml:"Type,omitempty" default:"" usage:"Type"` + DeviceTypeSlug string `json:"DeviceTypeSlug,omitempty" yaml:"DeviceTypeSlug,omitempty" default:"" usage:"Hardware Type Library Device slug"` + Vendor string `json:"Vendor,omitempty" yaml:"Vendor,omitempty" default:"" usage:"Vendor"` + Architecture string `json:"Architecture,omitempty" yaml:"Architecture,omitempty" default:"" usage:"Architecture"` + Model string `json:"Model,omitempty" yaml:"Model,omitempty" default:"" usage:"Model"` + Status HardwareStatus `json:"Status,omitempty" yaml:"Status,omitempty" default:"Staged" usage:"Hardware can be [staged, provisioned, decomissioned]"` + Properties map[string]interface{} `json:"Properties,omitempty" yaml:"Properties,omitempty" default:"" usage:"Properties"` + ProviderMetadata map[Provider]ProviderMetadataRaw `json:"ProviderMetadata,omitempty" yaml:"ProviderMetadata,omitempty" default:"" usage:"ProviderMetadata"` Parent uuid.UUID `json:"Parent,omitempty" yaml:"Parent,omitempty" default:"00000000-0000-0000-0000-000000000000" usage:"Parent hardware"` // The following are derived from Parent @@ -110,6 +107,16 @@ type Hardware struct { LocationOrdinal *int } +func (hardware *Hardware) SetProviderMetadata(provider Provider, metadata map[string]interface{}) { + // Initialize ProviderMetadata map if nil + if hardware.ProviderMetadata == nil { + hardware.ProviderMetadata = map[Provider]ProviderMetadataRaw{} + } + + // Set provider metadata + hardware.ProviderMetadata[provider] = metadata +} + func NewHardwareFromBuildOut(hardwareBuildOut hardwaretypes.HardwareBuildOut, status HardwareStatus) Hardware { locationOrdinal := hardwareBuildOut.OrdinalPath[len(hardwareBuildOut.OrdinalPath)-1] @@ -149,6 +156,9 @@ const ( CSMProvider = Provider("csm") ) +// ProviderMetadataRaw stores the metadata from a provider in a generic map. +type ProviderMetadataRaw map[string]interface{} + type LocationToken struct { HardwareType hardwaretypes.HardwareType Ordinal int diff --git a/internal/provider/csm/csv.go b/internal/provider/csm/csv.go index 47f72ae1..3b587154 100644 --- a/internal/provider/csm/csv.go +++ b/internal/provider/csm/csv.go @@ -38,10 +38,18 @@ import ( func (csm *CSM) GetFields(hw *inventory.Hardware, fieldNames []string) (values []string, err error) { values = make([]string, len(fieldNames)) - rawCsmProps := hw.ProviderProperties["csm"] - csmProps, ok := rawCsmProps.(map[string]interface{}) - if !ok { - csmProps = make(map[string]interface{}) + nodeProps := make(map[string]interface{}) + cabinetProps := make(map[string]interface{}) + if csmProps, ok := hw.ProviderMetadata[inventory.CSMProvider]; ok { + nodePropsRaw, ok := csmProps["Node"] + if ok { + nodeProps = nodePropsRaw.(map[string]interface{}) + } + + cabinetPropsRaw, ok := csmProps["Cabinet"] + if ok { + cabinetProps = cabinetPropsRaw.(map[string]interface{}) + } } for i, name := range fieldNames { @@ -59,15 +67,15 @@ func (csm *CSM) GetFields(hw *inventory.Hardware, fieldNames []string) (values [ case "Status": values[i] = fmt.Sprintf("%v", hw.Status) case "Vlan": - values[i] = toString(csmProps["HMNVlan"]) + values[i] = toString(cabinetProps["HMNVlan"]) case "Role": - values[i] = toString(csmProps["Role"]) + values[i] = toString(nodeProps["Role"]) case "SubRole": - values[i] = toString(csmProps["SubRole"]) + values[i] = toString(nodeProps["SubRole"]) case "Alias": - values[i] = getStringFromArray(csmProps["Alias"], 0) + values[i] = getStringFromArray(nodeProps["Alias"], 0) case "Nid": - values[i] = toString(csmProps["Nid"]) + values[i] = toString(nodeProps["Nid"]) default: // This case should never be hit. // The call to normalize should return an error for unknown headers @@ -80,36 +88,36 @@ func (csm *CSM) GetFields(hw *inventory.Hardware, fieldNames []string) (values [ } func (csm *CSM) SetFields(hw *inventory.Hardware, values map[string]string) (result provider.SetFieldsResult, err error) { - csmHardware, err := ToCsmHardware(hw) + csmMetadata, err := DecodeProviderMetadata(*hw) if err != nil { return result, err } - if csmHardware.CabinetMetadata == nil && csmHardware.NodeMetadata == nil { + if csmMetadata.Cabinet == nil && csmMetadata.Node == nil { log.Debug().Msgf("Skipping %v of the type %v. It does not have writable properties", hw.ID, hw.Type) return } - if csmHardware.NodeMetadata != nil { + if csmMetadata.Node != nil { for key, value := range values { switch key { case "Role": - modified := setRole(value, csmHardware.NodeMetadata) + modified := setRole(value, csmMetadata.Node) if modified { result.ModifiedFields = append(result.ModifiedFields, "Role") } case "SubRole": - modified := setSubRole(value, csmHardware.NodeMetadata) + modified := setSubRole(value, csmMetadata.Node) if modified { result.ModifiedFields = append(result.ModifiedFields, "SubRole") } case "Alias": - modified := setAlias(value, csmHardware.NodeMetadata) + modified := setAlias(value, csmMetadata.Node) if modified { result.ModifiedFields = append(result.ModifiedFields, "Alias") } case "Nid": - modified, err := setNid(value, csmHardware.NodeMetadata) + modified, err := setNid(value, csmMetadata.Node) if err != nil { return result, err } @@ -118,11 +126,18 @@ func (csm *CSM) SetFields(hw *inventory.Hardware, values map[string]string) (res } } } - } else if csmHardware.CabinetMetadata != nil { + if len(result.ModifiedFields) > 0 { + metadataRaw, err := EncodeProviderMetadata(Metadata{Node: csmMetadata.Node}) + if err != nil { + return result, err + } + hw.SetProviderMetadata(inventory.CSMProvider, metadataRaw) + } + } else if csmMetadata.Cabinet != nil { for key, value := range values { switch key { case "Vlan": - modified, err := setVlan(value, csmHardware.CabinetMetadata) + modified, err := setVlan(value, csmMetadata.Cabinet) if err != nil { return result, err } @@ -131,6 +146,13 @@ func (csm *CSM) SetFields(hw *inventory.Hardware, values map[string]string) (res } } } + if len(result.ModifiedFields) > 0 { + metadataRaw, err := EncodeProviderMetadata(Metadata{Cabinet: csmMetadata.Cabinet}) + if err != nil { + return result, err + } + hw.SetProviderMetadata(inventory.CSMProvider, metadataRaw) + } } return diff --git a/internal/provider/csm/import.go b/internal/provider/csm/import.go index 2dcd8706..83f1f274 100644 --- a/internal/provider/csm/import.go +++ b/internal/provider/csm/import.go @@ -41,6 +41,7 @@ import ( "github.com/Cray-HPE/cani/internal/provider/csm/sls" "github.com/Cray-HPE/cani/pkg/hardwaretypes" hsm_client "github.com/Cray-HPE/cani/pkg/hsm-client" + "github.com/Cray-HPE/cani/pkg/pointers" sls_client "github.com/Cray-HPE/cani/pkg/sls-client" "github.com/Cray-HPE/hms-xname/xnames" "github.com/Cray-HPE/hms-xname/xnametypes" @@ -276,7 +277,7 @@ func (csm *CSM) Import(ctx context.Context, datastore inventory.Datastore) error // Set cabinet metadata cabinetMetadata := CabinetMetadata{} if vlan, exists := cabinetHMNVlans[slsCabinet.Xname]; exists { - cabinetMetadata.HMNVlan = IntPtr(vlan) + cabinetMetadata.HMNVlan = pointers.IntPtr(vlan) } cCabinet, err = tempDatastore.GetAtLocation(locationPath) @@ -284,10 +285,14 @@ func (csm *CSM) Import(ctx context.Context, datastore inventory.Datastore) error return errors.Join(fmt.Errorf("failed to query datastore for %s", locationPath), err) } - cCabinet.ProviderProperties = map[string]interface{}{ - "csm": cabinetMetadata, + // Encode cabinet metadata + metadataRaw, err := EncodeProviderMetadata(Metadata{Cabinet: &cabinetMetadata}) + if err != nil { + return fmt.Errorf("failed to encode provider metadata for hardware (%s)", cCabinet.ID) } + cCabinet.SetProviderMetadata(inventory.CSMProvider, metadataRaw) + // Push cabinet in datastore. if err := tempDatastore.Update(&cCabinet); err != nil { return fmt.Errorf("failed to update hardware (%s) in memory datastore", cCabinet.ID) } @@ -590,15 +595,15 @@ func (csm *CSM) Import(ctx context.Context, datastore inventory.Datastore) error nodeMetadata := NodeMetadata{} if slsNodeEP.Role != "" { - nodeMetadata.Role = StringPtr(slsNodeEP.Role) + nodeMetadata.Role = pointers.StringPtr(slsNodeEP.Role) } if slsNodeEP.SubRole != "" { - nodeMetadata.Role = StringPtr(slsNodeEP.SubRole) + nodeMetadata.Role = pointers.StringPtr(slsNodeEP.SubRole) } if slsNodeEP.NID != 0 { - nodeMetadata.Nid = IntPtr(int(slsNodeEP.NID)) + nodeMetadata.Nid = pointers.IntPtr(int(slsNodeEP.NID)) } if len(slsNodeEP.Aliases) != 0 { @@ -644,11 +649,12 @@ func (csm *CSM) Import(ctx context.Context, datastore inventory.Datastore) error return errors.Join(fmt.Errorf("failed to query datastore for %s", nodeLocationPath), err) } - // Initialize the properties map if not done already - if cNode.ProviderProperties == nil { - cNode.ProviderProperties = map[string]interface{}{} + // Set the node metadata + metadataRaw, err := EncodeProviderMetadata(Metadata{Node: &nodeMetadata}) + if err != nil { + return fmt.Errorf("failed to encode provider metadata for hardware (%s)", cNode.ID) } - cNode.ProviderProperties["csm"] = nodeMetadata + cNode.SetProviderMetadata(inventory.CSMProvider, metadataRaw) // Push updates into the datastore if err := tempDatastore.Update(&cNode); err != nil { diff --git a/internal/provider/csm/metadata.go b/internal/provider/csm/metadata.go index 82340dbe..c6e8874f 100644 --- a/internal/provider/csm/metadata.go +++ b/internal/provider/csm/metadata.go @@ -26,26 +26,40 @@ package csm import ( + "errors" "fmt" "sort" "github.com/Cray-HPE/cani/internal/inventory" "github.com/Cray-HPE/cani/internal/provider" "github.com/Cray-HPE/cani/pkg/hardwaretypes" + "github.com/Cray-HPE/cani/pkg/pointers" "github.com/mitchellh/mapstructure" "github.com/rs/zerolog/log" ) const ( - ProviderPropertyVlanId = "vlanID" + ProviderMetadataVlanId = "VlanID" + ProviderMetadataRole = "Role" + ProviderMetadataSubRole = "SubRole" + ProviderMetadataAlias = "Alias" + ProviderMetadataNID = "NID" ) +// NOTE: When adding new Metadata structure make sure to add them to the MetadataStructTagSuite test suite +// in metadata_test.go + +type Metadata struct { + Cabinet *CabinetMetadata `json:"Cabinet,omitempty" mapstructure:"Cabinet,omitempty"` + Node *NodeMetadata `json:"Node,omitempty" mapstructure:"Node,omitempty"` +} + type NodeMetadata struct { - Role *string - SubRole *string - Nid *int - Alias []string - AdditionalProperties map[string]interface{} + Role *string `json:"Role" mapstructure:"Role"` + SubRole *string `json:"SubRole" mapstructure:"SubRole"` + Nid *int `json:"Nid" mapstructure:"Nid"` + Alias []string `json:"Alias" mapstructure:"Alias"` + AdditionalProperties map[string]interface{} `json:"AdditionalProperties" mapstructure:"AdditionalProperties"` } type NodeMetadataStrings struct { @@ -57,212 +71,138 @@ type NodeMetadataStrings struct { } type CabinetMetadata struct { - HMNVlan *int -} - -type CsmHardware struct { - Hardware *inventory.Hardware - CabinetMetadata *CabinetMetadata - NodeMetadata *NodeMetadata -} - -// TODO this might need a better home -func StringPtr(s string) *string { - return &s -} -func IntPtr(i int) *int { - return &i + HMNVlan *int `json:"HMNVlan" mapstructure:"HMNVlan"` } -// Warning: This modifies the provider properties of the passed in hardware object -func ToCsmHardware(hardware *inventory.Hardware) (csmHardware CsmHardware, err error) { - csmHardware = CsmHardware{ - Hardware: hardware, - } - - providerPropertiesRaw, hasCsmProperties := hardware.ProviderProperties["csm"] - - // check if the properties exist, and if they have already been parsed into the CSM struct - if hasCsmProperties { - switch providerPropertiesRaw.(type) { - case NodeMetadata: - csmHardware.NodeMetadata = providerPropertiesRaw.(*NodeMetadata) - return - case CabinetMetadata: - csmHardware.CabinetMetadata = providerPropertiesRaw.(*CabinetMetadata) - return - } - } - - // create new CSM struct - var properties interface{} - switch hardware.Type { - case hardwaretypes.Node: - csmHardware.NodeMetadata = &NodeMetadata{} - properties = csmHardware.NodeMetadata - case hardwaretypes.Cabinet: - csmHardware.CabinetMetadata = &CabinetMetadata{} - properties = csmHardware.CabinetMetadata - } - - // parse existing properties into the CSM struct - // and set the struct as the properties on the hardware object - if properties != nil { - if hasCsmProperties { - decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - DecodeHook: mapstructure.StringToIPHookFunc(), - Result: &properties, - }) - if err != nil { - return csmHardware, err - } - err = decoder.Decode(providerPropertiesRaw) - if err != nil { - return csmHardware, err - } - } else { - hardware.ProviderProperties = make(map[string]interface{}) - } - hardware.ProviderProperties["csm"] = properties - } - - return csmHardware, err -} - -func GetProviderMetadata(cHardware inventory.Hardware) (result interface{}, err error) { - providerPropertiesRaw, ok := cHardware.ProviderProperties["csm"] +// DecodeProviderMetadata return a Metadata structure from the given hardwares CSM Provider properties. +// If the hardware doesn't have any metadata set an empty Metadata struct will be returned. +func DecodeProviderMetadata(cHardware inventory.Hardware) (result Metadata, err error) { + ProviderMetadataRaw, ok := cHardware.ProviderMetadata[inventory.CSMProvider] if !ok { log.Debug().Any("id", cHardware.ID).Msgf("GetProviderMetadata: No CSM provider properties found") - return nil, nil // This should be ok, as its possible as not all hardware inventory items may have CSM specific data + return Metadata{}, nil // This should be ok, as its possible as not all hardware inventory items may have CSM specific data } - switch cHardware.Type { - case hardwaretypes.Node: - result = NodeMetadata{} - case hardwaretypes.Cabinet: - result = CabinetMetadata{} - default: - // This may be caused if new metadata structs are added, but not to this switch case - return nil, fmt.Errorf("hardware object (%s) has unexpected provider metadata", cHardware.ID) - } - - // Decode the Raw extra properties into a give structure + // Decode the Raw extra properties into the Metadata structure decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ DecodeHook: mapstructure.StringToIPHookFunc(), Result: &result, }) if err != nil { - return nil, err + return Metadata{}, err } - err = decoder.Decode(providerPropertiesRaw) + err = decoder.Decode(ProviderMetadataRaw) return result, err } -func GetProviderMetadataT[T any](cHardware inventory.Hardware) (*T, error) { - metadataRaw, err := GetProviderMetadata(cHardware) +func EncodeProviderMetadata(metadata Metadata) (result map[string]interface{}, err error) { + // Encode the Metadata struct into map[string]interface{} + decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToIPHookFunc(), + Result: &result, + }) if err != nil { return nil, err } - - if metadataRaw == nil { - log.Debug().Any("id", cHardware.ID).Msgf("GetProviderMetadataT: No metadata returned from GetProviderMetadata") - return nil, nil + err = decoder.Decode(metadata) + if err != nil { + return nil, err } - metadata, ok := metadataRaw.(T) - if !ok { - var expectedType T - return nil, fmt.Errorf("unexpected provider metadata type (%T) expected (%T)", metadataRaw, expectedType) - } - return &metadata, nil + return result, err } func (csm *CSM) BuildHardwareMetadata(cHardware *inventory.Hardware, rawProperties map[string]interface{}) error { - if cHardware.ProviderProperties == nil { - cHardware.ProviderProperties = map[string]interface{}{} + if cHardware == nil { + return fmt.Errorf("provided hardware is nil") + } + + metadata := Metadata{} + if cHardware.ProviderMetadata != nil { + var err error + metadata, err = DecodeProviderMetadata(*cHardware) + + if err != nil { + return errors.Join(fmt.Errorf("failed to decode CSM metadata from hardware (%v)", cHardware.ID), err) + } } switch cHardware.Type { case hardwaretypes.Cabinet: - properties := CabinetMetadata{} - if _, exists := cHardware.ProviderProperties["csm"]; exists { - // If one exists set it. - if err := mapstructure.Decode(cHardware.ProviderProperties["csm"], &properties); err != nil { - return err - } + if metadata.Cabinet == nil { + // Create an cabinet metadata object it does not exist + metadata.Cabinet = &CabinetMetadata{} } // Make changes to the node metadata // The keys of rawProperties need to match what is defined in ./cmd/cabinet/add_cabinet.go - if vlanIDRaw, exists := rawProperties[ProviderPropertyVlanId]; exists { + if vlanIDRaw, exists := rawProperties[ProviderMetadataVlanId]; exists { // Check if the VLAN exceeds the valid range for the hardware max, err := DetermineEndingVlanFromSlug(cHardware.DeviceTypeSlug, *csm.hardwareLibrary) if err != nil { return err } - // if trhe VLAN is greater than the max, fail + // if the VLAN is greater than the max, fail if vlanIDRaw.(int) > max { return fmt.Errorf("VLAN exceeds the provider's maximum range (%d). Please choose a valid VLAN", max) } if vlanIDRaw == nil { - properties.HMNVlan = nil + metadata.Cabinet.HMNVlan = nil } else { - properties.HMNVlan = IntPtr(vlanIDRaw.(int)) + metadata.Cabinet.HMNVlan = pointers.IntPtr(vlanIDRaw.(int)) } } - - cHardware.ProviderProperties["csm"] = properties - - return nil case hardwaretypes.Node: - // TODO do something interesting with the raw data, and convert it/validate it - properties := NodeMetadata{} // Create an empty one - if _, exists := cHardware.ProviderProperties["csm"]; exists { - // If one exists set it. - if err := mapstructure.Decode(cHardware.ProviderProperties["csm"], &properties); err != nil { - return err - } + if metadata.Node == nil { + // Create an cabinet metadata object it does not exist + metadata.Node = &NodeMetadata{} } + // Make changes to the node metadata // The keys of rawProperties need to match what is defined in ./cmd/node/update_node.go - if roleRaw, exists := rawProperties["role"]; exists { + if roleRaw, exists := rawProperties[ProviderMetadataRole]; exists { if roleRaw == nil { - properties.Role = nil + metadata.Node.Role = nil } else { - properties.Role = StringPtr(roleRaw.(string)) + metadata.Node.Role = pointers.StringPtr(roleRaw.(string)) } } - if subroleRaw, exists := rawProperties["subrole"]; exists { + if subroleRaw, exists := rawProperties[ProviderMetadataSubRole]; exists { if subroleRaw == nil { - properties.SubRole = nil + metadata.Node.SubRole = nil } else { - properties.SubRole = StringPtr(subroleRaw.(string)) + metadata.Node.SubRole = pointers.StringPtr(subroleRaw.(string)) } } - if nidRaw, exists := rawProperties["nid"]; exists { + if nidRaw, exists := rawProperties[ProviderMetadataNID]; exists { if nidRaw == nil { - properties.Nid = nil + metadata.Node.Nid = nil } else { - properties.Nid = IntPtr(nidRaw.(int)) + metadata.Node.Nid = pointers.IntPtr(nidRaw.(int)) } } - if aliasRaw, exists := rawProperties["alias"]; exists { + if aliasRaw, exists := rawProperties[ProviderMetadataAlias]; exists { if aliasRaw == nil { - properties.Alias = nil + metadata.Node.Alias = nil } else { - properties.Alias = []string{aliasRaw.(string)} + metadata.Node.Alias = []string{aliasRaw.(string)} } } - cHardware.ProviderProperties["csm"] = properties - - return nil default: // This hardware type doesn't have metadata for it right now return nil } + // Set the hardware metadata + metadataRaw, err := EncodeProviderMetadata(metadata) + if err != nil { + return errors.Join(fmt.Errorf("failed to encoder CSM Metadata for hardware (%v)", cHardware.ID), err) + } + + cHardware.SetProviderMetadata(inventory.CSMProvider, metadataRaw) + return nil } func (csm *CSM) RecommendCabinet(inv inventory.Inventory, deviceTypeSlug string) (recommended provider.HardwareRecommendations, err error) { @@ -274,25 +214,24 @@ func (csm *CSM) RecommendCabinet(inv inventory.Inventory, deviceTypeSlug string) // loop through the existing inventory to check for vlans log.Debug().Msg("Checking existing hardware to find recommendations") for _, cHardware := range inv.Hardware { - if cHardware.ProviderProperties == nil { - cHardware.ProviderProperties = map[string]interface{}{} - } - switch cHardware.Type { case hardwaretypes.Cabinet: log.Debug().Msgf("Checking %s (%s)", cHardware.Type, cHardware.ID.String()) - properties := CabinetMetadata{} - if _, exists := cHardware.ProviderProperties[string(inventory.CSMProvider)]; exists { - // If one exists set it. - log.Debug().Msgf("Decoding csm properties %+v", cHardware.ProviderProperties) - if err := mapstructure.Decode(cHardware.ProviderProperties[string(inventory.CSMProvider)], &properties); err != nil { - return recommended, err - } + log.Debug().Msgf("Decoding csm properties %+v", cHardware.ProviderMetadata) + + metadata, err := DecodeProviderMetadata(cHardware) + if err != nil { + return provider.HardwareRecommendations{}, errors.Join(fmt.Errorf("failed to decode CSM metadata from cabinet (%v)", cHardware.ID), err) + } + + if metadata.Cabinet == nil { + // There is no existing cabinet metadata + continue } - if properties.HMNVlan != nil { + if metadata.Cabinet.HMNVlan != nil { // add it to the slice that tracks existing vlans - existingVlans = append(existingVlans, *properties.HMNVlan) + existingVlans = append(existingVlans, *metadata.Cabinet.HMNVlan) } // add the ordinal to the existing cabinets slice for choosing a new one later @@ -347,7 +286,7 @@ func (csm *CSM) RecommendCabinet(inv inventory.Inventory, deviceTypeSlug string) // set the provider metadata recommended.ProviderMetadata = map[string]interface{}{ // there are no vlans yet, and presumably no cabinets, so set it to 1 - ProviderPropertyVlanId: chosenVlan, + ProviderMetadataVlanId: chosenVlan, } // return the recommendations @@ -377,33 +316,15 @@ func nextAvailableInt(s []int, offset int) int { } func (nm *NodeMetadata) Pretty() (prettyNm NodeMetadataStrings) { - var role, subrole, nid string - var alias []string - if nm.Role == nil { - role = "" - } else { - role = *nm.Role - } - if nm.SubRole == nil { - subrole = "" - } else { - subrole = *nm.SubRole - } - if nm.Alias == nil { - alias = []string{} - } else { + alias := []string{} + if nm.Alias != nil { alias = nm.Alias } - if nm.Nid == nil { - nid = "" - } else { - nid = fmt.Sprint(*nm.Nid) - } - prettyNm.Role = role - prettyNm.SubRole = subrole - prettyNm.Nid = nid - prettyNm.Alias = alias - - return prettyNm + return NodeMetadataStrings{ + Role: pointers.StrPtrToStr(nm.Role), + SubRole: pointers.StrPtrToStr(nm.SubRole), + Alias: alias, + Nid: pointers.IntPtrToStr(nm.Nid), + } } diff --git a/internal/provider/csm/metadata_test.go b/internal/provider/csm/metadata_test.go new file mode 100644 index 00000000..34e6c876 --- /dev/null +++ b/internal/provider/csm/metadata_test.go @@ -0,0 +1,246 @@ +/* + * + * MIT License + * + * (C) Copyright 2023 Hewlett Packard Enterprise Development LP + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ +package csm + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "github.com/Cray-HPE/cani/internal/inventory" + "github.com/Cray-HPE/cani/pkg/hardwaretypes" + "github.com/Cray-HPE/cani/pkg/pointers" + "github.com/google/uuid" + "github.com/stretchr/testify/suite" +) + +type MetadataStructTagSuite struct { + suite.Suite + + structuresUnderTest []interface{} +} + +func (suite *MetadataStructTagSuite) SetupSuite() { + suite.structuresUnderTest = []interface{}{ + Metadata{}, + NodeMetadata{}, + CabinetMetadata{}, + } +} + +func (suite *MetadataStructTagSuite) verifyUniqueStructTags(structure interface{}, tagName string) { + for _, structure := range suite.structuresUnderTest { + suite.T().Logf("Validating structure: %T", structure) + + foundTagValues := map[string]bool{} + + // Iterate over the fields of the structure to find all tags with name + structType := reflect.TypeOf(structure) + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + suite.T().Logf("Checking field: %v", field.Name) + + // Verify tag exists + tagValue, exists := field.Tag.Lookup("json") + suite.Truef(exists, "Structure %T is missing %s struct tag on field %v", structure, tagName, field) + if !exists { + continue + } + tagValue = strings.TrimSuffix(tagValue, ",omitempty") + + // Check for duplicates + if _, exists := foundTagValues[tagValue]; exists { + suite.Fail(fmt.Sprintf("Structure %T has a non-unique %s struct tag value of %v", structure, tagName, tagValue)) + } + foundTagValues[tagValue] = true + } + } +} + +func (suite *MetadataStructTagSuite) TestVerifyUniqueJSONTags() { + for _, structure := range suite.structuresUnderTest { + suite.T().Logf("Validating structure: %T", structure) + suite.verifyUniqueStructTags(structure, "json") + } +} + +func (suite *MetadataStructTagSuite) TestVerifyUniqueMapstructureTags() { + for _, structure := range suite.structuresUnderTest { + suite.verifyUniqueStructTags(structure, "mapstructure") + } +} + +func (suite *MetadataStructTagSuite) TestVerifyJSONMapstructureTagsMatch() { + for _, structure := range suite.structuresUnderTest { + // Iterate over the fields of the structure to find all tags with name + structType := reflect.TypeOf(structure) + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + suite.T().Logf("Checking field: %v", field.Name) + + // Grab JSON tag value + jsonTagValue, exists := field.Tag.Lookup("json") + suite.Truef(exists, "Structure %T is missing json struct tag on field %v", structure, field) + if !exists { + continue + } + jsonTagValue = strings.TrimSuffix(jsonTagValue, ",omitempty") + + // Grab the mapstructure tag + mapstructTagValue, exists := field.Tag.Lookup("mapstructure") + suite.Truef(exists, "Structure %T is missing mapstructure struct tag on field %v", structure, field) + if !exists { + continue + } + mapstructTagValue = strings.TrimSuffix(mapstructTagValue, ",omitempty") + + // Verify they are the same + suite.Equal(jsonTagValue, mapstructTagValue, "Structure %T has mis-match json/mapstructure tag values on field %v") + } + } +} + +func TestMetadataStructTagSuite(t *testing.T) { + suite.Run(t, new(MetadataStructTagSuite)) +} + +type BuildHardwareMetadataTestSuite struct { + suite.Suite + + csm *CSM +} + +func (suite *BuildHardwareMetadataTestSuite) SetupSuite() { + hardwareTypeLibrary, err := hardwaretypes.NewEmbeddedLibrary() + suite.NoError(err) + + // Stand up a minimal CSM provider to run BuildHardwareMetadata + suite.csm = &CSM{ + hardwareLibrary: hardwareTypeLibrary, + } +} + +func (suite *BuildHardwareMetadataTestSuite) TestCabinet() { + rawProperties := map[string]interface{}{ + ProviderMetadataVlanId: 1234, + } + + hardware := inventory.Hardware{ + ID: uuid.New(), + Type: hardwaretypes.Cabinet, + DeviceTypeSlug: "hpe-ex4000", + Vendor: "HPE", + Model: "EX4000", + Status: inventory.HardwareStatusStaged, + } + + err := suite.csm.BuildHardwareMetadata(&hardware, rawProperties) + suite.NoError(err) + + suite.NotNil(hardware.ProviderMetadata) + suite.Contains(hardware.ProviderMetadata, inventory.CSMProvider, "CSM Metadata is missing from cabinet") + + csmMetadata, err := DecodeProviderMetadata(hardware) + suite.NoError(err) + + suite.Nil(csmMetadata.Node, "Cabinet has Node metadata set") + suite.NotNil(csmMetadata.Cabinet, "Cabinet is missing node metadata") + + expectedMetadata := Metadata{ + Cabinet: &CabinetMetadata{ + HMNVlan: pointers.IntPtr(1234), + }, + } + suite.EqualValues(expectedMetadata, csmMetadata) +} + +func (suite *BuildHardwareMetadataTestSuite) TestNode() { + +} + +func (suite *BuildHardwareMetadataTestSuite) TestNodeUpdateOneFieldExistingData() { + +} + +func TestBuildHardwareMetadataTestSuite(t *testing.T) { + suite.Run(t, new(BuildHardwareMetadataTestSuite)) +} + +type EncodeProviderMetadataTestSuite struct { + suite.Suite +} + +func (suite *EncodeProviderMetadataTestSuite) TestCabinet() { + metadata := Metadata{ + Cabinet: &CabinetMetadata{ + HMNVlan: pointers.IntPtr(4321), + }, + } + + metadataRaw, err := EncodeProviderMetadata(metadata) + suite.NoError(err) + + expectedMetadata := map[string]interface{}{ + "Cabinet": map[string]interface{}{ + "HMNVlan": pointers.IntPtr(4321), + }, + } + suite.EqualValues(expectedMetadata, metadataRaw) +} + +func (suite *EncodeProviderMetadataTestSuite) TestNode() { + metadata := Metadata{ + Node: &NodeMetadata{ + Role: pointers.StringPtr("Management"), + SubRole: pointers.StringPtr("Worker"), + Nid: pointers.IntPtr(10000), + Alias: []string{ + "ncn-w001", + }, + }, + } + + metadataRaw, err := EncodeProviderMetadata(metadata) + suite.NoError(err) + + expectedMetadata := map[string]interface{}{ + "Node": map[string]interface{}{ + "Role": pointers.StringPtr("Management"), + "SubRole": pointers.StringPtr("Worker"), + "Nid": pointers.IntPtr(10000), + "Alias": []string{ + "ncn-w001", + }, + "AdditionalProperties": map[string]interface{}(nil), + }, + } + suite.EqualValues(expectedMetadata, metadataRaw) +} + +func TestEncodeProviderMetadataTestSuite(t *testing.T) { + suite.Run(t, new(EncodeProviderMetadataTestSuite)) +} diff --git a/internal/provider/csm/reconcile.go b/internal/provider/csm/reconcile.go index 6c2509fd..10606e68 100644 --- a/internal/provider/csm/reconcile.go +++ b/internal/provider/csm/reconcile.go @@ -471,17 +471,17 @@ func reconcileNetworkChanges(datastore inventory.Datastore, hardwareTypeLibrary // Allocate Node IPs for _, node := range sortByLocationPath(allHardware.FilterHardwareByTypeStatus(inventory.HardwareStatusStaged, hardwaretypes.Node)) { - providerProperties, err := GetProviderMetadataT[NodeMetadata](node) + providerMetadata, err := DecodeProviderMetadata(node) if err != nil { return nil, errors.Join(fmt.Errorf("failed to get provider properties for node (%s)", node.ID), err) } var role, subRole string - if providerProperties.Role != nil { - role = *providerProperties.Role + if providerMetadata.Node.Role != nil { + role = *providerMetadata.Node.Role } - if providerProperties.SubRole != nil { - subRole = *providerProperties.SubRole + if providerMetadata.Node.SubRole != nil { + subRole = *providerMetadata.Node.SubRole } switch role { @@ -505,17 +505,17 @@ func reconcileNetworkChanges(datastore inventory.Datastore, hardwareTypeLibrary // Deallocate Node IPs for _, node := range allHardware.FilterHardwareByTypeStatus(inventory.HardwareStatusStaged, hardwaretypes.Node) { - providerProperties, err := GetProviderMetadataT[NodeMetadata](node) + providerMetadata, err := DecodeProviderMetadata(node) if err != nil { return nil, errors.Join(fmt.Errorf("failed to get provider properties for node (%s)", node.ID), err) } var role, subRole string - if providerProperties.Role != nil { - role = *providerProperties.Role + if providerMetadata.Node.Role != nil { + role = *providerMetadata.Node.Role } - if providerProperties.SubRole != nil { - subRole = *providerProperties.SubRole + if providerMetadata.Node.SubRole != nil { + subRole = *providerMetadata.Node.SubRole } switch role { diff --git a/internal/provider/csm/sls_state_generator.go b/internal/provider/csm/sls_state_generator.go index 4d226582..38af78ae 100644 --- a/internal/provider/csm/sls_state_generator.go +++ b/internal/provider/csm/sls_state_generator.go @@ -378,7 +378,7 @@ func BuildSLSHardware(cHardware inventory.Hardware, locationPath inventory.Locat // Not represented in SLS return sls_client.Hardware{}, nil case hardwaretypes.Node: - metadata, err := GetProviderMetadataT[NodeMetadata](cHardware) + metadata, err := DecodeProviderMetadata(cHardware) if err != nil { return sls_client.Hardware{}, errors.Join( fmt.Errorf("failed to get provider metadata from hardware (%s)", cHardware.ID), @@ -393,7 +393,7 @@ func BuildSLSHardware(cHardware inventory.Hardware, locationPath inventory.Locat nodeExtraProperties.CaniLastModified = time.Now().UTC().String() // Logical metadata - if metadata != nil { + if metadata.Node != nil { // In order to properly populate SLS several bits of information are required. // This information should have been collected when hardware was added to the inventory @@ -401,17 +401,17 @@ func BuildSLSHardware(cHardware inventory.Hardware, locationPath inventory.Locat // - SubRole // - NID // - Alias/Common Name - if metadata.Role != nil { - nodeExtraProperties.Role = *metadata.Role + if metadata.Node.Role != nil { + nodeExtraProperties.Role = *metadata.Node.Role } - if metadata.SubRole != nil { - nodeExtraProperties.Role = *metadata.SubRole + if metadata.Node.SubRole != nil { + nodeExtraProperties.Role = *metadata.Node.SubRole } - if metadata.Nid != nil { - nodeExtraProperties.NID = int32(*metadata.Nid) + if metadata.Node.Nid != nil { + nodeExtraProperties.NID = int32(*metadata.Node.Nid) } - if metadata.Alias != nil { - nodeExtraProperties.Aliases = metadata.Alias + if metadata.Node.Alias != nil { + nodeExtraProperties.Aliases = metadata.Node.Alias } log.Info().Any("nodeEp", nodeExtraProperties).Msgf("Generated Extra Properties for %s", xname.String()) diff --git a/internal/provider/csm/validation.go b/internal/provider/csm/validation.go index 12f8d7be..578d93a0 100644 --- a/internal/provider/csm/validation.go +++ b/internal/provider/csm/validation.go @@ -137,7 +137,7 @@ func (csm *CSM) validateInternalNode(allHardware map[uuid.UUID]inventory.Hardwar } log.Debug().Msgf("Validating %s: %v", cHardware.ID, cHardware) - metadata, err := GetProviderMetadataT[NodeMetadata](cHardware) + metadata, err := DecodeProviderMetadata(cHardware) if err != nil { return errors.Join( fmt.Errorf("failed to get provider metadata from hardware (%s)", cHardware.ID), @@ -146,9 +146,9 @@ func (csm *CSM) validateInternalNode(allHardware map[uuid.UUID]inventory.Hardwar } // There is no metadata for this node - if metadata == nil { + if metadata.Node == nil { log.Debug().Msgf("No metadata found for %s", cHardware.ID) - metadata = &NodeMetadata{} + metadata.Node = &NodeMetadata{} } // @@ -161,46 +161,46 @@ func (csm *CSM) validateInternalNode(allHardware map[uuid.UUID]inventory.Hardwar validationResult := results[cHardware.ID] // Verify all specified Roles are valid - if metadata.Role != nil { - if !validRoles[*metadata.Role] { + if metadata.Node.Role != nil { + if !validRoles[*metadata.Node.Role] { validationResult.Errors = append(validationResult.Errors, - fmt.Sprintf("Specified role (%s) is invalid, choose from: %s", *metadata.Role, strings.Join(csm.ValidRoles, ", ")), + fmt.Sprintf("Specified role (%s) is invalid, choose from: %s", *metadata.Node.Role, strings.Join(csm.ValidRoles, ", ")), ) } } // Verify all specified SubRoles are valid - if metadata.SubRole != nil { - if !validSubRoles[*metadata.SubRole] { + if metadata.Node.SubRole != nil { + if !validSubRoles[*metadata.Node.SubRole] { validationResult.Errors = append(validationResult.Errors, - fmt.Sprintf("Specified sub-role (%s) is invalid, choose from: %s", *metadata.SubRole, strings.Join(csm.ValidSubRoles, ", ")), + fmt.Sprintf("Specified sub-role (%s) is invalid, choose from: %s", *metadata.Node.SubRole, strings.Join(csm.ValidSubRoles, ", ")), ) } } // Verify NID is valid - if metadata.Nid != nil { - nodeNIDLookup[*metadata.Nid] = append(nodeNIDLookup[*metadata.Nid], cHardware.ID) - if *metadata.Nid <= 0 { + if metadata.Node.Nid != nil { + nodeNIDLookup[*metadata.Node.Nid] = append(nodeNIDLookup[*metadata.Node.Nid], cHardware.ID) + if *metadata.Node.Nid <= 0 { validationResult.Errors = append(validationResult.Errors, - fmt.Sprintf("Specified NID (%d) invalid, needs to be positive integer", *metadata.Nid), + fmt.Sprintf("Specified NID (%d) invalid, needs to be positive integer", *metadata.Node.Nid), ) } } // Verify Alias is valid - if metadata.Alias != nil { - for _, alias := range metadata.Alias { + if metadata.Node.Alias != nil { + for _, alias := range metadata.Node.Alias { nodeAliasLookup[alias] = append(nodeAliasLookup[alias], cHardware.ID) - if metadata.Alias != nil && len(alias) == 0 { + if metadata.Node.Alias != nil && len(alias) == 0 { validationResult.Errors = append(validationResult.Errors, "Specified Alias is empty") } // TODO a regex here might be better if strings.Contains(alias, " ") { validationResult.Errors = append(validationResult.Errors, - fmt.Sprintf("Specified alias (%d) is invalid, alias contains spaces", *metadata.Nid), + fmt.Sprintf("Specified alias (%d) is invalid, alias contains spaces", *metadata.Node.Nid), ) } } @@ -217,13 +217,13 @@ func (csm *CSM) validateInternalNode(allHardware map[uuid.UUID]inventory.Hardwar // - Alias // - NID // - Role - if metadata.Role == nil { + if metadata.Node.Role == nil { validationResult.Errors = append(validationResult.Errors, "Missing required information: Role is not set") } - if metadata.Nid == nil { + if metadata.Node.Nid == nil { validationResult.Errors = append(validationResult.Errors, "Missing required information: NID is not set") } - if metadata.Alias == nil { + if metadata.Node.Alias == nil { validationResult.Errors = append(validationResult.Errors, "Missing required information: Alias is not set") } @@ -277,7 +277,7 @@ func (csm *CSM) validateInternalCabinet(allHardware map[uuid.UUID]inventory.Hard log.Debug().Msgf("Validating %s: %v", cHardware.ID, cHardware) - metadata, err := GetProviderMetadataT[CabinetMetadata](cHardware) + metadata, err := DecodeProviderMetadata(cHardware) if err != nil { return errors.Join( fmt.Errorf("failed to get provider metadata from hardware (%s)", cHardware.ID), @@ -286,22 +286,22 @@ func (csm *CSM) validateInternalCabinet(allHardware map[uuid.UUID]inventory.Hard } // There is no metadata for this cabinet - if metadata == nil { + if metadata.Cabinet == nil { log.Debug().Msgf("No metadata found for %s", cHardware.ID) - metadata = &CabinetMetadata{} + metadata.Cabinet = &CabinetMetadata{} } validationResult := results[cHardware.ID] - if metadata.HMNVlan != nil { + if metadata.Cabinet.HMNVlan != nil { // Verify the vlan is within the allowed range - if !(0 <= *metadata.HMNVlan && *metadata.HMNVlan <= 4094) { + if !(0 <= *metadata.Cabinet.HMNVlan && *metadata.Cabinet.HMNVlan <= 4094) { validationResult.Errors = append(validationResult.Errors, - fmt.Sprintf("Specified HMN Vlan (%d) is invalid, must be in range: 0-4094", *metadata.HMNVlan), + fmt.Sprintf("Specified HMN Vlan (%d) is invalid, must be in range: 0-4094", *metadata.Cabinet.HMNVlan), ) } - cabinetVLANLookup[*metadata.HMNVlan] = append(cabinetVLANLookup[*metadata.HMNVlan], cHardware.ID) + cabinetVLANLookup[*metadata.Cabinet.HMNVlan] = append(cabinetVLANLookup[*metadata.Cabinet.HMNVlan], cHardware.ID) } if enableRequiredDataChecks { @@ -325,7 +325,7 @@ func (csm *CSM) validateInternalCabinet(allHardware map[uuid.UUID]inventory.Hard } if cecManagedCabinet { - if metadata.HMNVlan == nil { + if metadata.Cabinet.HMNVlan == nil { validationResult.Errors = append(validationResult.Errors, "Missing required information: HMN Vlan is not set") } } diff --git a/internal/provider/interface.go b/internal/provider/interface.go index cc3f4864..6eb54bd3 100644 --- a/internal/provider/interface.go +++ b/internal/provider/interface.go @@ -51,15 +51,18 @@ type InventoryProvider interface { // Reconcile CANI's inventory state with the external inventory state and apply required changes Reconcile(ctx context.Context, datastore inventory.Datastore, dryrun bool) error + // RecommendCabinet returns recommended settings for adding a cabinet + RecommendCabinet(inv inventory.Inventory, deviceTypeSlug string) (HardwareRecommendations, error) + + // + // Provider Hardware Metadata + // + // Build metadata, and add ito the hardware object // This function could return the data to put into object BuildHardwareMetadata(hw *inventory.Hardware, rawProperties map[string]interface{}) error - // RecommendCabinet returns recommended settings for adding a cabinet - RecommendCabinet(inv inventory.Inventory, deviceTypeSlug string) (HardwareRecommendations, error) - GetFields(hw *inventory.Hardware, fieldNames []string) (values []string, err error) - SetFields(hw *inventory.Hardware, values map[string]string) (result SetFieldsResult, err error) } diff --git a/pkg/pointers/pointers.go b/pkg/pointers/pointers.go new file mode 100644 index 00000000..2f5d4158 --- /dev/null +++ b/pkg/pointers/pointers.go @@ -0,0 +1,52 @@ +/* + * + * MIT License + * + * (C) Copyright 2023 Hewlett Packard Enterprise Development LP + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ +package pointers + +import "fmt" + +func IntPtrToStr(in *int) string { + if in == nil { + return "" + } + + return fmt.Sprintf("%d", *in) +} + +func StrPtrToStr(in *string) string { + if in == nil { + return "" + } + + return *in +} + +func StringPtr(s string) *string { + return &s +} + +func IntPtr(i int) *int { + return &i +}