Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 67 additions & 44 deletions apis/metal3.io/v1alpha1/firmwareschema_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1alpha1

import (
"fmt"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
Expand Down Expand Up @@ -54,85 +56,106 @@ type SettingSchema struct {
Unique *bool `json:"unique,omitempty"`
}

// FirmwareSchemaSpec defines the desired state of FirmwareSchema
type FirmwareSchemaSpec struct {

// The hardware vendor associated with this schema
// +optional
HardwareVendor string `json:"hardwareVendor,omitempty"`

// The hardware model associated with this schema
// +optional
HardwareModel string `json:"hardwareModel,omitempty"`

// Map of firmware name to schema
Schema map[string]SettingSchema `json:"schema" required:"true"`
type SchemaSettingError struct {
name string
message string
}

//+kubebuilder:object:root=true

// FirmwareSchema is the Schema for the firmwareschemas API
type FirmwareSchema struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec FirmwareSchemaSpec `json:"spec,omitempty"`
func (e SchemaSettingError) Error() string {
return fmt.Sprintf("Setting %s is invalid, %s", e.name, e.message)
}

// Check whether the setting's name and value is valid using the schema
func (host *FirmwareSchema) CheckSettingIsValid(name string, value intstr.IntOrString, schemas map[string]SettingSchema) bool {

schema, ok := schemas[name]
if !ok {
// The setting must exist in the status
return false
}
func (schema *SettingSchema) Validate(name string, value intstr.IntOrString) error {

if schema.ReadOnly != nil && *schema.ReadOnly == true {
Comment thread
bfournie marked this conversation as resolved.
return false
return SchemaSettingError{name: name, message: "it is ReadOnly"}
}

// Check if valid based on type
switch schema.AttributeType {
case "Enumeration":
for _, av := range schema.AllowableValues {
if value.String() == av {
return true
return nil
}
}
return false
return SchemaSettingError{name: name, message: fmt.Sprintf("unknown enumeration value - %s", value.String())}

case "Integer":
if schema.LowerBound == nil || schema.UpperBound == nil {
// return true if no settings to check validity
return true
if schema.LowerBound != nil && value.IntValue() < *schema.LowerBound {
return SchemaSettingError{name: name, message: fmt.Sprintf("integer %s is below minimum value %d", value.String(), *schema.LowerBound)}
}
if schema.UpperBound != nil && value.IntValue() > *schema.UpperBound {
return SchemaSettingError{name: name, message: fmt.Sprintf("integer %s is above maximum value %d", value.String(), *schema.UpperBound)}
}
return (value.IntValue() >= *schema.LowerBound && value.IntValue() <= *schema.UpperBound)
return nil

case "String":
if schema.MinLength == nil || schema.MaxLength == nil {
// return true if no settings to check validity
return true
strLen := len(value.String())
if schema.MinLength != nil && strLen < *schema.MinLength {
return SchemaSettingError{name: name, message: fmt.Sprintf("string %s length is below minimum length %d", value.String(), *schema.MinLength)}
}
return (len(value.String()) >= *schema.MinLength && len(value.String()) <= *schema.MaxLength)
if schema.MaxLength != nil && strLen > *schema.MaxLength {
return SchemaSettingError{name: name, message: fmt.Sprintf("string %s length is above maximum length %d", value.String(), *schema.MaxLength)}
}
return nil

case "Boolean":
return (value.String() == "true" || value.String() == "false")
if value.String() == "true" || value.String() == "false" {
return nil
}
return SchemaSettingError{name: name, message: fmt.Sprintf("%s is not a boolean", value.String())}

case "Password":
// Prevent sets of password types
return false
return SchemaSettingError{name: name, message: "passwords are immutable"}

case "":
// allow the set as BIOS registry fields may not have been available
return true
return nil

default:
// Unexpected attribute type
return false
return SchemaSettingError{name: name, message: fmt.Sprintf("unexpected attribute type %s", schema.AttributeType)}
}
}

// FirmwareSchemaSpec defines the desired state of FirmwareSchema
type FirmwareSchemaSpec struct {

// The hardware vendor associated with this schema
// +optional
HardwareVendor string `json:"hardwareVendor,omitempty"`

// The hardware model associated with this schema
// +optional
HardwareModel string `json:"hardwareModel,omitempty"`

// Map of firmware name to schema
Schema map[string]SettingSchema `json:"schema" required:"true"`
}

//+kubebuilder:object:root=true

// FirmwareSchema is the Schema for the firmwareschemas API
type FirmwareSchema struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec FirmwareSchemaSpec `json:"spec,omitempty"`
}

// Check whether the setting's name and value is valid using the schema
func (host *FirmwareSchema) ValidateSetting(name string, value intstr.IntOrString, schemas map[string]SettingSchema) error {

schema, ok := schemas[name]
if !ok {
return SchemaSettingError{name: name, message: "it is not in the associated schema"}
}

return schema.Validate(name, value)
}

//+kubebuilder:object:root=true

// FirmwareSchemaList contains a list of FirmwareSchema
Expand Down
19 changes: 19 additions & 0 deletions apis/metal3.io/v1alpha1/hostfirmwaresettings_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ type SchemaReference struct {
Name string `json:"name"`
}

type SettingsConditionType string

const (
// Indicates that the settings in the Spec are different than Status
UpdateRequested SettingsConditionType = "UpdateRequested"
Comment thread
bfournie marked this conversation as resolved.

// Indicates if the settings are valid and can be configured on the host
SettingsValid SettingsConditionType = "Valid"
)

// HostFirmwareSettingsSpec defines the desired state of HostFirmwareSettings
type HostFirmwareSettingsSpec struct {

Expand All @@ -53,9 +63,18 @@ type HostFirmwareSettingsStatus struct {

// Settings are the actual firmware settings stored as name/value pairs
Settings SettingsMap `json:"settings" required:"true"`

// Track whether settings stored in the spec are valid based on the schema
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
}

//+kubebuilder:object:root=true
//+kubebuilder:resource:shortName=hfs
//+kubebuilder:subresource:status

// HostFirmwareSettings is the Schema for the hostfirmwaresettings API
Expand Down
40 changes: 22 additions & 18 deletions apis/metal3.io/v1alpha1/hostfirmwaresettings_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
)

func TestCheckSettingIsValid(t *testing.T) {
func TestValidateSetting(t *testing.T) {

lower_bound := 1
upper_bound := 20
Expand Down Expand Up @@ -42,96 +42,100 @@ func TestCheckSettingIsValid(t *testing.T) {
Scenario string
Name string
Value intstr.IntOrString
Expected bool
Expected string
}{
{
Scenario: "StringTypePass",
Name: "AssetTag",
Value: intstr.FromString("NewServer"),
Expected: true,
Expected: "",
},
{
Scenario: "StringTypeFailUpper",
Name: "AssetTag",
Value: intstr.FromString("NewServerPutInServiceIn2021"),
Expected: false,
Expected: "Setting AssetTag is invalid, string NewServerPutInServiceIn2021 length is above maximum length 16",
},
{
Scenario: "StringTypeFailLower",
Name: "AssetTag",
Value: intstr.FromString(""),
Expected: false,
Expected: "Setting AssetTag is invalid, string length is below minimum length 1",
},
{
Scenario: "EnumerationTypePass",
Name: "ProcVirtualization",
Value: intstr.FromString("Disabled"),
Expected: true,
Expected: "",
},
{
Scenario: "EnumerationTypeFail",
Name: "ProcVirtualization",
Value: intstr.FromString("Foo"),
Expected: false,
Expected: "Setting ProcVirtualization is invalid, unknown enumeration value - Foo",
},
{
Scenario: "IntegerTypePassAsString",
Name: "NetworkBootRetryCount",
Value: intstr.FromString("10"),
Expected: true,
Expected: "",
},
{
Scenario: "IntegerTypePassAsInt",
Name: "NetworkBootRetryCount",
Value: intstr.FromInt(10),
Expected: true,
Expected: "",
},
{
Scenario: "IntegerTypeFailUpper",
Name: "NetworkBootRetryCount",
Value: intstr.FromString("42"),
Expected: false,
Expected: "Setting NetworkBootRetryCount is invalid, integer 42 is above maximum value 20",
},
{
Scenario: "IntegerTypeFailLower",
Name: "NetworkBootRetryCount",
Value: intstr.FromInt(0),
Expected: false,
Expected: "Setting NetworkBootRetryCount is invalid, integer 0 is below minimum value 1",
},
{
Scenario: "BooleanTypePass",
Name: "QuietBoot",
Value: intstr.FromString("true"),
Expected: true,
Expected: "",
},
{
Scenario: "BooleanTypeFail",
Name: "QuietBoot",
Value: intstr.FromString("Enabled"),
Expected: false,
Expected: "Setting QuietBoot is invalid, Enabled is not a boolean",
},
{
Scenario: "ReadOnlyTypeFail",
Name: "SerialNumber",
Value: intstr.FromString("42"),
Expected: false,
Expected: "Setting SerialNumber is invalid, it is ReadOnly",
},
{
Scenario: "MissingEnumerationField",
Name: "SriovEnable",
Value: intstr.FromString("Disabled"),
Expected: true,
Expected: "",
},
{
Scenario: "UnknownSettingFail",
Name: "IceCream",
Value: intstr.FromString("Vanilla"),
Expected: false,
Expected: "Setting IceCream is invalid, it is not in the associated schema",
},
} {
t.Run(tc.Scenario, func(t *testing.T) {
actual := fwSchema.CheckSettingIsValid(tc.Name, tc.Value, fwSchema.Spec.Schema)
assert.Equal(t, tc.Expected, actual)
err := fwSchema.ValidateSetting(tc.Name, tc.Value, fwSchema.Spec.Schema)
if err == nil {
assert.Equal(t, tc.Expected, "")
} else {
assert.Equal(t, tc.Expected, err.Error())
}
})
}
}
23 changes: 23 additions & 0 deletions apis/metal3.io/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading