From 12686c85b05fe29b6aed26e9e3380b4fba6581a9 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 4 Feb 2026 15:42:26 +0100 Subject: [PATCH 01/22] Initial support basis --- connect/connect.go | 5 + internal/redfish/redfish.go | 53 +++++- internal/vendors/fujitsu/fujitsu.go | 265 ++++++++++++++++++++++++++++ pkg/api/types.go | 5 +- 4 files changed, 321 insertions(+), 7 deletions(-) create mode 100644 internal/vendors/fujitsu/fujitsu.go diff --git a/connect/connect.go b/connect/connect.go index 6ddad53..2c012b6 100644 --- a/connect/connect.go +++ b/connect/connect.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/metal-stack/go-hal/internal/vendors/fujitsu" "github.com/metal-stack/go-hal/internal/vendors/gigabyte" "github.com/metal-stack/go-hal/internal/vendors/vagrant" @@ -38,6 +39,8 @@ func InBand(log logger.Logger) (hal.InBand, error) { return vagrant.InBand(b, log) case api.VendorGigabyte: return gigabyte.InBand(b, log) + case api.VendorFujitsu: + return fujitsu.InBand(b, log) case api.VendorDell, api.VendorUnknown: fallthrough default: @@ -67,6 +70,8 @@ func OutBand(ip string, ipmiPort int, user, password string, log logger.Logger, return vagrant.OutBand(b, ip, ipmiPort, user, password), nil case api.VendorGigabyte: return gigabyte.OutBand(r, b), nil + case api.VendorFujitsu: + return fujitsu.OutBand(r, b), nil case api.VendorDell, api.VendorUnknown: fallthrough default: diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index b0de44e..8db0b06 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "net/http" "time" @@ -264,13 +265,13 @@ func (c *APIClient) SetChassisIdentifyLEDOn() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/1", c.urlPrefix), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/0", c.urlPrefix), bytes.NewReader(body)) // TODO Chassis number if err != nil { return err } c.addHeadersAndAuth(req) - resp, err := c.Do(req) + resp, err := c.doWithETag(req) if err == nil { _ = resp.Body.Close() } @@ -290,19 +291,20 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/1", c.urlPrefix), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/0", c.urlPrefix), bytes.NewReader(body)) // TODO Chassis number if err != nil { return err } c.addHeadersAndAuth(req) - resp, err := c.Do(req) + resp, err := c.doWithETag(req) if err == nil { _ = resp.Body.Close() } if err != nil { return fmt.Errorf("unable to turn off the chassis identify LED %w", err) } + // TODO http error handling return nil } @@ -365,8 +367,8 @@ func (c *APIClient) setBootOrderOverride(payload bootOverrideRequest) error { func (c *APIClient) addHeadersAndAuth(req *http.Request) { req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Basic "+c.basicAuth) - req.Header.Add("If-Match", "*") + req.Header.Add("Authorization", "Basic "+c.basicAuth) // TODO why do we neeed this if setBasicAuth is called below? + req.Header.Set("Accept", "application/json") req.SetBasicAuth(c.user, c.password) } @@ -418,3 +420,42 @@ func (c *APIClient) BMC() (*api.BMC, error) { return bmc, nil } + +func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return "", err + } + c.addHeadersAndAuth(req) + + resp, err := c.Do(req) + fmt.Println("ETag response body:", resp.Body) // TODO remove debug + if err != nil { + return "", err + } + defer resp.Body.Close() + + // Drain the body to ensure the connection can be reused + _, _ = io.Copy(io.Discard, resp.Body) + + if resp.StatusCode != http.StatusOK { + c.log.Warnw("failed to get etag, defaulting to wildcard", "status", resp.StatusCode, "url", url) + return "*", nil + } + + etag := resp.Header.Get("ETag") + if etag == "" { + return "*", nil + } + return etag, nil +} + +func (c *APIClient) doWithETag(req *http.Request) (*http.Response, error) { + etag, err := c.getETag(req.Context(), req.URL.String()) + if err != nil { + return nil, fmt.Errorf("failed to get ETag: %w", err) + } + + req.Header.Set("If-Match", etag) + return c.Do(req) +} diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go new file mode 100644 index 0000000..b00a5e5 --- /dev/null +++ b/internal/vendors/fujitsu/fujitsu.go @@ -0,0 +1,265 @@ +package fujitsu + +import ( + "fmt" + + "github.com/gliderlabs/ssh" + "github.com/google/uuid" + + "github.com/metal-stack/go-hal" + "github.com/metal-stack/go-hal/internal/inband" + "github.com/metal-stack/go-hal/internal/ipmi" + "github.com/metal-stack/go-hal/internal/outband" + "github.com/metal-stack/go-hal/internal/redfish" + "github.com/metal-stack/go-hal/pkg/api" + "github.com/metal-stack/go-hal/pkg/logger" +) + +var ( + // errorNotImplemented for all functions that are not implemented yet + errorNotImplemented = fmt.Errorf("not implemented yet") +) + +const ( + vendor = api.VendorFujitsu +) + +type ( + inBand struct { + *inband.InBand + } + outBand struct { + *outband.OutBand + } + bmcConnection struct { + *inBand + } + bmcConnectionOutBand struct { + *outBand + } +) + +// InBand creates an inband connection to a Fujitsu server. +func InBand(board *api.Board, log logger.Logger) (hal.InBand, error) { + ib, err := inband.New(board, true, log) + if err != nil { + return nil, err + } + return &inBand{ + InBand: ib, + }, nil +} + +// OutBand creates an outband connection to a Fujitsu server. +func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { + return &outBand{ + OutBand: outband.ViaRedfish(r, board), + } +} + +// InBand +func (ib *inBand) PowerOff() error { + return errorNotImplemented + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerDown) +} + +func (ib *inBand) PowerCycle() error { + return errorNotImplemented + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerCycle) +} + +func (ib *inBand) PowerReset() error { + return errorNotImplemented + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlHardReset) +} + +func (ib *inBand) IdentifyLEDState(state hal.IdentifyLEDState) error { + return errorNotImplemented + return ib.IpmiTool.SetChassisIdentifyLEDState(state) +} + +func (ib *inBand) IdentifyLEDOn() error { + return errorNotImplemented + return ib.IpmiTool.SetChassisIdentifyLEDOn() +} + +func (ib *inBand) IdentifyLEDOff() error { + return errorNotImplemented + return ib.IpmiTool.SetChassisIdentifyLEDOff() +} + +func (ib *inBand) BootFrom(bootTarget hal.BootTarget) error { + return errorNotImplemented + return ib.IpmiTool.SetBootOrder(bootTarget, vendor) +} + +func (ib *inBand) SetFirmware(hal.FirmwareMode) error { + return errorNotImplemented +} + +func (ib *inBand) Describe() string { + return "InBand connected to Fujitsu" +} + +func (ib *inBand) BMCConnection() api.BMCConnection { + return &bmcConnection{ + inBand: ib, + } +} + +func (c *bmcConnection) BMC() (*api.BMC, error) { + // return errorNotImplemented + return c.IpmiTool.BMC() +} + +func (c *bmcConnection) PresentSuperUser() api.BMCUser { + // return errorNotImplemented + return api.BMCUser{ + Name: "USERID", + Id: "2", + ChannelNumber: 1, + } +} + +func (c *bmcConnection) SuperUser() api.BMCUser { + // return errorNotImplemented + return api.BMCUser{ + Name: "superuser", + Id: "4", + ChannelNumber: 1, + } +} + +func (c *bmcConnection) User() api.BMCUser { + // return errorNotImplemented + return api.BMCUser{ + Name: "metal", + Id: "3", + ChannelNumber: 1, + } +} + +func (c *bmcConnection) Present() bool { + // return errorNotImplemented + return c.IpmiTool.DevicePresent() +} + +func (c *bmcConnection) CreateUserAndPassword(user api.BMCUser, privilege api.IpmiPrivilege) (string, error) { + // return errorNotImplemented + return c.IpmiTool.CreateUser(user, privilege, "", c.Board().Vendor.PasswordConstraints(), ipmi.HighLevel) +} + +func (c *bmcConnection) CreateUser(user api.BMCUser, privilege api.IpmiPrivilege, password string) error { + return errorNotImplemented + _, err := c.IpmiTool.CreateUser(user, privilege, password, nil, ipmi.HighLevel) + return err +} + +func (c *bmcConnection) NeedsPasswordChange(user api.BMCUser, password string) (bool, error) { + // return errorNotImplemented + return c.IpmiTool.NeedsPasswordChange(user, password) +} + +func (c *bmcConnection) ChangePassword(user api.BMCUser, newPassword string) error { + return errorNotImplemented + return c.IpmiTool.ChangePassword(user, newPassword, ipmi.HighLevel) +} + +func (c *bmcConnection) SetUserEnabled(user api.BMCUser, enabled bool) error { + return errorNotImplemented + return c.IpmiTool.SetUserEnabled(user, enabled, ipmi.HighLevel) +} + +func (ib *inBand) ConfigureBIOS() (bool, error) { + // return errorNotImplemented + //return false, errorNotImplemented // do not throw an error to not break manual tests + return false, nil //TODO https://github.com/metal-stack/go-hal/issues/11 +} + +func (ib *inBand) EnsureBootOrder(bootloaderID string) error { + return nil +} + +// OutBand +func (ob *outBand) UUID() (*uuid.UUID, error) { + u, err := ob.Redfish.MachineUUID() + if err != nil { + return nil, err + } + us, err := uuid.Parse(u) + if err != nil { + return nil, err + } + return &us, nil +} + +func (ob *outBand) PowerState() (hal.PowerState, error) { + // return errorNotImplemented + return ob.Redfish.PowerState() +} + +func (ob *outBand) PowerOff() error { + return errorNotImplemented + return ob.Redfish.PowerOff() +} + +func (ob *outBand) PowerOn() error { + return errorNotImplemented + return ob.Redfish.PowerReset() +} + +func (ob *outBand) PowerReset() error { + return errorNotImplemented + return ob.Redfish.PowerReset() +} + +func (ob *outBand) PowerCycle() error { + return errorNotImplemented + return ob.Redfish.PowerReset() +} + +func (ob *outBand) IdentifyLEDState(state hal.IdentifyLEDState) error { + // return errorNotImplemented + return ob.Redfish.SetChassisIdentifyLEDState(state) +} + +func (ob *outBand) IdentifyLEDOn() error { + // return errorNotImplemented + return ob.Redfish.SetChassisIdentifyLEDOn() +} + +func (ob *outBand) IdentifyLEDOff() error { + // return errorNotImplemented + return ob.Redfish.SetChassisIdentifyLEDOff() +} + +func (ob *outBand) BootFrom(target hal.BootTarget) error { + return errorNotImplemented + return ob.Redfish.SetBootOrder(target) +} + +func (ob *outBand) Describe() string { + return "OutBand connected to Fujitsu" +} + +func (ob *outBand) Console(s ssh.Session) error { + return errorNotImplemented +} + +func (ob *outBand) UpdateBIOS(url string) error { + return nil +} + +func (ob *outBand) UpdateBMC(url string) error { + return nil +} + +func (ob *outBand) BMCConnection() api.OutBandBMCConnection { + return &bmcConnectionOutBand{ + outBand: ob, + } +} + +func (c *bmcConnectionOutBand) BMC() (*api.BMC, error) { + return c.Redfish.BMC() +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 21b9e68..06cac84 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -198,6 +198,8 @@ const ( VendorLenovo // VendorDell identifies all Dell servers VendorDell + // VendorFujitsu identifies all Fujitsu servers + VendorFujitsu // VendorVagrant is a virtual machine. VendorVagrant // VendorGigabyte identifies all Gigabyte servers @@ -210,11 +212,12 @@ var ( VendorNovarion: "Novarion-Systems", VendorLenovo: "Lenovo", VendorDell: "Dell", + VendorFujitsu: "FUJITSU", VendorVagrant: "Vagrant", VendorUnknown: "UNKNOWN", VendorGigabyte: "Giga Computing", } - allVendors = [...]Vendor{VendorSupermicro, VendorNovarion, VendorLenovo, VendorDell, VendorVagrant, VendorUnknown, VendorGigabyte} + allVendors = [...]Vendor{VendorSupermicro, VendorNovarion, VendorLenovo, VendorDell, VendorFujitsu, VendorVagrant, VendorUnknown, VendorGigabyte} ) func (v Vendor) String() string { return vendors[v] } From ce9721f839fae2d39a061caed61932d56fd431fe Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 4 Feb 2026 17:43:37 +0100 Subject: [PATCH 02/22] Improve error handling and fix stuff --- internal/redfish/redfish.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 8db0b06..ced6c16 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -278,6 +278,9 @@ func (c *APIClient) SetChassisIdentifyLEDOn() error { if err != nil { return fmt.Errorf("unable to turn on the chassis identify LED %w", err) } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unable to turn on the chassis identify LED, status code: %d", resp.StatusCode) + } return nil } @@ -304,7 +307,9 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { if err != nil { return fmt.Errorf("unable to turn off the chassis identify LED %w", err) } - // TODO http error handling + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unable to turn off the chassis identify LED, status code: %d", resp.StatusCode) + } return nil } @@ -367,7 +372,6 @@ func (c *APIClient) setBootOrderOverride(payload bootOverrideRequest) error { func (c *APIClient) addHeadersAndAuth(req *http.Request) { req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", "Basic "+c.basicAuth) // TODO why do we neeed this if setBasicAuth is called below? req.Header.Set("Accept", "application/json") req.SetBasicAuth(c.user, c.password) } @@ -422,14 +426,13 @@ func (c *APIClient) BMC() (*api.BMC, error) { } func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return "", err } c.addHeadersAndAuth(req) resp, err := c.Do(req) - fmt.Println("ETag response body:", resp.Body) // TODO remove debug if err != nil { return "", err } From 0a6cd303e5f3021248a57f429c0caf1256969f41 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 4 Feb 2026 18:26:12 +0100 Subject: [PATCH 03/22] Allow setting ChassisID per vendor --- internal/redfish/redfish.go | 12 +++++++++--- internal/vendors/fujitsu/fujitsu.go | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index ced6c16..3bb9ba1 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -26,9 +26,10 @@ type APIClient struct { urlPrefix string user string password string - basicAuth string + basicAuth string // TODO Why do we need this? Seems to be never used log logger.Logger connectionTimeout time.Duration + chassisID int } type bootOverrideRequest struct { @@ -69,9 +70,14 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio urlPrefix: fmt.Sprintf("%s/redfish/v1", url), log: log, connectionTimeout: timeout, + chassisID: 1, // TODO should there be a default chassis ID? }, nil } +func (c *APIClient) SetChassisID(id int) { + c.chassisID = id +} + func (c *APIClient) BoardInfo() (*api.Board, error) { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() @@ -265,7 +271,7 @@ func (c *APIClient) SetChassisIdentifyLEDOn() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/0", c.urlPrefix), bytes.NewReader(body)) // TODO Chassis number + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%d", c.urlPrefix, c.chassisID), bytes.NewReader(body)) if err != nil { return err } @@ -294,7 +300,7 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/0", c.urlPrefix), bytes.NewReader(body)) // TODO Chassis number + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%d", c.urlPrefix, c.chassisID), bytes.NewReader(body)) if err != nil { return err } diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index b00a5e5..701aa13 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -21,7 +21,8 @@ var ( ) const ( - vendor = api.VendorFujitsu + vendor = api.VendorFujitsu + chassisID = 0 ) type ( @@ -52,6 +53,7 @@ func InBand(board *api.Board, log logger.Logger) (hal.InBand, error) { // OutBand creates an outband connection to a Fujitsu server. func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { + r.SetChassisID(chassisID) return &outBand{ OutBand: outband.ViaRedfish(r, board), } From 0667d83fdb083439aaac035edb6ff8622614de4b Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 08:50:29 +0100 Subject: [PATCH 04/22] Finish outband implementation --- internal/redfish/redfish.go | 20 +++++++++++++++++--- internal/vendors/fujitsu/fujitsu.go | 16 ++++------------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 3bb9ba1..7c6f663 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -30,6 +30,7 @@ type APIClient struct { log logger.Logger connectionTimeout time.Duration chassisID int + systemID string } type bootOverrideRequest struct { @@ -61,6 +62,9 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio if err != nil { return nil, err } + + // TODO do we want systemID and chassisID autodiscovery here? + return &APIClient{ client: c, Client: c.HTTPClient, @@ -70,7 +74,8 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio urlPrefix: fmt.Sprintf("%s/redfish/v1", url), log: log, connectionTimeout: timeout, - chassisID: 1, // TODO should there be a default chassis ID? + chassisID: 1, // TODO should there be a default chassis ID? + systemID: "Self", // TODO should there be a default system ID? }, nil } @@ -78,6 +83,10 @@ func (c *APIClient) SetChassisID(id int) { c.chassisID = id } +func (c *APIClient) SetSystemID(id string) { + c.systemID = id +} + func (c *APIClient) BoardInfo() (*api.Board, error) { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() @@ -359,19 +368,23 @@ func (c *APIClient) setBootOrderOverride(payload bootOverrideRequest) error { if err != nil { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Systems/Self", c.urlPrefix), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Systems/%s", c.urlPrefix, c.systemID), bytes.NewReader(body)) if err != nil { return err } c.addHeadersAndAuth(req) - resp, err := c.Do(req) + resp, err := c.doWithETag(req) if err == nil { + // TODO drain body? _ = resp.Body.Close() } if err != nil { return fmt.Errorf("unable to override boot order %w", err) } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("unable to override boot order, status code: %d", resp.StatusCode) + } return nil } @@ -431,6 +444,7 @@ func (c *APIClient) BMC() (*api.BMC, error) { return bmc, nil } +// TODO should we cache this? func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 701aa13..ff379f1 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -54,6 +54,7 @@ func InBand(board *api.Board, log logger.Logger) (hal.InBand, error) { // OutBand creates an outband connection to a Fujitsu server. func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { r.SetChassisID(chassisID) + r.SetSystemID("0") return &outBand{ OutBand: outband.ViaRedfish(r, board), } @@ -196,47 +197,38 @@ func (ob *outBand) UUID() (*uuid.UUID, error) { } func (ob *outBand) PowerState() (hal.PowerState, error) { - // return errorNotImplemented return ob.Redfish.PowerState() } func (ob *outBand) PowerOff() error { - return errorNotImplemented return ob.Redfish.PowerOff() } func (ob *outBand) PowerOn() error { - return errorNotImplemented - return ob.Redfish.PowerReset() + return ob.Redfish.PowerOn() } func (ob *outBand) PowerReset() error { - return errorNotImplemented return ob.Redfish.PowerReset() } func (ob *outBand) PowerCycle() error { - return errorNotImplemented - return ob.Redfish.PowerReset() + return ob.Redfish.PowerCycle() } func (ob *outBand) IdentifyLEDState(state hal.IdentifyLEDState) error { - // return errorNotImplemented return ob.Redfish.SetChassisIdentifyLEDState(state) } func (ob *outBand) IdentifyLEDOn() error { - // return errorNotImplemented return ob.Redfish.SetChassisIdentifyLEDOn() } func (ob *outBand) IdentifyLEDOff() error { - // return errorNotImplemented return ob.Redfish.SetChassisIdentifyLEDOff() } func (ob *outBand) BootFrom(target hal.BootTarget) error { - return errorNotImplemented return ob.Redfish.SetBootOrder(target) } @@ -245,7 +237,7 @@ func (ob *outBand) Describe() string { } func (ob *outBand) Console(s ssh.Session) error { - return errorNotImplemented + return errorNotImplemented // TODO use the same implementation as for dell after it's merged } func (ob *outBand) UpdateBIOS(url string) error { From 62815dfae14ade8792a079e1d109423411d30134 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 10:14:49 +0100 Subject: [PATCH 05/22] Umcomment inband implementation --- internal/redfish/redfish.go | 4 +++- internal/vendors/fujitsu/fujitsu.go | 18 +----------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 7c6f663..73b1900 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -456,7 +456,9 @@ func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { if err != nil { return "", err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() // Drain the body to ensure the connection can be reused _, _ = io.Copy(io.Discard, resp.Body) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index ff379f1..f55b10a 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -62,37 +62,30 @@ func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { // InBand func (ib *inBand) PowerOff() error { - return errorNotImplemented return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerDown) } func (ib *inBand) PowerCycle() error { - return errorNotImplemented return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerCycle) } func (ib *inBand) PowerReset() error { - return errorNotImplemented return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlHardReset) } func (ib *inBand) IdentifyLEDState(state hal.IdentifyLEDState) error { - return errorNotImplemented return ib.IpmiTool.SetChassisIdentifyLEDState(state) } func (ib *inBand) IdentifyLEDOn() error { - return errorNotImplemented return ib.IpmiTool.SetChassisIdentifyLEDOn() } func (ib *inBand) IdentifyLEDOff() error { - return errorNotImplemented return ib.IpmiTool.SetChassisIdentifyLEDOff() } func (ib *inBand) BootFrom(bootTarget hal.BootTarget) error { - return errorNotImplemented return ib.IpmiTool.SetBootOrder(bootTarget, vendor) } @@ -111,12 +104,10 @@ func (ib *inBand) BMCConnection() api.BMCConnection { } func (c *bmcConnection) BMC() (*api.BMC, error) { - // return errorNotImplemented return c.IpmiTool.BMC() } func (c *bmcConnection) PresentSuperUser() api.BMCUser { - // return errorNotImplemented return api.BMCUser{ Name: "USERID", Id: "2", @@ -125,7 +116,6 @@ func (c *bmcConnection) PresentSuperUser() api.BMCUser { } func (c *bmcConnection) SuperUser() api.BMCUser { - // return errorNotImplemented return api.BMCUser{ Name: "superuser", Id: "4", @@ -134,7 +124,6 @@ func (c *bmcConnection) SuperUser() api.BMCUser { } func (c *bmcConnection) User() api.BMCUser { - // return errorNotImplemented return api.BMCUser{ Name: "metal", Id: "3", @@ -143,33 +132,27 @@ func (c *bmcConnection) User() api.BMCUser { } func (c *bmcConnection) Present() bool { - // return errorNotImplemented return c.IpmiTool.DevicePresent() } func (c *bmcConnection) CreateUserAndPassword(user api.BMCUser, privilege api.IpmiPrivilege) (string, error) { - // return errorNotImplemented return c.IpmiTool.CreateUser(user, privilege, "", c.Board().Vendor.PasswordConstraints(), ipmi.HighLevel) } func (c *bmcConnection) CreateUser(user api.BMCUser, privilege api.IpmiPrivilege, password string) error { - return errorNotImplemented _, err := c.IpmiTool.CreateUser(user, privilege, password, nil, ipmi.HighLevel) return err } func (c *bmcConnection) NeedsPasswordChange(user api.BMCUser, password string) (bool, error) { - // return errorNotImplemented return c.IpmiTool.NeedsPasswordChange(user, password) } func (c *bmcConnection) ChangePassword(user api.BMCUser, newPassword string) error { - return errorNotImplemented return c.IpmiTool.ChangePassword(user, newPassword, ipmi.HighLevel) } func (c *bmcConnection) SetUserEnabled(user api.BMCUser, enabled bool) error { - return errorNotImplemented return c.IpmiTool.SetUserEnabled(user, enabled, ipmi.HighLevel) } @@ -229,6 +212,7 @@ func (ob *outBand) IdentifyLEDOff() error { } func (ob *outBand) BootFrom(target hal.BootTarget) error { + // On Fujitsu for BootSourceOverrideTarget = "BiosSetup" BootSourceOverrideEnabled is restricted to "Once" return ob.Redfish.SetBootOrder(target) } From 9aa5ff158de8b55a51872b3f88b42e4df1e109f7 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 10:15:17 +0100 Subject: [PATCH 06/22] Fix a typo --- hal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hal.go b/hal.go index a4e1c1a..7b00ef5 100644 --- a/hal.go +++ b/hal.go @@ -107,7 +107,7 @@ type InBand interface { // PowerCycle cycle the power state of the server PowerCycle() error - // IdentifyLEDState get the identify LED state + // IdentifyLEDState set the identify LED state IdentifyLEDState(IdentifyLEDState) error // IdentifyLEDOn set the identify LED to on IdentifyLEDOn() error @@ -157,7 +157,7 @@ type OutBand interface { // PowerCycle cycle the power state of the server PowerCycle() error - // IdentifyLEDState get the identify LED state + // IdentifyLEDState set the identify LED state IdentifyLEDState(IdentifyLEDState) error // IdentifyLEDOn set the identify LED to on IdentifyLEDOn() error From 382a6a92c12b0ab5f0a8875aa9754bfaf7595904 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 11:15:47 +0100 Subject: [PATCH 07/22] Add automatic SystemID/ChassisID discovery to make the code more robust --- internal/redfish/redfish.go | 91 +++++++++++++++++++++++++---- internal/vendors/fujitsu/fujitsu.go | 5 +- 2 files changed, 82 insertions(+), 14 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 1acddf9..f3f4be2 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -28,7 +28,7 @@ type APIClient struct { basicAuth string // TODO Why do we need this? Seems to be never used log logger.Logger connectionTimeout time.Duration - chassisID int + chassisID string systemID string } @@ -62,9 +62,7 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio return nil, err } - // TODO do we want systemID and chassisID autodiscovery here? - - return &APIClient{ + apiClient := &APIClient{ client: c, Client: c.HTTPClient, user: user, @@ -73,12 +71,85 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio urlPrefix: fmt.Sprintf("%s/redfish/v1", url), log: log, connectionTimeout: timeout, - chassisID: 1, // TODO should there be a default chassis ID? - systemID: "Self", // TODO should there be a default system ID? - }, nil + chassisID: "", + systemID: "", + } + + // Discover systemID and chassisID + if err := apiClient.discoverIDs(ctx); err != nil { + log.Warnw("failed to auto-discover system/chassis ID, using defaults", "error", err) + } + + if apiClient.systemID == "" { + apiClient.systemID = "Self" // Default SystemID to ensure backwards compatibility + } + if apiClient.chassisID == "" { + apiClient.chassisID = "1" // Default ChassisID to ensure backwards compatibility + } + + return apiClient, nil +} + +// discoverIDs attempts to automatically discover the systemID and chassisID +func (c *APIClient) discoverIDs(ctx context.Context) error { + g := c.client.WithContext(ctx) + + if g.Service == nil { + return fmt.Errorf("gofish service root is not available") + } + + // Discover System ID + systems, err := g.Service.Systems() + if err != nil { + return fmt.Errorf("failed to query systems: %w", err) + } + + if len(systems) > 0 { + // Use the ID from the first system + c.systemID = systems[0].ID + c.log.Debugw("discovered system ID", "systemID", c.systemID) + } else { + c.log.Warnw("no systems found during discovery") + } + + // Discover Chassis ID + chassis, err := g.Service.Chassis() + if err != nil { + return fmt.Errorf("failed to query chassis: %w", err) + } + + // Prefer RackMount chassis, but fall back to any chassis if none found + var selectedChassis *schemas.Chassis + for _, chass := range chassis { + if chass.ChassisType == schemas.RackMountChassisType { + selectedChassis = chass + break + } + } + + // If no RackMount chassis found, use the first available + if selectedChassis == nil && len(chassis) > 0 { + selectedChassis = chassis[0] + c.log.Debugw("no RackMount chassis found, using first available", + "chassisType", selectedChassis.ChassisType) + } + + if selectedChassis != nil { + c.chassisID = selectedChassis.ID + c.log.Debugw("discovered chassis ID", "chassisID", c.chassisID, "chassisType", selectedChassis.ChassisType) + } else { + c.log.Warnw("no chassis found during discovery") + } + + // Error if we couldn't find either ID + if c.systemID == "" || c.chassisID == "" { + return fmt.Errorf("failed to discover any system or chassis IDs") + } + + return nil } -func (c *APIClient) SetChassisID(id int) { +func (c *APIClient) SetChassisID(id string) { c.chassisID = id } @@ -278,7 +349,7 @@ func (c *APIClient) SetChassisIdentifyLEDOn() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%d", c.urlPrefix, c.chassisID), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%s", c.urlPrefix, c.chassisID), bytes.NewReader(body)) if err != nil { return err } @@ -307,7 +378,7 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%d", c.urlPrefix, c.chassisID), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/%s", c.urlPrefix, c.chassisID), bytes.NewReader(body)) if err != nil { return err } diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index f55b10a..2953f98 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -21,8 +21,7 @@ var ( ) const ( - vendor = api.VendorFujitsu - chassisID = 0 + vendor = api.VendorFujitsu ) type ( @@ -53,8 +52,6 @@ func InBand(board *api.Board, log logger.Logger) (hal.InBand, error) { // OutBand creates an outband connection to a Fujitsu server. func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { - r.SetChassisID(chassisID) - r.SetSystemID("0") return &outBand{ OutBand: outband.ViaRedfish(r, board), } From fd2f577238bc8a12500ed390443b0437a6f4fcf6 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 14:29:25 +0100 Subject: [PATCH 08/22] Remove unnecessary code --- internal/redfish/redfish.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index f3f4be2..70d1eea 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -3,7 +3,6 @@ package redfish import ( "bytes" "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -25,7 +24,6 @@ type APIClient struct { urlPrefix string user string password string - basicAuth string // TODO Why do we need this? Seems to be never used log logger.Logger connectionTimeout time.Duration chassisID string @@ -67,7 +65,6 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio Client: c.HTTPClient, user: user, password: password, - basicAuth: base64.StdEncoding.EncodeToString([]byte(user + ":" + password)), urlPrefix: fmt.Sprintf("%s/redfish/v1", url), log: log, connectionTimeout: timeout, From cc98d0d7321315c76c052b160d4f3fb7b9c9e7a8 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 17:50:46 +0100 Subject: [PATCH 09/22] Don't use ETags if not necessary --- internal/redfish/redfish.go | 29 +++++++++++++++++++++++------ internal/vendors/fujitsu/fujitsu.go | 1 + 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 70d1eea..b61ff76 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -28,6 +28,7 @@ type APIClient struct { connectionTimeout time.Duration chassisID string systemID string + ETagRequired bool } type bootOverrideRequest struct { @@ -70,6 +71,7 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio connectionTimeout: timeout, chassisID: "", systemID: "", + ETagRequired: false, } // Discover systemID and chassisID @@ -87,6 +89,10 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio return apiClient, nil } +func (c *APIClient) SetETagRequired(required bool) { + c.ETagRequired = required +} + // discoverIDs attempts to automatically discover the systemID and chassisID func (c *APIClient) discoverIDs(ctx context.Context) error { g := c.client.WithContext(ctx) @@ -510,7 +516,6 @@ func (c *APIClient) BMC() (*api.BMC, error) { return bmc, nil } -// TODO should we cache this? func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -542,11 +547,23 @@ func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { } func (c *APIClient) doWithETag(req *http.Request) (*http.Response, error) { - etag, err := c.getETag(req.Context(), req.URL.String()) - if err != nil { - return nil, fmt.Errorf("failed to get ETag: %w", err) - } + if c.ETagRequired { + // Create a context with timeout for the ETag fetch + ctx := req.Context() + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, c.connectionTimeout) + defer cancel() + } - req.Header.Set("If-Match", etag) + etag, err := c.getETag(ctx, req.URL.String()) + if err != nil { + return nil, fmt.Errorf("failed to get ETag: %w", err) + } + + req.Header.Set("If-Match", etag) + } else { + req.Header.Set("If-Match", "*") + } return c.Do(req) } diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 2953f98..63c7a6b 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -52,6 +52,7 @@ func InBand(board *api.Board, log logger.Logger) (hal.InBand, error) { // OutBand creates an outband connection to a Fujitsu server. func OutBand(r *redfish.APIClient, board *api.Board) hal.OutBand { + r.SetETagRequired(true) return &outBand{ OutBand: outband.ViaRedfish(r, board), } From bc9953b548385846b5ad799ce169deb4f27c602d Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 5 Feb 2026 18:02:54 +0100 Subject: [PATCH 10/22] Adjust usernames --- internal/vendors/fujitsu/fujitsu.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 63c7a6b..a49c469 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -115,7 +115,7 @@ func (c *bmcConnection) PresentSuperUser() api.BMCUser { func (c *bmcConnection) SuperUser() api.BMCUser { return api.BMCUser{ - Name: "superuser", + Name: "root", Id: "4", ChannelNumber: 1, } From 845ef3d65cb479268e5dfc314eb6c87efcbcde90 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Tue, 10 Feb 2026 18:14:20 +0100 Subject: [PATCH 11/22] Adjust password constraints to fix user creation --- internal/vendors/fujitsu/fujitsu.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index a49c469..c436fcd 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -134,7 +134,9 @@ func (c *bmcConnection) Present() bool { } func (c *bmcConnection) CreateUserAndPassword(user api.BMCUser, privilege api.IpmiPrivilege) (string, error) { - return c.IpmiTool.CreateUser(user, privilege, "", c.Board().Vendor.PasswordConstraints(), ipmi.HighLevel) + password_constraints := c.Board().Vendor.PasswordConstraints() + password_constraints.Length = 12 + return c.IpmiTool.CreateUser(user, privilege, "", password_constraints, ipmi.HighLevel) } func (c *bmcConnection) CreateUser(user api.BMCUser, privilege api.IpmiPrivilege, password string) error { From 6b1c48c8f7d4bd158ec6de74e84472cb53336353 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 11 Feb 2026 16:49:19 +0100 Subject: [PATCH 12/22] Adjust BMC channel number --- internal/vendors/fujitsu/fujitsu.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index c436fcd..22f4d5f 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -109,7 +109,7 @@ func (c *bmcConnection) PresentSuperUser() api.BMCUser { return api.BMCUser{ Name: "USERID", Id: "2", - ChannelNumber: 1, + ChannelNumber: 2, } } @@ -117,7 +117,7 @@ func (c *bmcConnection) SuperUser() api.BMCUser { return api.BMCUser{ Name: "root", Id: "4", - ChannelNumber: 1, + ChannelNumber: 2, } } @@ -125,7 +125,7 @@ func (c *bmcConnection) User() api.BMCUser { return api.BMCUser{ Name: "metal", Id: "3", - ChannelNumber: 1, + ChannelNumber: 2, } } From 32808a8015bae9414d8d492350ab1c46231ee33f Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Mon, 16 Feb 2026 16:17:07 +0100 Subject: [PATCH 13/22] Fix PowerCycle() on Fujitsu --- internal/redfish/redfish.go | 21 +++++++++++++++++++++ internal/vendors/fujitsu/fujitsu.go | 15 ++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index b61ff76..07aa02f 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -89,6 +89,27 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio return apiClient, nil } +func (r *APIClient) GetSystem() (*schemas.ComputerSystem, error) { + g := r.client.WithContext(context.Background()) + + if g.Service == nil { + return nil, fmt.Errorf("gofish service root is not available") + } + service := g.Service + + systems, err := service.Systems() + if err != nil { + return nil, err + } + + for _, s := range systems { + if s.ID == r.systemID { + return s, nil + } + } + return nil, fmt.Errorf("system with ID %q not found", r.systemID) +} + func (c *APIClient) SetETagRequired(required bool) { c.ETagRequired = required } diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 22f4d5f..7405a9a 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -196,7 +196,20 @@ func (ob *outBand) PowerReset() error { } func (ob *outBand) PowerCycle() error { - return ob.Redfish.PowerCycle() + system, err := ob.Redfish.GetSystem() + if err != nil { + return err + } + + // Construct OEM action path + oemPath := fmt.Sprintf("%s/Actions/Oem/FTSComputerSystem.Reset", system.ODataID) + + body := map[string]interface{}{ + "FTSResetType": "PowerCycle", + } + + err = system.Post(oemPath, body) + return err } func (ob *outBand) IdentifyLEDState(state hal.IdentifyLEDState) error { From 61d0c75c98ec4c784de2b395c106ca1fff148af5 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Mon, 16 Feb 2026 16:25:25 +0100 Subject: [PATCH 14/22] Drain http response body to allow golang to reuse the connection --- internal/redfish/redfish.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 07aa02f..b039849 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -469,7 +469,8 @@ func (c *APIClient) setBootOrderOverride(payload bootOverrideRequest) error { resp, err := c.doWithETag(req) if err == nil { - // TODO drain body? + // Drain the body to ensure the connection can be reused + _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() } if err != nil { From 5829d27a73888e077ec54a4abb4fd63ef2a7ca49 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Tue, 17 Feb 2026 15:24:44 +0100 Subject: [PATCH 15/22] Fix Reset() methods by using the gofish fork with a fix (to be replaced when merged to the upstream) --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index d656c61..575cdf9 100644 --- a/go.mod +++ b/go.mod @@ -73,3 +73,5 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/stmcginnis/gofish => github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac diff --git a/go.sum b/go.sum index 5b01b1e..a55e756 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac h1:f3dM7py5J/Gv3ZokOJ8pKB7Sb/u39VtxQfVd6+vI4ok= +github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -106,8 +108,6 @@ github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZP github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/stmcginnis/gofish v0.20.1-0.20260203173523-22b2013b7c28 h1:SbDUY/fBx550CKQ/DhSlNCTXyEJTDn+gFO0J6Gt7cgs= -github.com/stmcginnis/gofish v0.20.1-0.20260203173523-22b2013b7c28/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= From c15d12177b00299a7271660bfcc19004c9ee728f Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Tue, 17 Feb 2026 15:48:29 +0100 Subject: [PATCH 16/22] Fix Reset() methods: use the upstream --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 575cdf9..158ec04 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/uuid v1.6.0 github.com/metal-stack/metal-lib v0.23.5 github.com/sethvargo/go-password v0.3.1 - github.com/stmcginnis/gofish v0.20.1-0.20260203173523-22b2013b7c28 + github.com/stmcginnis/gofish v0.21.1 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go v0.40.0 github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 @@ -73,5 +73,3 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/stmcginnis/gofish => github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac diff --git a/go.sum b/go.sum index a55e756..5c9a1c6 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= -github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac h1:f3dM7py5J/Gv3ZokOJ8pKB7Sb/u39VtxQfVd6+vI4ok= -github.com/izvyk/gofish v0.0.0-20260217135108-fb92da3e96ac/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -108,6 +106,8 @@ github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZP github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stmcginnis/gofish v0.21.1 h1:sutDvBhmLh4RDOZ1DN8GUyYRu7f1ggvKMMnSaiqhwn4= +github.com/stmcginnis/gofish v0.21.1/go.mod h1:PzF5i8ecRG9A2ol8XT64npKUunyraJ+7t0kYMpQAtqU= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= From 40058f0838152b6cadf2e3458b60df43a9ba9837 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Thu, 19 Feb 2026 11:55:41 +0100 Subject: [PATCH 17/22] Fix RedFish permissions via IPMI inband command to fix user creation --- internal/vendors/fujitsu/fujitsu.go | 84 +++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 7405a9a..6d89b84 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -2,6 +2,7 @@ package fujitsu import ( "fmt" + "strconv" "github.com/gliderlabs/ssh" "github.com/google/uuid" @@ -136,12 +137,24 @@ func (c *bmcConnection) Present() bool { func (c *bmcConnection) CreateUserAndPassword(user api.BMCUser, privilege api.IpmiPrivilege) (string, error) { password_constraints := c.Board().Vendor.PasswordConstraints() password_constraints.Length = 12 - return c.IpmiTool.CreateUser(user, privilege, "", password_constraints, ipmi.HighLevel) + s, err := c.IpmiTool.CreateUser(user, privilege, "", password_constraints, ipmi.HighLevel) + if err != nil { + return "", err + } + + err_perm := c.syncRedfishPermissions(user, privilege) + if err_perm != nil { + return "", err_perm + } + return s, nil } func (c *bmcConnection) CreateUser(user api.BMCUser, privilege api.IpmiPrivilege, password string) error { _, err := c.IpmiTool.CreateUser(user, privilege, password, nil, ipmi.HighLevel) - return err + if err != nil { + return err + } + return c.syncRedfishPermissions(user, privilege) } func (c *bmcConnection) NeedsPasswordChange(user api.BMCUser, password string) (bool, error) { @@ -153,7 +166,72 @@ func (c *bmcConnection) ChangePassword(user api.BMCUser, newPassword string) err } func (c *bmcConnection) SetUserEnabled(user api.BMCUser, enabled bool) error { - return c.IpmiTool.SetUserEnabled(user, enabled, ipmi.HighLevel) + err := c.IpmiTool.SetUserEnabled(user, enabled, ipmi.HighLevel) + if err != nil { + return err + } + return c.syncRedfishPermissions(user, api.UserPrivilege) +} + +func (c *bmcConnection) syncRedfishPermissions(user api.BMCUser, privilege api.IpmiPrivilege) error { + // 1. Parse the User ID + if user.Id == "" { + return fmt.Errorf("user ID is empty, cannot set Redfish permissions") + } + + userIDInt, err := strconv.Atoi(user.Id) + if err != nil { + return fmt.Errorf("failed to parse user ID %q: %w", user.Id, err) + } + + // IPMI raw userID = - 1 + targetUserID := userIDInt - 1 + userIDHex := fmt.Sprintf("0x%02x", targetUserID) + + // 2. Map standard IPMI Privileges to Fujitsu OEM Redfish Role values + var roleHex string + enabled := "0x01" // Enabled + switch privilege { + case api.AdministratorPrivilege: + roleHex = "0x02" // Administrator + case api.OperatorPrivilege: + roleHex = "0x01" // Operator + case api.UserPrivilege: + roleHex = "0x03" // Read-Only + default: + roleHex = "0x00" // No Access + enabled = "0x00" // Disabled + } + + // 3. Set Role (0x81 0x1D feature code) + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x81 0x1D 0x01 + // Example for user ID 3 (which becomes 2 in hex) and Administrator role (0x02): + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x02 0x81 0x1D 0x01 0x02 + setRoleCmd := []string{ + "raw", "0x2e", "0xe0", "0x80", "0x28", "0x00", + "0x02", userIDHex, "0x81", "0x1D", "0x01", roleHex, + } + + _, err = c.IpmiTool.Run(setRoleCmd...) + if err != nil { + return fmt.Errorf("failed to set fujitsu redfish role for user %d: %w", userIDInt, err) + } + + // 4. Enable Access (0x81 0x1D feature code) + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x81 0x1D 0x01 <0x01 for enable, 0x00 for disable> + // Example to enable access for user ID 3: + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x02 0x81 0x1D 0x01 0x01 + enableAccessCmd := []string{ + "raw", "0x2e", "0xe0", "0x80", "0x28", "0x00", + "0x02", userIDHex, "0x80", "0x1D", "0x01", enabled, + } + + _, err = c.IpmiTool.Run(enableAccessCmd...) + if err != nil { + return fmt.Errorf("failed to enable fujitsu redfish access for user %d: %w", userIDInt, err) + } + + return nil } func (ib *inBand) ConfigureBIOS() (bool, error) { From 0c999d37750313d451645617a37eac7377aae1e3 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 25 Feb 2026 16:34:11 +0100 Subject: [PATCH 18/22] Try serial console via IPMI --- internal/vendors/fujitsu/fujitsu.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 6d89b84..e0699d7 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -312,7 +312,7 @@ func (ob *outBand) Describe() string { } func (ob *outBand) Console(s ssh.Session) error { - return errorNotImplemented // TODO use the same implementation as for dell after it's merged + return ob.IpmiTool.OpenConsole(s) } func (ob *outBand) UpdateBIOS(url string) error { From 3702a944ed82f38f247e765cfcd2dbad076da3bb Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Fri, 10 Apr 2026 15:15:07 +0200 Subject: [PATCH 19/22] Fix the bootTarget --- internal/vendors/fujitsu/fujitsu.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index e0699d7..650a323 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -304,7 +304,7 @@ func (ob *outBand) IdentifyLEDOff() error { func (ob *outBand) BootFrom(target hal.BootTarget) error { // On Fujitsu for BootSourceOverrideTarget = "BiosSetup" BootSourceOverrideEnabled is restricted to "Once" - return ob.Redfish.SetBootOrder(target) + return ob.Redfish.SetBootTarget(target) } func (ob *outBand) Describe() string { From 972c7523a3bbb8798bd75664d0d0eeb897d747fb Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Fri, 10 Apr 2026 17:35:02 +0200 Subject: [PATCH 20/22] Refactoring and fixes --- internal/redfish/redfish.go | 228 +++++++++++----------------- internal/vendors/fujitsu/fujitsu.go | 12 +- 2 files changed, 94 insertions(+), 146 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 3361e63..a09c2f9 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -133,92 +133,71 @@ func (c *APIClient) getChassis(ctx context.Context) (*schemas.Chassis, error) { func (c *APIClient) BoardInfo() (*api.Board, error) { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() - g := c.client.WithContext(ctx) - // Query the chassis data using the session token - if g.Service == nil { - return nil, fmt.Errorf("gofish service root is not available most likely due to missing username") + + system, err := c.getSystem(ctx) + if err != nil { + c.log.Warnw("ignore system query", "error", err) } biosVersion := "" manufacturer := "" model := "" + if system != nil { + biosVersion = system.BiosVersion + manufacturer = system.Manufacturer + model = system.Model + } - systems, err := g.Service.Systems() + chass, err := c.getChassis(ctx) if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) - } - for _, system := range systems { - if system.BiosVersion != "" { - biosVersion = system.BiosVersion - break - } - } - for _, system := range systems { - if system.Manufacturer != "" { - manufacturer = system.Manufacturer - break - } - } - for _, system := range systems { - if system.Model != "" { - model = system.Model - break - } + return nil, fmt.Errorf("no board detected: %w", err) } - chassis, err := g.Service.Chassis() + power, err := chass.Power() + var powerMetric *api.PowerMetric + var powerSupplies []api.PowerSupply if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) - } - for _, chass := range chassis { - if chass.ChassisType == schemas.RackMountChassisType { - power, err := chass.Power() - var powerMetric *api.PowerMetric - if err != nil { - c.log.Warnw("ignoring power detection", "error", err) - } else { - for _, pc := range power.PowerControl { - pm := pc.PowerMetrics - if pm.AverageConsumedWatts == nil && pm.IntervalInMin == nil { - continue - } - powerMetric = &api.PowerMetric{ - AverageConsumedWatts: pointer.SafeDeref(pm.AverageConsumedWatts), - IntervalInMin: float32(pointer.SafeDeref(pm.IntervalInMin)), - MaxConsumedWatts: pointer.SafeDeref(pm.MaxConsumedWatts), - MinConsumedWatts: pointer.SafeDeref(pm.MinConsumedWatts), - } - c.log.Debugw("power consumption", "metrics", powerMetric) - break - } + c.log.Warnw("ignoring power detection", "error", err) + } else { + for _, pc := range power.PowerControl { + pm := pc.PowerMetrics + if pm.AverageConsumedWatts == nil && pm.IntervalInMin == nil { + continue } - var powerSupplies []api.PowerSupply - for _, ps := range power.PowerSupplies { - powerSupplies = append(powerSupplies, api.PowerSupply{ - Status: api.Status{ - Health: string(ps.Status.Health), - State: string(ps.Status.State), - }, - }) - c.log.Debugw("powersupplies", "powersupply", ps) + powerMetric = &api.PowerMetric{ + AverageConsumedWatts: pointer.SafeDeref(pm.AverageConsumedWatts), + IntervalInMin: float32(pointer.SafeDeref(pm.IntervalInMin)), + MaxConsumedWatts: pointer.SafeDeref(pm.MaxConsumedWatts), + MinConsumedWatts: pointer.SafeDeref(pm.MinConsumedWatts), } - c.log.Debugw("got chassis", - "Manufacturer", manufacturer, "Model", model, "Name", chass.Name, - "PartNumber", chass.PartNumber, "SerialNumber", chass.SerialNumber, - "BiosVersion", biosVersion, "led", chass.IndicatorLED) //nolint:staticcheck - return &api.Board{ - VendorString: manufacturer, - Model: model, - PartNumber: chass.PartNumber, - SerialNumber: chass.SerialNumber, - BiosVersion: biosVersion, - IndicatorLED: toMetalLEDState(chass.IndicatorLED), //nolint:staticcheck - PowerMetric: powerMetric, - PowerSupplies: powerSupplies, - }, nil + c.log.Debugw("power consumption", "metrics", powerMetric) + break + } + for _, ps := range power.PowerSupplies { + powerSupplies = append(powerSupplies, api.PowerSupply{ + Status: api.Status{ + Health: string(ps.Status.Health), + State: string(ps.Status.State), + }, + }) + c.log.Debugw("powersupplies", "powersupply", ps) } } - return nil, fmt.Errorf("no board detected: #chassis:%d", len(chassis)) + + c.log.Debugw("got chassis", + "Manufacturer", manufacturer, "Model", model, "Name", chass.Name, + "PartNumber", chass.PartNumber, "SerialNumber", chass.SerialNumber, + "BiosVersion", biosVersion, "led", chass.IndicatorLED) //nolint:staticcheck + return &api.Board{ + VendorString: manufacturer, + Model: model, + PartNumber: chass.PartNumber, + SerialNumber: chass.SerialNumber, + BiosVersion: biosVersion, + IndicatorLED: toMetalLEDState(chass.IndicatorLED), //nolint:staticcheck + PowerMetric: powerMetric, + PowerSupplies: powerSupplies, + }, nil } func toMetalLEDState(state schemas.IndicatorLED) string { @@ -280,17 +259,15 @@ func (c *APIClient) PowerCycle() error { func (c *APIClient) setPower(resetType schemas.ResetType) error { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() - g := c.client.WithContext(ctx) - systems, err := g.Service.Systems() + + system, err := c.getSystem(ctx) if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) + return fmt.Errorf("failed to get system for power action %s: %w", resetType, err) } - for _, system := range systems { - if _, err = system.Reset(resetType); err == nil { - return nil - } + if _, err = system.Reset(resetType); err != nil { + return fmt.Errorf("failed to set power to %s: %w", resetType, err) } - return fmt.Errorf("failed to set power to %s %w", resetType, err) + return nil } // SetChassisIdentifyLEDState sets the chassis identify LED to given state @@ -309,41 +286,16 @@ func (c *APIClient) SetChassisIdentifyLEDState(state hal.IdentifyLEDState) error // SetChassisIdentifyLEDOn turns on the chassis identify LED func (c *APIClient) SetChassisIdentifyLEDOn() error { - ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) - defer cancel() - - chassis, err := c.getChassis(ctx) - if err != nil { - return err - } - - payload := indicatorLEDRequest{ - IndicatorLED: schemas.LitIndicatorLED, - } - body, err := json.Marshal(payload) - if err != nil { - return err - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("%s/Chassis/%s", c.urlPrefix, chassis.ID), bytes.NewReader(body)) - if err != nil { - return err - } - c.addHeadersAndAuth(req) - - resp, err := c.doWithETag(req) - if err != nil { - return fmt.Errorf("unable to turn on the chassis identify LED %w", err) - } - _ = resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("unable to turn on the chassis identify LED, status code: %d", resp.StatusCode) - } - return nil + return c.setChassisIndicatorLED(schemas.LitIndicatorLED) } // SetChassisIdentifyLEDOff turns off the chassis identify LED func (c *APIClient) SetChassisIdentifyLEDOff() error { + return c.setChassisIndicatorLED(schemas.OffIndicatorLED) +} + +// setChassisIndicatorLED is the shared implementation for setting the chassis indicator LED state. +func (c *APIClient) setChassisIndicatorLED(state schemas.IndicatorLED) error { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() @@ -352,9 +304,7 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { return err } - payload := indicatorLEDRequest{ - IndicatorLED: schemas.OffIndicatorLED, - } + payload := indicatorLEDRequest{IndicatorLED: state} body, err := json.Marshal(payload) if err != nil { return err @@ -368,11 +318,12 @@ func (c *APIClient) SetChassisIdentifyLEDOff() error { resp, err := c.doWithETag(req) if err != nil { - return fmt.Errorf("unable to turn off the chassis identify LED %w", err) + return fmt.Errorf("unable to set chassis identify LED to %s: %w", state, err) } + _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("unable to turn off the chassis identify LED, status code: %d", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unable to set chassis identify LED to %s, status code: %d", state, resp.StatusCode) } return nil } @@ -445,7 +396,7 @@ func (c *APIClient) setBootTargetOverride(payload bootOverrideRequest) error { } func (c *APIClient) addHeadersAndAuth(req *http.Request) { - req.Header.Add("Content-Type", "application/json") + req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") req.SetBasicAuth(c.user, c.password) } @@ -464,33 +415,24 @@ func (c *APIClient) setNextBootBIOS() error { func (c *APIClient) BMC() (*api.BMC, error) { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() - g := c.client.WithContext(ctx) - systems, err := g.Service.Systems() - if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) - } - - chassis, err := g.Service.Chassis() - if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) - } bmc := &api.BMC{} - for _, system := range systems { + system, err := c.getSystem(ctx) + if err != nil { + c.log.Warnw("ignore system query", "error", err) + } else { bmc.ProductManufacturer = system.Manufacturer bmc.ProductPartNumber = system.PartNumber bmc.ProductSerial = system.SerialNumber } - for _, chass := range chassis { - if chass.ChassisType != schemas.RackMountChassisType { - continue - } - + chass, err := c.getChassis(ctx) + if err != nil { + c.log.Warnw("ignore chassis query", "error", err) + } else { bmc.ChassisPartNumber = chass.PartNumber bmc.ChassisPartSerial = chass.SerialNumber - bmc.BoardMfg = chass.Manufacturer } @@ -524,6 +466,10 @@ func (c *APIClient) GetBootOptions() ([]*schemas.BootOption, error) { // SetBootOrder sets the boot order to match the sequence of the boot option entries func (c *APIClient) SetBootOrder(entries []*schemas.BootOption) error { + if len(entries) == 0 { + return fmt.Errorf("cannot set boot order: no boot entries provided") + } + ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() @@ -554,10 +500,11 @@ func (c *APIClient) SetBootOrder(entries []*schemas.BootOption) error { } c.addHeadersAndAuth(req) - resp, err := c.Do(req) + resp, err := c.doWithETag(req) if err != nil { return fmt.Errorf("unable to set boot order: %w", err) } + _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unable to set boot order, http status: %s", resp.Status) @@ -588,7 +535,7 @@ func (c *APIClient) UpdateFirmware(url string) error { } c.addHeadersAndAuth(req) - resp, err := c.Do(req) + resp, err := c.doWithETag(req) if err != nil { return fmt.Errorf("unable to trigger update: %w", err) } @@ -618,12 +565,9 @@ func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { if err != nil { return "", err } - defer func() { - _ = resp.Body.Close() - }() - - // Drain the body to ensure the connection can be reused + // Drain and close the body to ensure the connection can be reused _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() if resp.StatusCode != http.StatusOK { c.log.Warnw("failed to get etag, defaulting to wildcard", "status", resp.StatusCode, "url", url) diff --git a/internal/vendors/fujitsu/fujitsu.go b/internal/vendors/fujitsu/fujitsu.go index 650a323..6d23e68 100644 --- a/internal/vendors/fujitsu/fujitsu.go +++ b/internal/vendors/fujitsu/fujitsu.go @@ -170,7 +170,11 @@ func (c *bmcConnection) SetUserEnabled(user api.BMCUser, enabled bool) error { if err != nil { return err } - return c.syncRedfishPermissions(user, api.UserPrivilege) + privilege := api.UserPrivilege + if !enabled { + privilege = api.NoAccessPrivilege + } + return c.syncRedfishPermissions(user, privilege) } func (c *bmcConnection) syncRedfishPermissions(user api.BMCUser, privilege api.IpmiPrivilege) error { @@ -217,10 +221,10 @@ func (c *bmcConnection) syncRedfishPermissions(user api.BMCUser, privilege api.I return fmt.Errorf("failed to set fujitsu redfish role for user %d: %w", userIDInt, err) } - // 4. Enable Access (0x81 0x1D feature code) - // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x81 0x1D 0x01 <0x01 for enable, 0x00 for disable> + // 4. Enable/disable Redfish access (0x80 0x1D feature code) + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x80 0x1D 0x01 <0x01 for enable, 0x00 for disable> // Example to enable access for user ID 3: - // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x02 0x81 0x1D 0x01 0x01 + // ipmitool raw 0x2e 0xe0 0x80 0x28 0x00 0x02 0x02 0x80 0x1D 0x01 0x01 enableAccessCmd := []string{ "raw", "0x2e", "0xe0", "0x80", "0x28", "0x00", "0x02", userIDHex, "0x80", "0x1D", "0x01", enabled, From d9fc0353dbcdee6f0af93cbcd3a022e0e05ea474 Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Sun, 12 Apr 2026 20:41:11 +0200 Subject: [PATCH 21/22] Fix http response code check --- internal/redfish/redfish.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index a09c2f9..49b8d8b 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -322,7 +322,7 @@ func (c *APIClient) setChassisIndicatorLED(state schemas.IndicatorLED) error { } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("unable to set chassis identify LED to %s, status code: %d", state, resp.StatusCode) } return nil @@ -389,7 +389,7 @@ func (c *APIClient) setBootTargetOverride(payload bootOverrideRequest) error { // Drain the body to ensure the connection can be reused _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("unable to override boot order, http status: %s", resp.Status) } return nil @@ -506,7 +506,7 @@ func (c *APIClient) SetBootOrder(entries []*schemas.BootOption) error { } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("unable to set boot order, http status: %s", resp.Status) } return nil @@ -569,7 +569,7 @@ func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() - if resp.StatusCode != http.StatusOK { + if resp.StatusCode < 200 || resp.StatusCode >= 300 { c.log.Warnw("failed to get etag, defaulting to wildcard", "status", resp.StatusCode, "url", url) return "*", nil } From bded598c6e398348b369a34fa4ea5d03b5bf6e0d Mon Sep 17 00:00:00 2001 From: Philipp Mukosey Date: Wed, 15 Apr 2026 14:47:07 +0200 Subject: [PATCH 22/22] Fix cancelled context issue for PowerCycle on Fujitsu --- internal/redfish/redfish.go | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 49b8d8b..78ed29e 100644 --- a/internal/redfish/redfish.go +++ b/internal/redfish/redfish.go @@ -81,15 +81,11 @@ func (c *APIClient) SetETagRequired(required bool) { c.ETagRequired = required } -// GetSystem returns the single managed ComputerSystem. -// Servers are expected to expose exactly one system via Redfish. func (c *APIClient) GetSystem() (*schemas.ComputerSystem, error) { - ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) - defer cancel() - return c.getSystem(ctx) + return c.getSystem(context.Background()) + } -// getSystem is the internal implementation of GetSystem, accepting an existing context. func (c *APIClient) getSystem(ctx context.Context) (*schemas.ComputerSystem, error) { g := c.client.WithContext(ctx) if g.Service == nil { @@ -108,8 +104,6 @@ func (c *APIClient) getSystem(ctx context.Context) (*schemas.ComputerSystem, err return systems[0], nil } -// getChassis returns the primary chassis, preferring RackMount type. -// Servers are expected to expose exactly one chassis via Redfish. func (c *APIClient) getChassis(ctx context.Context) (*schemas.Chassis, error) { g := c.client.WithContext(ctx) if g.Service == nil { @@ -284,17 +278,14 @@ func (c *APIClient) SetChassisIdentifyLEDState(state hal.IdentifyLEDState) error } } -// SetChassisIdentifyLEDOn turns on the chassis identify LED func (c *APIClient) SetChassisIdentifyLEDOn() error { return c.setChassisIndicatorLED(schemas.LitIndicatorLED) } -// SetChassisIdentifyLEDOff turns off the chassis identify LED func (c *APIClient) SetChassisIdentifyLEDOff() error { return c.setChassisIndicatorLED(schemas.OffIndicatorLED) } -// setChassisIndicatorLED is the shared implementation for setting the chassis indicator LED state. func (c *APIClient) setChassisIndicatorLED(state schemas.IndicatorLED) error { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel()