diff --git a/cmd/smd/smd-api.go b/cmd/smd/smd-api.go index 188ad9ba..ecfa8270 100644 --- a/cmd/smd/smd-api.go +++ b/cmd/smd/smd-api.go @@ -39,9 +39,9 @@ import ( "github.com/Cray-HPE/hms-xname/xnametypes" "github.com/OpenCHAMI/smd/v2/internal/hmsds" rf "github.com/OpenCHAMI/smd/v2/pkg/redfish" + "github.com/OpenCHAMI/smd/v2/pkg/schemas" "github.com/OpenCHAMI/smd/v2/pkg/sm" "github.com/go-chi/chi/v5" - "github.com/openchami/schemas/schemas" redfish "github.com/openchami/schemas/schemas/csm" ) diff --git a/pkg/schemas/README.md b/pkg/schemas/README.md new file mode 100644 index 00000000..498eabf5 --- /dev/null +++ b/pkg/schemas/README.md @@ -0,0 +1,74 @@ + +# OpenCHAMI Schema Repository + +Welcome to the OpenCHAMI Schema Repository! This repository serves as the central source for all JSON schemas used across the OpenCHAMI consortium. By maintaining a unified set of schemas, we ensure consistency and compatibility across all OpenCHAMI projects. + +## Overview + +This repository contains JSON schemas that define the structure and validation rules for data used across various OpenCHAMI projects. Each schema is generated from Go structs using reflection, ensuring that the schema remains consistent with the underlying data models. + +## Directory Structure + +The repository is organized as follows: + +- **schemas/**: This directory contains all the Go structs that will become JSON schemas, each in its own file. Schemas are named according to their purpose or associated data structure. +- **examples/**: This directory contains example payloads that conform to the schemas. These examples serve as references for developers implementing or integrating with OpenCHAMI components. +- **docs/**: Documentation related to the schemas, including detailed descriptions and usage guidelines, is found here. + +## How to Contribute + +We welcome contributions to the schema repository! Here’s how you can get involved: + +1. **Fork the Repository**: Start by forking this repository to your own GitHub account. +2. **Create a Branch**: Create a new branch for your changes. +3. **Add/Update Schemas**: Modify or add new Go structs in the appropriate files. Use reflection to generate the corresponding JSON schema. +4. **Test Your Changes**: Ensure that your changes are valid and conform to the repository’s guidelines. Include example payloads in the `examples/` directory. +5. **Submit a Pull Request**: Once your changes are ready, submit a pull request for review. + +## Generating JSON Schemas + +Schemas in this repository are generated from Go structs using reflection. Here’s an example of how to generate a JSON schema: + +```go +package schemas + +// Example struct definition +type Node struct { + ID string `json:"id"` + Name string `json:"name"` + IP string `json:"ip"` +} +``` + +## Schema Versioning + +Each schema is versioned using an envelope/header format. This allows servers to verify the schema version before processing the contained data. Here’s an example: + +```go +package schemas + +// Envelope structure for schema versioning +type Envelope struct { + SchemaID string `json:"schema_id"` + Version string `json:"version"` + Payload interface{} `json:"payload"` +} +``` + +## Referencing Schemas + +All schemas are published on a webpage for easy access and reference. Servers and clients can use these schemas to validate data and ensure compliance with OpenCHAMI standards. + +## Resources + +- [Kubernetes API Conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md) +- [Kubernetes API Versioning](https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning) +- [OpenCHAMI Node Orchestrator](https://github.com/OpenCHAMI/node-orchestrator) + +## License + +This repository is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. + +--- + +Thank you for contributing to the OpenCHAMI Schema Repository! Together, we can maintain a consistent and reliable set of data models for all OpenCHAMI projects. diff --git a/pkg/schemas/csm/components.go b/pkg/schemas/csm/components.go new file mode 100644 index 00000000..526e028c --- /dev/null +++ b/pkg/schemas/csm/components.go @@ -0,0 +1,287 @@ +package csm + +import ( + "github.com/google/uuid" + "github.com/invopop/jsonschema" +) + +// Component represents a CSM Component +type Component struct { + UID uuid.UUID `json:"UID,omitempty" db:"uid"` + ID string `json:"ID" db:"id" jsonschema:"description=Xname"` + Type ComponentType `json:"Type" db:"type"` + Subtype string `json:"Subtype,omitempty" db:"subtype"` + Role ComponentRole `json:"Role,omitempty" db:"role"` + SubRole ComponentSubRole `json:"SubRole,omitempty" db:"sub_role"` + NetType ComponentNetType `json:"NetType,omitempty" db:"net_type"` + Arch ComponentArch `json:"Arch,omitempty" db:"arch"` + Class ComponentClass `json:"Class,omitempty" db:"class"` + State ComponentState `json:"State,omitempty" db:"state"` + Flag ComponentFlag `json:"Flag,omitempty" db:"flag"` + Enabled bool `json:"Enabled,omitempty" db:"enabled"` + SwStatus string `json:"SoftwareStatus,omitempty" db:"sw_status"` + NID int `json:"NID,omitempty" db:"nid"` + ReservationDisabled bool `json:"ReservationDisabled,omitempty" db:"reservation_disabled"` + Locked bool `json:"Locked,omitempty" db:"locked"` +} + +type ComponentType string + +const ( + TypeCDU ComponentType = "CDU" + TypeCabinetCDU ComponentType = "CabinetCDU" + TypeCabinetPDU ComponentType = "CabinetPDU" + TypeCabinetPDUOutlet ComponentType = "CabinetPDUOutlet" + TypeCabinetPDUPowerConnector ComponentType = "CabinetPDUPowerConnector" + TypeCabinetPDUController ComponentType = "CabinetPDUController" + TypeCabinet ComponentType = "Cabinet" + TypeChassis ComponentType = "Chassis" + TypeChassisBMC ComponentType = "ChassisBMC" + TypeCMMRectifier ComponentType = "CMMRectifier" + TypeCMMFpga ComponentType = "CMMFpga" + TypeCEC ComponentType = "CEC" + TypeComputeModule ComponentType = "ComputeModule" + TypeRouterModule ComponentType = "RouterModule" + TypeNodeBMC ComponentType = "NodeBMC" + TypeNodeEnclosure ComponentType = "NodeEnclosure" + TypeNodeEnclosurePowerSupply ComponentType = "NodeEnclosurePowerSupply" + TypeHSNBoard ComponentType = "HSNBoard" + TypeMgmtSwitch ComponentType = "MgmtSwitch" + TypeMgmtHLSwitch ComponentType = "MgmtHLSwitch" + TypeCDUMgmtSwitch ComponentType = "CDUMgmtSwitch" + TypeNode ComponentType = "Node" + TypeVirtualNode ComponentType = "VirtualNode" + TypeProcessor ComponentType = "Processor" + TypeDrive ComponentType = "Drive" + TypeStorageGroup ComponentType = "StorageGroup" + TypeNodeNIC ComponentType = "NodeNIC" + TypeMemory ComponentType = "Memory" + TypeNodeAccel ComponentType = "NodeAccel" + TypeNodeAccelRiser ComponentType = "NodeAccelRiser" + TypeNodeFpga ComponentType = "NodeFpga" + TypeHSNAsic ComponentType = "HSNAsic" + TypeRouterFpga ComponentType = "RouterFpga" + TypeRouterBMC ComponentType = "RouterBMC" + TypeHSNLink ComponentType = "HSNLink" + TypeHSNConnector ComponentType = "HSNConnector" + TypeINVALID ComponentType = "INVALID" +) + +func (ComponentType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(TypeCDU), + string(TypeCabinetCDU), + string(TypeCabinetPDU), + string(TypeCabinetPDUOutlet), + string(TypeCabinetPDUPowerConnector), + string(TypeCabinetPDUController), + string(TypeCabinet), + string(TypeChassis), + string(TypeChassisBMC), + string(TypeCMMRectifier), + string(TypeCMMFpga), + string(TypeCEC), + string(TypeComputeModule), + string(TypeRouterModule), + string(TypeNodeBMC), + string(TypeNodeEnclosure), + string(TypeNodeEnclosurePowerSupply), + string(TypeHSNBoard), + string(TypeMgmtSwitch), + string(TypeMgmtHLSwitch), + string(TypeCDUMgmtSwitch), + string(TypeNode), + string(TypeVirtualNode), + string(TypeProcessor), + string(TypeDrive), + string(TypeStorageGroup), + string(TypeNodeNIC), + string(TypeMemory), + string(TypeNodeAccel), + string(TypeNodeAccelRiser), + string(TypeNodeFpga), + string(TypeHSNAsic), + string(TypeRouterFpga), + string(TypeRouterBMC), + string(TypeHSNLink), + string(TypeHSNConnector), + string(TypeINVALID), + }, + Description: "This is the CSM component type category. It has a particular xname format and represents the kind of component that can occupy that location. Not to be confused with RedfishType which is Redfish specific and only used when providing Redfish endpoint data from discovery.", + } +} + +// ComponentState represents the state of an CSM component +type ComponentState string + +const ( + StateUnknown ComponentState = "Unknown" // The State is unknown. Appears missing but has not been confirmed as empty. + StateEmpty ComponentState = "Empty" // The location is not populated with a component + StatePopulated ComponentState = "Populated" // Present (not empty), but no further track can or is being done. + StateOff ComponentState = "Off" // Present but powered off + StateOn ComponentState = "On" // Powered on. If no heartbeat mechanism is available, its software state may be unknown. + + StateStandby ComponentState = "Standby" // No longer Ready and presumed dead. It typically means HB has been lost (w/alert). + StateHalt ComponentState = "Halt" // No longer Ready and halted. OS has been gracefully shutdown or panicked (w/ alert). + StateReady ComponentState = "Ready" // Both On and Ready to provide its expected services, i.e. used for jobs. +) + +func (ComponentState) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(StateUnknown), + string(StateEmpty), + string(StatePopulated), + string(StateOff), + string(StateOn), + string(StateStandby), + string(StateHalt), + string(StateReady), + }, + Description: "The state of an CSM component", + } +} + +type ComponentFlag string + +// Valid flag values. +const ( + FlagUnknown ComponentFlag = "Unknown" + FlagOK ComponentFlag = "OK" // Functioning properly + FlagWarning ComponentFlag = "Warning" // Continues to operate, but has an issue that may require attention. + FlagAlert ComponentFlag = "Alert" // No longer operating as expected. The state may also have changed due to error. + FlagLocked ComponentFlag = "Locked" // Another service has reserved this component. +) + +func (ComponentFlag) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(FlagUnknown), + string(FlagOK), + string(FlagWarning), + string(FlagAlert), + string(FlagLocked), + }, + Description: "The flag of an CSM component", + } +} + +type ComponentRole string + +// Valid role values. +const ( + RoleCompute ComponentRole = "Compute" + RoleService ComponentRole = "Service" + RoleSystem ComponentRole = "System" + RoleApplication ComponentRole = "Application" + RoleStorage ComponentRole = "Storage" + RoleManagement ComponentRole = "Management" +) + +func (ComponentRole) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(RoleCompute), + string(RoleService), + string(RoleSystem), + string(RoleApplication), + string(RoleStorage), + string(RoleManagement), + }, + Description: "The role of an CSM component", + } +} + +type ComponentSubRole string + +// Valid SubRole values. +const ( + SubRoleMaster ComponentSubRole = "Master" + SubRoleWorker ComponentSubRole = "Worker" + SubRoleStorage ComponentSubRole = "Storage" +) + +func (ComponentSubRole) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(SubRoleMaster), + string(SubRoleWorker), + string(SubRoleStorage), + }, + Description: "The sub-role of an CSM component", + } +} + +type ComponentNetType string + +const ( + NetSling ComponentNetType = "Sling" + NetInfiniband ComponentNetType = "Infiniband" + NetEthernet ComponentNetType = "Ethernet" + NetOEM ComponentNetType = "OEM" // Placeholder for non-slingshot + NetNone ComponentNetType = "None" +) + +func (ComponentNetType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(NetSling), + string(NetInfiniband), + string(NetEthernet), + string(NetOEM), + string(NetNone), + }, + Description: "The network type of an CSM component", + } +} + +type ComponentArch string + +const ( + ArchX86 ComponentArch = "X86" + ArchARM ComponentArch = "ARM" + ArchUnknown ComponentArch = "UNKNOWN" + ArchOther ComponentArch = "Other" +) + +func (ComponentArch) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(ArchX86), + string(ArchARM), + string(ArchUnknown), + string(ArchOther), + }, + Description: "The architecture of an CSM component", + } +} + +type ComponentClass string + +const ( + ClassRiver ComponentClass = "River" + ClassMountain ComponentClass = "Mountain" + ClassHill ComponentClass = "Hill" + ClassOther ComponentClass = "Other" +) + +func (ComponentClass) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Enum: []interface{}{ + string(ClassRiver), + string(ClassMountain), + string(ClassHill), + string(ClassOther), + }, + Description: "The class of an CSM component", + } +} diff --git a/pkg/schemas/csm/redfish_endpoints.go b/pkg/schemas/csm/redfish_endpoints.go new file mode 100644 index 00000000..e6887464 --- /dev/null +++ b/pkg/schemas/csm/redfish_endpoints.go @@ -0,0 +1,44 @@ +package csm + +import ( + "time" + + "github.com/google/uuid" +) + +type DiscoveryInfo struct { + LastAttempt time.Time `json:"LastAttempt,omitempty" jsonschema:"description=The time the last discovery attempt took place,format=date-time,readOnly=true"` + LastStatus string `json:"LastStatus,omitempty" jsonschema:"description=Describes the outcome of the last discovery attempt,enum=EndpointInvalid,enum=EPResponseFailedDecode,enum=HTTPsGetFailed,enum=NotYetQueried,enum=VerificationFailed,enum=ChildVerificationFailed,enum=DiscoverOK,readOnly=true"` + RedfishVersion string `json:"RedfishVersion,omitempty" jsonschema:"description=Version of Redfish as reported by the RF service root,readOnly=true"` +} + +type RedfishDiscovery struct { + EntrypointID string `json:"EntrypointID,omitempty" jsonschema:"description=ID of the entrypoint that was used to discover the endpoint"` + UID uuid.UUID `json:"UID,omitempty" jsonschema:"$ref=#/definitions/UUID.1.0.0"` + URI string `json:"EndpointID,omitempty" jsonschema:"description=ID of the endpoint that was discovered"` + Attempted time.Time `json:"Attempted,omitempty" jsonschema:"description=Time the discovery was started,format=date-time"` + Completed time.Time `json:"Completed,omitempty" jsonschema:"description=Time the discovery was completed,format=date-time"` + Status string `json:"Status,omitempty" jsonschema:"description=Describes the outcome of the discovery attempt,enum=EndpointInvalid,enum=EPResponseFailedDecode,enum=HTTPsGetFailed,enum=NotYetQueried,enum=VerificationFailed,enum=ChildVerificationFailed,enum=DiscoverOK"` + Payload RedfishEndpoint `json:"Payload,omitempty" jsonschema:"description=The discovered endpoint"` +} + +type RedfishEndpoint struct { + ID string `json:"ID" jsonschema:"description=HMS Logical component type e.g. NodeBMC, ChassisBMC.,$ref=#/definitions/HMSType.1.0.0"` + Type ComponentType `json:"Type,omitempty"` + Name string `json:"Name,omitempty" jsonschema:"description=This is an arbitrary, user-provided name for the endpoint. It can describe anything that is not captured by the ID/xname."` + Hostname string `json:"Hostname,omitempty" jsonschema:"description=Hostname of the endpoint's FQDN, will always be the host portion of the fully-qualified domain name. Note that the hostname should normally always be the same as the ID field (i.e. xname) of the endpoint."` + Domain string `json:"Domain,omitempty" jsonschema:"description=Domain of the endpoint's FQDN. Will always match remaining non-hostname portion of fully-qualified domain name (FQDN)."` + FQDN string `json:"FQDN,omitempty" jsonschema:"description=Fully-qualified domain name of RF endpoint on management network. This is not writable because it is made up of the Hostname and Domain."` + Enabled bool `json:"Enabled,omitempty" jsonschema:"description=To disable a component without deleting its data from the database, can be set to false,example=true"` + URI string `json:"URI,omitempty" jsonschema:"description=URI of the Redfish service root"` + UID uuid.UUID `json:"UUID,omitempty" jsonschema:"$ref=#/definitions/UUID.1.0.0"` + User string `json:"User,omitempty" jsonschema:"description=Username to use when interrogating endpoint"` + Password string `json:"Password,omitempty" jsonschema:"description=Password to use when interrogating endpoint, normally suppressed in output."` + UseSSDP bool `json:"UseSSDP,omitempty" jsonschema:"description=Whether to use SSDP for discovery if the EP supports it."` + MacRequired bool `json:"MacRequired,omitempty" jsonschema:"description=Whether the MAC must be used (e.g. in River) in setting up geolocation info so the endpoint's location in the system can be determined. The MAC does not need to be provided when creating the endpoint if the endpoint type can arrive at a geolocated hostname on its own."` + MACAddr string `json:"MACAddr,omitempty" jsonschema:"description=This is the MAC on the of the Redfish Endpoint on the management network, i.e. corresponding to the FQDN field's Ethernet interface where the root service is running. Not the HSN MAC. This is a MAC address in the standard colon-separated 12 byte hex format.,pattern=^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$,example=ae:12:e2:ff:89:9d"` + IPAddress string `json:"IPAddress,omitempty" jsonschema:"description=This is the IP of the Redfish Endpoint on the management network, i.e. corresponding to the FQDN field's Ethernet interface where the root service is running. This may be IPv4 or IPv6,example=10.254.2.10"` + RediscoverOnUpdate bool `json:"RediscoverOnUpdate,omitempty" jsonschema:"description=Trigger a rediscovery when endpoint info is updated."` + TemplateID string `json:"TemplateID,omitempty" jsonschema:"description=Links to a discovery template defining how the endpoint should be discovered."` + DiscoveryInfo DiscoveryInfo `json:"DiscoveryInfo,omitempty" jsonschema:"description=Contains info about the discovery status of the given endpoint,readOnly=true"` +} diff --git a/pkg/schemas/csm/xnames.go b/pkg/schemas/csm/xnames.go new file mode 100644 index 00000000..11157d34 --- /dev/null +++ b/pkg/schemas/csm/xnames.go @@ -0,0 +1,223 @@ +package csm + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + + "github.com/invopop/jsonschema" +) + +type NodeXname struct { + Value string +} + +func (n NodeXname) Cabinet() (int, error) { + if n.Value == "" { + return 0, fmt.Errorf("node does not have an XName") + } + return extractXNameComponents(n.Value).Cabinet, nil +} + +func (n NodeXname) Chassis() (int, error) { + if n.Value == "" { + return 0, fmt.Errorf("node does not have an XName") + } + return extractXNameComponents(n.Value).Chassis, nil +} + +func (n NodeXname) Slot() (int, error) { + if n.Value == "" { + return 0, fmt.Errorf("node does not have an XName") + } + return extractXNameComponents(n.Value).Slot, nil +} + +func (n NodeXname) NodePosition() (int, error) { + if n.Value == "" { + return 0, fmt.Errorf("node does not have an XName") + } + return extractXNameComponents(n.Value).NodePosition, nil +} + +func (n NodeXname) BMCPosition() (int, error) { + if n.Value == "" { + return 0, fmt.Errorf("node does not have an XName") + } + return extractXNameComponents(n.Value).BMCPosition, nil +} + +func (n NodeXname) String() string { + return n.Value +} + +type XNameComponents struct { + Cabinet int `json:"cabinet"` + Chassis int `json:"chassis"` + Slot int `json:"slot"` + BMCPosition int `json:"bmc_position"` + NodePosition int `json:"node_position"` + Type string `json:"type"` // 'n' for node, 'b' for BMC +} + +func extractXNameComponents(xname string) XNameComponents { + var components XNameComponents + _, err := fmt.Sscanf(xname, "x%dc%ds%db%dn%d", &components.Cabinet, &components.Chassis, &components.Slot, &components.BMCPosition, &components.NodePosition) + if err == nil { + components.Type = "n" + return components + } + _, err = fmt.Sscanf(xname, "x%dc%ds%db%d", &components.Cabinet, &components.Chassis, &components.Slot, &components.BMCPosition) + if err == nil { + components.Type = "b" + return components + } + return components +} + +func (NodeXname) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Title: "NodeXName", + Description: "XName for a compute node", + Pattern: `^x(\d{3,5})c(\d{1,3})s(\d{1,3})b(\d{1,3})n(\d{1,3})$`, + } +} + +func (xname NodeXname) MarshalJSON() ([]byte, error) { + return json.Marshal(xname.Value) +} + +func (xname *NodeXname) UnmarshalJSON(data []byte) error { + xname.Value = string(data) + // Remove quotation marks if they exist + if len(xname.Value) >= 2 && xname.Value[0] == '"' && xname.Value[len(xname.Value)-1] == '"' { + xname.Value = xname.Value[1 : len(xname.Value)-1] + } + return nil +} + +func (xname NodeXname) Valid() (bool, error) { + nodeXnameRegex := regexp.MustCompile(`^x(?P\d{3,5})c(?P\d{1,3})s(?P\d{1,3})b(?P\d{1,3})n(?P\d{1,3})$`) + if !nodeXnameRegex.MatchString(xname.Value) { + return false, fmt.Errorf("XName does not match regex") + } + + // Extract the named groups + match := nodeXnameRegex.FindStringSubmatch(xname.Value) + result := make(map[string]string) + for i, name := range nodeXnameRegex.SubexpNames() { + if i > 0 && i <= len(match) { + result[name] = match[i] + } + } + + // Convert and check chassis number + chassis, err := strconv.Atoi(result["chassis"]) + if err != nil { + return false, fmt.Errorf("chassis is not a valid number: %s", result["chassis"]) + } + if chassis >= 256 { + return false, fmt.Errorf("chassis number %d exceeds the maximum allowed value of 255", chassis) + } + + return true, nil +} + +// XnameSliceString converts a slice of NodeCollectionType to a slice of strings. +func XnameSliceString(slice []NodeXname) []string { + strSlice := make([]string, len(slice)) + for i, v := range slice { + strSlice[i] = v.String() + } + return strSlice +} + +func NewNodeXname(xname string) NodeXname { + return NodeXname{Value: xname} +} + +type BMCXname struct { + Value string +} + +func NewBMCXname(xname string) BMCXname { + return BMCXname{Value: xname} +} + +func (b BMCXname) String() string { + return b.Value +} + +func (b BMCXname) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Title: "BMCXName", + Description: "XName for a BMC", + Pattern: `^x(\d{3,5})c(\d{1,3})s(\d{1,3})b(\d{1,3})$`, + } +} + +func (b BMCXname) MarshalJSON() ([]byte, error) { + return json.Marshal(b.Value) +} + +func (b *BMCXname) UnmarshalJSON(data []byte) error { + b.Value = string(data) + // Remove quotation marks if they exist + if len(b.Value) >= 2 && b.Value[0] == '"' && b.Value[len(b.Value)-1] == '"' { + b.Value = b.Value[1 : len(b.Value)-1] + } + return nil +} + +func (b BMCXname) Valid() (bool, error) { + bmcXnameRegex := regexp.MustCompile(`^x(?P\d{3,5})c(?P\d{1,3})s(?P\d{1,3})b(?P\d{1,3})$`) + if !bmcXnameRegex.MatchString(b.Value) { + return false, fmt.Errorf("XName does not match regex") + } + + // Extract the named groups + match := bmcXnameRegex.FindStringSubmatch(b.Value) + result := make(map[string]string) + for i, name := range bmcXnameRegex.SubexpNames() { + if i > 0 && i <= len(match) { + result[name] = match[i] + } + } + + // Convert and check chassis number + chassis, err := strconv.Atoi(result["chassis"]) + if err != nil { + return false, fmt.Errorf("chassis is not a valid number: %s", result["chassis"]) + } + if chassis >= 256 { + return false, fmt.Errorf("chassis number %d exceeds the maximum allowed value of 255", chassis) + } + return true, nil +} + +func IsValidBMCXName(xname string) bool { + // Compile the regular expression. This is the pattern from your requirement. + re := regexp.MustCompile(`^x(?P\d{3,5})c(?P\d{1,3})s(?P\d{1,3})b(?P\d{1,3})$`) + + // Use FindStringSubmatch to capture the parts of the xname. + matches := re.FindStringSubmatch(xname) + if matches == nil { + return false + } + + // Since the cabinet can go up to 100,000 and others up to 255, we need to check these values. + // The order of subexpressions in matches corresponds to the groups in the regex. + cabinet, _ := strconv.Atoi(matches[1]) + chassis, _ := strconv.Atoi(matches[2]) + slot, _ := strconv.Atoi(matches[3]) + bmc, _ := strconv.Atoi(matches[4]) + + if cabinet > 100000 || chassis >= 256 || slot >= 256 || bmc >= 256 { + return false + } + + return true +} diff --git a/pkg/schemas/envelope.go b/pkg/schemas/envelope.go new file mode 100644 index 00000000..d30346fa --- /dev/null +++ b/pkg/schemas/envelope.go @@ -0,0 +1,8 @@ +package schemas + +// Envelope structure for schema versioning +type Envelope struct { + SchemaID string `json:"schema_id"` + Version string `json:"version"` + Payload interface{} `json:"payload"` +} diff --git a/pkg/schemas/inventory.go b/pkg/schemas/inventory.go new file mode 100644 index 00000000..00be8db6 --- /dev/null +++ b/pkg/schemas/inventory.go @@ -0,0 +1,88 @@ +package schemas + +type EthernetInterface struct { + URI string `json:"uri,omitempty"` // URI of the interface + MAC string `json:"mac,omitempty"` // MAC address of the interface + IP string `json:"ip,omitempty"` // IP address of the interface + Name string `json:"name,omitempty"` // Name of the interface + Description string `json:"description,omitempty"` // Description of the interface + Enabled bool `json:"enabled,omitempty"` // Whether interface is enabled +} + +type NetworkAdapter struct { + URI string `json:"uri,omitempty"` // URI of the adapter + Manufacturer string `json:"manufacturer,omitempty"` // Manufacturer of the adapter + Name string `json:"name,omitempty"` // Name of the adapter + Model string `json:"model,omitempty"` // Model of the adapter + Serial string `json:"serial,omitempty"` // Serial number of the adapter + Description string `json:"description,omitempty"` // Description of the adapter +} + +type NetworkInterface struct { + URI string `json:"uri,omitempty"` // URI of the interface + Name string `json:"name,omitempty"` // Name of the interface + Description string `json:"description,omitempty"` // Description of the interface + Adapter NetworkAdapter `json:"adapter,omitempty"` // Adapter of the interface +} + +type ResourceID struct { + OdataID string `json:"@odata.id"` +} + +type ActionReset struct { + AllowableValues []string `json:"ResetType@Redfish.AllowableValues"` + RFActionInfo string `json:"@Redfish.ActionInfo"` + Target string `json:"target"` + Title string `json:"title,omitempty"` +} + +type ComputerSystemActions struct { + ComputerSystemReset ActionReset `json:"#ComputerSystem.Reset"` +} + +type PowerControl struct { + ResourceID + MemberId string `json:"MemberId,omitempty"` + Name string `json:"Name,omitempty"` + PowerCapacityWatts int `json:"PowerCapacityWatts,omitempty"` + RelatedItem []*ResourceID `json:"RelatedItem,omitempty"` +} + +type Links struct { + Chassis []string `json:"chassis,omitempty"` + Managers []string `json:"managers,omitempty"` +} + +type Power struct { + State string `json:"state,omitempty"` + PowerControlIDS []string `json:"power_control_ids,omitempty"` +} + +type InventoryDetail struct { + URI string `json:"uri,omitempty"` // URI of the BMC + UUID string `json:"uuid,omitempty"` // UUID of Node + Manufacturer string `json:"manufacturer,omitempty"` // Manufacturer of the Node + SystemType string `json:"system_type,omitempty"` // System type of the Node + Name string `json:"name,omitempty"` // Name of the Node + Model string `json:"model,omitempty"` // Model of the Node + Serial string `json:"serial,omitempty"` // Serial number of the Node + BiosVersion string `json:"bios_version,omitempty"` // Version of the BIOS + EthernetInterfaces []EthernetInterface `json:"ethernet_interfaces,omitempty"` // Ethernet interfaces of the Node + NetworkInterfaces []NetworkInterface `json:"network_interfaces,omitempty"` // Network interfaces of the Node + Power *Power `json:"power,omitempty"` // Power state of the Node + ProcessorCount int `json:"processor_count,omitempty"` // Processors of the Node + ProcessorType string `json:"processor_type,omitempty"` // Processor type of the Node + MemoryTotal float32 `json:"memory_total,omitempty"` // Total memory of the Node in Gigabytes + TrustedModules []string `json:"trusted_modules,omitempty"` // Trusted modules of the Node + TrustedComponents []string `json:"trusted_components,omitempty"` // Trusted components of the Chassis + Chassis_SKU string `json:"chassis_sku,omitempty"` // SKU of the Chassis + Chassis_Serial string `json:"chassis_serial,omitempty"` // Serial number of the Chassis + Chassis_AssetTag string `json:"chassis_asset_tag,omitempty"` // Asset tag of the Chassis + Chassis_Manufacturer string `json:"chassis_manufacturer,omitempty"` // Manufacturer of the Chassis + Chassis_Model string `json:"chassis_model,omitempty"` // Model of the Chassis + OdataId string `json:"@odata.id,omitempty"` // OData ID for the computer system + PowerURL string `json:"PowerURL,omitempty"` // URL for power control + PowerControl []*PowerControl `json:"PowerControl,omitempty"` // Power control actions data + Actions []string `json:"actions,omitempty"` // Actions for the hardware + Links *Links `json:"links,omitempty"` // Links to related resources +} diff --git a/pkg/schemas/inventory_test.go b/pkg/schemas/inventory_test.go new file mode 100644 index 00000000..bf4dae2f --- /dev/null +++ b/pkg/schemas/inventory_test.go @@ -0,0 +1,73 @@ +package schemas + +import ( + "encoding/json" + "testing" + + "os" + "path/filepath" + + "log" + + "github.com/invopop/jsonschema" + "github.com/openchami/schemas/schemas/csm" +) + +type InventoryRequest struct { + Header Envelope `json:"header"` + InventoryDetailArray []InventoryDetail `json:"inventory_detail_array"` +} + +func generateAndWriteSchemas(path string) { + schemas := map[string]interface{}{ + + "Component.json": &csm.Component{}, + "RedfishEndpoint.json": &csm.RedfishEndpoint{}, + "InventoryDetailRequest.json": &InventoryRequest{}, + } + + if err := os.MkdirAll(path, 0755); err != nil { + log.Fatal("Failed to create schema directory") + } + + for filename, model := range schemas { + schema := jsonschema.Reflect(model) + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + log.Fatal("Failed to generate JSON schema") + } + fullpath := filepath.Join(path, filename) + if err := os.WriteFile(fullpath, data, 0644); err != nil { + log.Fatal("Failed to write JSON schema to file") + } + log.Println("Schema written") + } +} + +func TestGenerateAndWriteSchemas(t *testing.T) { + var ( + path = "jsonschemas" + schemas = map[string]interface{}{ + "Component.json": &csm.Component{}, + "RedfishEndpoint.json": &csm.RedfishEndpoint{}, + "InventoryDetailRequest.json": &InventoryRequest{}, + } + ) + + if err := os.MkdirAll(path, 0755); err != nil { + t.Fatal("Failed to create schema directory") + } + + for filename, model := range schemas { + schema := jsonschema.Reflect(model) + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + t.Fatal("Failed to generate JSON schema") + } + fullpath := filepath.Join(path, filename) + if err := os.WriteFile(fullpath, data, 0644); err != nil { + t.Fatal("Failed to write JSON schema to file") + } + log.Println("Schema written") + } +}