Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect GUID Encoding based on reasonable timestamp #59

Merged
merged 9 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
94 changes: 42 additions & 52 deletions internal/uuid-endianness/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"bytes"
"encoding/binary"
"fmt"
guuid "github.com/google/uuid"
"time"

"github.com/google/uuid"
)

// UuidSize is the size in bytes of a UUID object
const UuidSize = 16
// uuidSize is the size in bytes of a UUID object
const uuidSize = 16

// Uuid represents a UUID object as defined by RFC 4122.
type Uuid struct {
// uid represents a UUID object as defined by RFC 4122.
type uid struct {
TimeLow uint32
TimeMid uint16
TimeHiAndVersion uint16
Expand All @@ -20,65 +22,53 @@ type Uuid struct {
Node [6]byte
}

// String implements the fmt.Stringer interface
func (u Uuid) String() string {
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%x", u.TimeLow, u.TimeMid, u.TimeHiAndVersion, u.ClockSeqHiAndRes, u.ClockSeqLow, u.Node)
}

func FromString(s string) (Uuid, error) {
var uid Uuid
u, err := guuid.Parse(s)
// Redfish API and probably the DHCP Request return the GUID of a machine encoded either
// in middleEndianBytes
// or as normal string
//
// To detect which one is actually in use, we extract the creation time of the uuid,
// if this time is somehow reasonable, we return the string of the GUID as we got it,
// we need to convert it to mixed endian
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
func Convert(s string) (string, error) {
u, err := uuid.Parse(s)
if err != nil {
return uid, err
return "", err
}
if isNotEncoded(u) {
return u.String(), nil
}

b, err := u.MarshalBinary()
if err != nil {
return uid, err
return "", err
}
return FromBytes(b)
}

// ToMiddleEndian encodes the UUID into a middle-endian UUID.
//
// A middle-endian encoded UUID represents a UUID where the first
// three groups are Little Endian encoded, while the rest of the
// groups are Big Endian encoded.
func (u Uuid) ToMiddleEndian() (Uuid, error) {
buf, err := u.MiddleEndianBytes()
uid, err := fromBytes(b)
if err != nil {
return Uuid{}, err
return "", err
}

return FromBytes(buf)
}

// BigEndianBytes returns the UUID encoded in Big Endian.
func (u Uuid) BigEndianBytes() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, UuidSize))

err := binary.Write(buf, binary.BigEndian, u)
buf, err := uid.middleEndianBytes()
if err != nil {
return nil, err
return "", err
}

return buf.Bytes(), nil
uid, err = fromBytes(buf)
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%x", uid.TimeLow, uid.TimeMid, uid.TimeHiAndVersion, uid.ClockSeqHiAndRes, uid.ClockSeqLow, uid.Node), err
}

// LittleEndianBytes returns the UUID encoded in Little Endian.
func (u Uuid) LittleEndianBytes() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, UuidSize))
// thirtyYears is an educated guess for a plausible time stored in the uuid.
// RFC states that the time is stored in 100s of nanos since 15 Okt 1582 as defined in RFC 4122
// We check if this time is not more than 30 years apart from now.
// If the uid returned from the BMC is mixedEndian encoded, the time extracted is usually in the year 4000 or so.
const thirtyYears = 30 * 365 * 24 * time.Hour

err := binary.Write(buf, binary.LittleEndian, u)
if err != nil {
return nil, err
}

return buf.Bytes(), nil
func isNotEncoded(u uuid.UUID) bool {
timeDistance := time.Since(time.Unix(u.Time().UnixTime())).Abs()
return timeDistance < thirtyYears
mwennrich marked this conversation as resolved.
Show resolved Hide resolved
}

// MiddleEndianBytes returns the UUID encoded in Middle Endian.
func (u Uuid) MiddleEndianBytes() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, UuidSize))
// middleEndianBytes returns the UUID encoded in Middle Endian.
func (u uid) middleEndianBytes() ([]byte, error) {
buf := bytes.NewBuffer(make([]byte, 0, uuidSize))

if err := binary.Write(buf, binary.LittleEndian, u.TimeLow); err != nil {
return nil, err
Expand Down Expand Up @@ -108,8 +98,8 @@ func (u Uuid) MiddleEndianBytes() ([]byte, error) {
}

// UuidFromBytes reads a UUID from a given byte slice.
func FromBytes(buf []byte) (Uuid, error) {
var u Uuid
func fromBytes(buf []byte) (uid, error) {
var u uid
reader := bytes.NewReader(buf)

err := binary.Read(reader, binary.BigEndian, &u)
Expand Down
93 changes: 19 additions & 74 deletions internal/uuid-endianness/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package uuid

import (
"bytes"
"reflect"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func ConvertUUID(s string) []byte {
func convertUUID(s string) []byte {
u := uuid.MustParse(s)
b, _ := u.MarshalBinary()
return b
Expand All @@ -28,19 +29,19 @@ func TestUuid(t *testing.T) {
expect: []byte{0x68, 0x5, 0x34, 0x99, 0x75, 0xf7, 0xe7, 0x11, 0x8c, 0x3f, 0x9a, 0x21, 0x4c, 0xf0, 0x93, 0xae}, // 68053499-75f7-e711-8c3f-9a214cf093ae
},
{
value: ConvertUUID("00ecd471-0771-e911-8000-efbeaddeefbe"),
expect: ConvertUUID("71d4ec00-7107-11e9-8000-efbeaddeefbe"),
value: convertUUID("00ecd471-0771-e911-8000-efbeaddeefbe"),
expect: convertUUID("71d4ec00-7107-11e9-8000-efbeaddeefbe"),
},
}

for i := range tests {
tt := tests[i]
value, err := FromBytes(tt.value)
value, err := fromBytes(tt.value)
if err != nil {
t.Fatalf("Cannot parse UUID %+v: %s", tt.value, err)
}

got, err := value.MiddleEndianBytes()
got, err := value.middleEndianBytes()
if err != nil {
t.Fatalf("Cannot convert to middle endian %+v: %s", value, err)
}
Expand All @@ -51,73 +52,17 @@ func TestUuid(t *testing.T) {
}
}

func TestFromString(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "creates uuid",
input: "00ecd471-0771-e911-8000-efbeaddeefbe",
wantErr: false,
},
{
name: "creates uuid",
input: "11ecd471-2771-e911-8333-efbeaddeefbe",
wantErr: false,
},
{
name: "creates uuid",
input: "11ecd471-2771-e911-8333-0000addeefbe",
wantErr: false,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
got, err := FromString(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("FromString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.String(), tt.input) {
t.Errorf("FromString() = %v, want %v", got.String(), tt.input)
}
})
}
}
func TestUUIDConvert(t *testing.T) {
bad := "0060c2bd-3089-eb11-8000-7cc255106b08"
good := "bdc26000-8930-11eb-8000-7cc255106b08"

func TestUuid_ToMiddleEndian(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "convert to mixed endian",
input: "00ecd471-0771-e911-8000-efbeaddeefbe",
want: "71d4ec00-7107-11e9-8000-efbeaddeefbe",
wantErr: false,
},
}
for i := range tests {
tt := tests[i]
t.Run(tt.name, func(t *testing.T) {
u, err := FromString(tt.input)
if err != nil {
t.Errorf("Uuid.FormatString() error = %v", err)
return
}
got, err := u.ToMiddleEndian()
if (err != nil) != tt.wantErr {
t.Errorf("Uuid.ToMiddleEndian() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got.String(), tt.want) {
t.Errorf("Uuid.ToMiddleEndian() = %v, want %v", got.String(), tt.want)
}
})
}
result, err := Convert(good)
require.NoError(t, err)

assert.Equal(t, good, result)

result, err = Convert(bad)
require.NoError(t, err)

assert.Equal(t, good, result)
}
13 changes: 3 additions & 10 deletions internal/vendors/supermicro/supermicro.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,20 +193,13 @@ func (ob *outBand) UUID() (*uuid.UUID, error) {
return &us, nil
}

// Redfish returns the UUID in the wrong byte order
// we need to convert it to mixed endian
// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding
raw, err := uuidendian.FromString(u)
// Redfish returns the UUID sometimes in mixed encoding
uid, err := uuidendian.Convert(u)
if err != nil {
return nil, err
}

mixed, err := raw.ToMiddleEndian()
if err != nil {
return nil, err
}

us, err := uuid.Parse(mixed.String())
us, err := uuid.Parse(uid)
if err != nil {
return nil, err
}
Expand Down
41 changes: 0 additions & 41 deletions internal/vendors/supermicro/supermicro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,9 @@ package supermicro
import (
"testing"

"github.com/google/uuid"
"github.com/metal-stack/go-hal"
uuidendian "github.com/metal-stack/go-hal/internal/uuid-endianness"
"github.com/stretchr/testify/require"
)

func Test_UUID(t *testing.T) {
// given
u := "f6157800-70e3-11e9-8000-efbeaddeefbe"

uid, err := uuidendian.FromString(u)

// then
require.NoError(t, err)
require.NotNil(t, uid)
require.Equal(t, "f6157800-70e3-11e9-8000-efbeaddeefbe", uid.String())

// when
id, err := UUID(u)

// then
require.NoError(t, err)
require.NotNil(t, id)
require.Equal(t, "007815f6-e370-e911-8000-efbeaddeefbe", id.String())
}

func UUID(u string) (*uuid.UUID, error) {
raw, err := uuidendian.FromString(u)
if err != nil {
return nil, err
}

mixed, err := raw.ToMiddleEndian()
if err != nil {
return nil, err
}

us, err := uuid.Parse(mixed.String())
if err != nil {
return nil, err
}
return &us, nil
}

func Test_inBand_PowerOff(t *testing.T) {
type fields struct {
sum *sum
Expand Down
Loading