Skip to content

Commit 6de5afb

Browse files
committed
allow empty systemUuid, generate pseudoUuid from serial
1 parent f90c9e2 commit 6de5afb

7 files changed

Lines changed: 155 additions & 18 deletions

File tree

api/v1alpha1/server_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ type ServerSpec struct {
8383
UUID string `json:"uuid,omitempty"`
8484

8585
// SystemUUID is the unique identifier for the server.
86-
// +required
86+
// If not provided, it will be derived from the serial
87+
// +optional
8788
SystemUUID string `json:"systemUUID"`
8889

8990
// SystemURI is the unique URI for the server resource in REDFISH API.

bmc/redfish.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,9 @@ func (r *RedfishBMC) getSystemFromUri(ctx context.Context, systemURI string) (*r
851851
}); err != nil {
852852
return nil, fmt.Errorf("failed to wait for for server systems to be ready: %w", err)
853853
}
854-
if system.UUID != "" {
854+
// System is considered ready even if UUID is empty - allow graceful handling of systems
855+
// that don't expose System.UUID in their Redfish payload
856+
if system != nil {
855857
return system, nil
856858
}
857859
return nil, fmt.Errorf("no system found for %v", systemURI)

config/crd/bases/metal.ironcore.dev_servers.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,15 +299,15 @@ spec:
299299
REDFISH API.
300300
type: string
301301
systemUUID:
302-
description: SystemUUID is the unique identifier for the server.
302+
description: |-
303+
SystemUUID is the unique identifier for the server.
304+
If not provided, it will be derived from the serial
303305
type: string
304306
uuid:
305307
description: |-
306308
UUID is the unique identifier for the server.
307309
Deprecated in favor of systemUUID.
308310
type: string
309-
required:
310-
- systemUUID
311311
type: object
312312
status:
313313
description: ServerStatus defines the observed state of Server.

dist/chart/templates/crd/metal.ironcore.dev_servers.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,15 +305,15 @@ spec:
305305
REDFISH API.
306306
type: string
307307
systemUUID:
308-
description: SystemUUID is the unique identifier for the server.
308+
description: |-
309+
SystemUUID is the unique identifier for the server.
310+
If not provided, it will be derived from the serial
309311
type: string
310312
uuid:
311313
description: |-
312314
UUID is the unique identifier for the server.
313315
Deprecated in favor of systemUUID.
314316
type: string
315-
required:
316-
- systemUUID
317317
type: object
318318
status:
319319
description: ServerStatus defines the observed state of Server.

docs/api-reference/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,7 @@ _Appears in:_
15561556
| Field | Description | Default | Validation |
15571557
| --- | --- | --- | --- |
15581558
| `uuid` _string_ | UUID is the unique identifier for the server.<br />Deprecated in favor of systemUUID. | | |
1559-
| `systemUUID` _string_ | SystemUUID is the unique identifier for the server. | | |
1559+
| `systemUUID` _string_ | SystemUUID is the unique identifier for the server.<br />If not provided, it will be derived from the serial | | |
15601560
| `systemURI` _string_ | SystemURI is the unique URI for the server resource in REDFISH API. | | |
15611561
| `power` _[Power](#power)_ | Power specifies the desired power state of the server. | | |
15621562
| `indicatorLED` _[IndicatorLED](#indicatorled)_ | IndicatorLED specifies the desired state of the server's indicator LED. | | |

internal/controller/server_controller.go

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package controller
55

66
import (
77
"context"
8+
"crypto/md5"
89
"crypto/rand"
910
"crypto/rsa"
1011
"encoding/json"
@@ -812,6 +813,7 @@ func (r *ServerReconciler) extractServerDetailsFromRegistry(ctx context.Context,
812813
}
813814

814815
serverBase := server.DeepCopy()
816+
815817
// update network interfaces
816818
nics := make([]metalv1alpha1.NetworkInterface, 0, len(serverDetails.NetworkInterfaces))
817819
for _, s := range serverDetails.NetworkInterfaces {
@@ -888,6 +890,23 @@ func (r *ServerReconciler) patchServerState(ctx context.Context, server *metalv1
888890
return true, nil
889891
}
890892

893+
// generatePseudoUUID generates a deterministic UUID from an input string.
894+
// Format: 99999999-xxxx-3xxx-8xxx-xxxxxxxxxxxx (prefix identifies generated UUIDs)
895+
func generatePseudoUUID(input string) string {
896+
hash := md5.Sum([]byte(input))
897+
hashHex := fmt.Sprintf("%x", hash[:])
898+
899+
// Build UUID with version (3) and variant (8) bits embedded in groups
900+
// Format: 99999999 - 4 - 3+3 - 8+3 - 12
901+
uuid := fmt.Sprintf("99999999-%s-3%s-8%s-%s",
902+
hashHex[0:4], // 4 hex chars
903+
hashHex[4:7], // 3 hex chars (becomes 3XXX)
904+
hashHex[7:10], // 3 hex chars (becomes 8XXX)
905+
hashHex[10:22], // 12 hex chars
906+
)
907+
return uuid
908+
}
909+
891910
func (r *ServerReconciler) patchServerURI(ctx context.Context, log logr.Logger, bmcClient bmc.BMC, server *metalv1alpha1.Server) (bool, error) {
892911
if len(server.Spec.SystemURI) != 0 {
893912
return false, nil
@@ -899,21 +918,55 @@ func (r *ServerReconciler) patchServerURI(ctx context.Context, log logr.Logger,
899918
return false, err
900919
}
901920

902-
for _, system := range systems {
903-
if strings.EqualFold(system.UUID, server.Spec.SystemUUID) {
904-
serverBase := server.DeepCopy()
905-
server.Spec.SystemURI = system.URI
906-
if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil {
907-
return false, fmt.Errorf("failed to patch server URI: %w", err)
921+
// Try to find system by UUID if one is provided
922+
if len(server.Spec.SystemUUID) > 0 {
923+
for _, system := range systems {
924+
if strings.EqualFold(system.UUID, server.Spec.SystemUUID) {
925+
serverBase := server.DeepCopy()
926+
server.Spec.SystemURI = system.URI
927+
if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil {
928+
return false, fmt.Errorf("failed to patch server URI: %w", err)
929+
}
930+
return true, nil
908931
}
909932
}
910933
}
911-
if len(server.Spec.SystemURI) == 0 {
912-
log.V(1).Info("Patching systemURI failed", "no system found for UUID", server.Spec.SystemUUID)
934+
935+
// If no system found by UUID or UUID is empty, and we only have one system, use it
936+
// This handles cases where the Redfish implementation doesn't provide System.UUID
937+
if len(systems) == 1 {
938+
system := systems[0]
939+
serverBase := server.DeepCopy()
940+
server.Spec.SystemURI = system.URI
941+
942+
// If SystemUUID is empty, use system UUID if available, otherwise generate from SerialNumber
943+
if len(server.Spec.SystemUUID) == 0 {
944+
if len(system.UUID) > 0 {
945+
server.Spec.SystemUUID = system.UUID
946+
} else if len(system.SerialNumber) > 0 {
947+
server.Spec.SystemUUID = generatePseudoUUID(system.SerialNumber)
948+
log.V(1).Info("Generated pseudo-UUID from system serial number",
949+
"serialNumber", system.SerialNumber, "pseudoUUID", server.Spec.SystemUUID)
950+
} else {
951+
return false, fmt.Errorf("system does not provide UUID or SerialNumber; cannot generate a unique identifier")
952+
}
953+
}
954+
955+
if err := r.Patch(ctx, server, client.MergeFrom(serverBase)); err != nil {
956+
return false, fmt.Errorf("failed to patch server URI: %w", err)
957+
}
958+
return true, nil
959+
}
960+
961+
// Multiple systems available but couldn't match by UUID
962+
if len(server.Spec.SystemUUID) > 0 {
963+
log.V(1).Info("No system found for UUID, and multiple systems available", "requestedUUID", server.Spec.SystemUUID)
913964
return false, fmt.Errorf("unable to find system URI for UUID: %v", server.Spec.SystemUUID)
914965
}
915966

916-
return true, nil
967+
// No SystemUUID provided and multiple systems available - cannot determine which to use
968+
log.V(1).Info("No SystemUUID provided and multiple systems available, cannot determine target system")
969+
return false, fmt.Errorf("SystemUUID must be provided when multiple systems are available")
917970
}
918971

919972
func (r *ServerReconciler) ensureServerPowerState(ctx context.Context, log logr.Logger, bmcClient bmc.BMC, server *metalv1alpha1.Server) error {

internal/controller/server_controller_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,3 +813,84 @@ func deleteRegistrySystemIfExists(systemUUID string) {
813813
defer resp.Body.Close() //nolint:errcheck
814814
}
815815
}
816+
817+
var _ = Describe("generatePseudoUUID", func() {
818+
It("Should generate UUIDs matching RFC 4122-like format (8-4-4-4-12)", func() {
819+
testCases := []struct {
820+
name string
821+
serial string
822+
}{
823+
{"short_numeric", "123"},
824+
{"short_alphanumeric", "ABC"},
825+
{"medium_serial", "CZ2D1Y0BB3"},
826+
{"long_serial", "LENOVO-SR850P-00012345-ABCDEF"},
827+
{"uuid_like_serial", "550e8400-e29b-41d4-a716-446655440000"},
828+
{"special_chars", "SN-2024_001+TEST"},
829+
{"long_complex", "HPE-DL360-Gen10-Plus-SN123456789ABCDEFGHIJKLMNOP"},
830+
{"numeric_heavy", "999999999999999999999999"},
831+
{"mixed_case", "LenovoThinkSystem-SR850P-SN001"},
832+
{"minimal", "X"},
833+
}
834+
835+
for _, tc := range testCases {
836+
uuid := generatePseudoUUID(tc.serial)
837+
838+
// Verify 8-4-4-4-12 hex format
839+
Expect(uuid).To(MatchRegexp(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
840+
fmt.Sprintf("Invalid UUID format for %s: %s", tc.name, uuid))
841+
842+
// Verify 99999999 prefix
843+
Expect(uuid).To(HavePrefix("99999999-"),
844+
fmt.Sprintf("Missing 99999999 prefix for %s: %s", tc.name, uuid))
845+
846+
// Verify version 3 bit (3rd group)
847+
Expect(uuid).To(MatchRegexp(`-3[0-9a-f]{3}-`),
848+
fmt.Sprintf("Version 3 bit not set for %s: %s", tc.name, uuid))
849+
850+
// Verify variant 8 bit (4th group starts with 8, 9, a, or b)
851+
Expect(uuid).To(MatchRegexp(`-[89ab][0-9a-f]{3}-`),
852+
fmt.Sprintf("Variant bit not set for %s: %s", tc.name, uuid))
853+
}
854+
})
855+
856+
It("Should generate deterministic UUIDs", func() {
857+
serial := "TEST-SERIAL-12345"
858+
uuid1 := generatePseudoUUID(serial)
859+
uuid2 := generatePseudoUUID(serial)
860+
861+
Expect(uuid1).To(Equal(uuid2),
862+
"Same serial should always produce same UUID")
863+
})
864+
865+
It("Should generate unique UUIDs for different inputs", func() {
866+
serials := []string{"SN001", "SN002", "SN003", "SN004", "SN005"}
867+
uuids := make(map[string]bool)
868+
869+
for _, serial := range serials {
870+
uuid := generatePseudoUUID(serial)
871+
Expect(uuids[uuid]).To(BeFalse(),
872+
fmt.Sprintf("Duplicate UUID generated for serial %s: %s", serial, uuid))
873+
uuids[uuid] = true
874+
}
875+
876+
Expect(uuids).To(HaveLen(len(serials)))
877+
})
878+
879+
It("Should handle varying lengths and complexity", func() {
880+
testCases := []string{
881+
"X", // minimal
882+
"SHORT", // short
883+
"CZ2D1Y0BB3", // medium
884+
"LENOVO-SR850P-00012345-ABCDEF-GHIJKL-MNOPQR", // long
885+
"!@#$%^&*()-_=+[]{}|;:',.<>?/~`", // special
886+
"混合テスト", // unicode
887+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // very long
888+
}
889+
890+
for _, serial := range testCases {
891+
uuid := generatePseudoUUID(serial)
892+
Expect(uuid).To(MatchRegexp(`^99999999-[0-9a-f]{4}-3[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`),
893+
fmt.Sprintf("Invalid UUID for complex input: %s", uuid))
894+
}
895+
})
896+
})

0 commit comments

Comments
 (0)