diff --git a/connect/connect.go b/connect/connect.go index 27ba7af..7d467c1 100644 --- a/connect/connect.go +++ b/connect/connect.go @@ -5,6 +5,7 @@ import ( "time" "github.com/metal-stack/go-hal/internal/vendors/dell" + "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" @@ -39,6 +40,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: return dell.InBand(b, log) case api.VendorUnknown: @@ -70,6 +73,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: return dell.OutBand(r, b, user, password, ip, log), nil case api.VendorUnknown: 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 diff --git a/internal/redfish/redfish.go b/internal/redfish/redfish.go index 11761cb..78ed29e 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,9 +24,9 @@ type APIClient struct { urlPrefix string user string password string - basicAuth string log logger.Logger connectionTimeout time.Duration + ETagRequired bool } type bootOverrideRequest struct { @@ -65,107 +64,134 @@ func New(url, user, password string, insecure bool, log logger.Logger, connectio if err != nil { return nil, err } + return &APIClient{ client: c, 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, + ETagRequired: false, }, nil } -func (c *APIClient) BoardInfo() (*api.Board, error) { - ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) - defer cancel() +func (c *APIClient) SetETagRequired(required bool) { + c.ETagRequired = required +} + +func (c *APIClient) GetSystem() (*schemas.ComputerSystem, error) { + return c.getSystem(context.Background()) + +} + +func (c *APIClient) getSystem(ctx context.Context) (*schemas.ComputerSystem, error) { 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") + return nil, fmt.Errorf("gofish service root is not available") } - - biosVersion := "" - manufacturer := "" - model := "" - systems, err := g.Service.Systems() if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) - } - for _, system := range systems { - if system.BiosVersion != "" { - biosVersion = system.BiosVersion - break - } + return nil, fmt.Errorf("failed to query systems: %w", err) } - for _, system := range systems { - if system.Manufacturer != "" { - manufacturer = system.Manufacturer - break - } + if len(systems) == 0 { + return nil, fmt.Errorf("no system found") } - for _, system := range systems { - if system.Model != "" { - model = system.Model - break - } + if len(systems) > 1 { + c.log.Warnw("multiple systems found, using first one", "count", len(systems)) } + return systems[0], nil +} +func (c *APIClient) getChassis(ctx context.Context) (*schemas.Chassis, error) { + g := c.client.WithContext(ctx) + if g.Service == nil { + return nil, fmt.Errorf("gofish service root is not available") + } chassis, err := g.Service.Chassis() if err != nil { - c.log.Warnw("ignore system query", "error", err.Error()) + return nil, fmt.Errorf("failed to query chassis: %w", err) } 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 - } + return chass, nil + } + } + if len(chassis) > 0 { + return chassis[0], nil + } + return nil, fmt.Errorf("no chassis found") +} + +func (c *APIClient) BoardInfo() (*api.Board, error) { + ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) + defer cancel() + + 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 + } + + chass, err := c.getChassis(ctx) + if err != nil { + return nil, fmt.Errorf("no board detected: %w", err) + } + + power, err := chass.Power() + var powerMetric *api.PowerMetric + var powerSupplies []api.PowerSupply + 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 } - 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 { @@ -179,38 +205,33 @@ func toMetalLEDState(state schemas.IndicatorLED) string { } } -// MachineUUID retrieves a unique uuid for this (hardware) machine func (c *APIClient) MachineUUID() (string, 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.Errorw("error during system query, unable to detect uuid", "error", err.Error()) - return "", err + return "", fmt.Errorf("unable to detect machine UUID: %w", err) } - for _, system := range systems { - if system.UUID != "" { - return system.UUID, nil - } + if system.UUID == "" { + return "", fmt.Errorf("machine UUID is empty") } - return "", fmt.Errorf("failed to detect machine UUID") + return system.UUID, nil } func (c *APIClient) PowerState() (hal.PowerState, 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()) + c.log.Warnw("ignore system query", "error", err) + return hal.PowerUnknownState, nil } - for _, system := range systems { - if system.PowerState != "" { - return hal.GuessPowerState(string(system.PowerState)), nil - } + if system.PowerState == "" { + return hal.PowerUnknownState, nil } - return hal.PowerUnknownState, nil + return hal.GuessPowerState(string(system.PowerState)), nil } func (c *APIClient) PowerOn() error { @@ -232,17 +253,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 @@ -259,54 +278,43 @@ func (c *APIClient) SetChassisIdentifyLEDState(state hal.IdentifyLEDState) error } } -// SetChassisIdentifyLEDOn turns on the chassis identify LED func (c *APIClient) SetChassisIdentifyLEDOn() error { - payload := indicatorLEDRequest{ - IndicatorLED: schemas.LitIndicatorLED, - } - body, err := json.Marshal(payload) - if err != nil { - return err - } + return c.setChassisIndicatorLED(schemas.LitIndicatorLED) +} - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/1", c.urlPrefix), bytes.NewReader(body)) - if err != nil { - return err - } - c.addHeadersAndAuth(req) +func (c *APIClient) SetChassisIdentifyLEDOff() error { + return c.setChassisIndicatorLED(schemas.OffIndicatorLED) +} - resp, err := c.Do(req) - if err == nil { - _ = resp.Body.Close() - } +func (c *APIClient) setChassisIndicatorLED(state schemas.IndicatorLED) error { + ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) + defer cancel() + + chassis, err := c.getChassis(ctx) if err != nil { - return fmt.Errorf("unable to turn on the chassis identify LED %w", err) + return err } - return nil -} -// SetChassisIdentifyLEDOff turns off the chassis identify LED -func (c *APIClient) SetChassisIdentifyLEDOff() error { - payload := indicatorLEDRequest{ - IndicatorLED: schemas.OffIndicatorLED, - } + payload := indicatorLEDRequest{IndicatorLED: state} body, err := json.Marshal(payload) if err != nil { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Chassis/1", c.urlPrefix), bytes.NewReader(body)) + 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.Do(req) - if err == nil { - _ = resp.Body.Close() - } + 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 set chassis identify LED to %s, status code: %d", state, resp.StatusCode) } return nil } @@ -349,48 +357,38 @@ func (c *APIClient) setPersistentHDD() error { func (c *APIClient) setBootTargetOverride(payload bootOverrideRequest) error { ctx, cancel := context.WithTimeout(context.Background(), c.connectionTimeout) defer cancel() - g := c.client.WithContext(ctx) - systems, err := g.Service.Systems() - if err != nil { - return fmt.Errorf("unable to query systems: %w", err) - } - - if len(systems) == 0 { - return fmt.Errorf("no system found to set boot target") - } - if len(systems) > 1 { - c.log.Warnw("multiple systems found, ignoring all but the first one", "count", len(systems)) + system, err := c.getSystem(ctx) + if err != nil { + return err } - // Assuming there's typically one primary system. - system := systems[0] - body, err := json.Marshal(payload) if err != nil { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Systems/%s", c.urlPrefix, system.ID), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("%s/Systems/%s", c.urlPrefix, system.ID), bytes.NewReader(body)) if err != nil { return err } c.addHeadersAndAuth(req) - resp, err := c.Do(req) - _ = resp.Body.Close() + resp, err := c.doWithETag(req) if err != nil { return fmt.Errorf("unable to override boot order %w", err) } - if resp.StatusCode != http.StatusOK { + // Drain the body to ensure the connection can be reused + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("unable to override boot order, http status: %s", resp.Status) } return nil } 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.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") req.SetBasicAuth(c.user, c.password) } @@ -408,33 +406,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 } @@ -447,61 +436,48 @@ func (c *APIClient) GetBootOptions() ([]*schemas.BootOption, error) { // The curl command here would be curl -k -u : https://10.1.1.18/redfish/v1/Systems/System.Embedded.1/BootOptions 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()) - } - for _, system := range systems { - bootOptions, err := system.BootOptions() - if err != nil { - c.log.Warnw("ignore boot options query", "error", err.Error()) - continue - } - if len(bootOptions) == 0 { - c.log.Warnw("no boot options found", "error") - continue - } - if len(system.Boot.BootOrder) == 0 { - c.log.Warnw("no boot order found", "error") - continue - } - return bootOptions, nil + return nil, err } - return nil, fmt.Errorf("failed to get boot options") + bootOptions, err := system.BootOptions() + if err != nil { + return nil, fmt.Errorf("failed to get boot options: %w", err) + } + if len(bootOptions) == 0 { + return nil, fmt.Errorf("no boot options found") + } + if len(system.Boot.BootOrder) == 0 { + c.log.Warnw("no boot order found") + } + return bootOptions, nil } // 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() - g := c.client.WithContext(ctx) - systems, err := g.Service.Systems() - if err != nil { - return fmt.Errorf("unable to query systems: %w", err) - } - if len(systems) == 0 { - return fmt.Errorf("no system found to set boot order") + system, err := c.getSystem(ctx) + if err != nil { + return err } - if len(systems) > 1 { - c.log.Warnw("multiple systems found, ignoring all but the first one", "count", len(systems)) + if len(system.Boot.BootOrder) == 0 { + c.log.Errorw("no boot order found") } - // Assuming there's typically one primary system. - system := systems[0] - var bootOrder []string for _, entry := range entries { bootOrder = append(bootOrder, entry.ID) } - if len(system.Boot.BootOrder) == 0 { - c.log.Errorw("no boot order found") - } - payload := bootOrderSetRequest{} payload.Boot.BootOrder = bootOrder @@ -509,20 +485,21 @@ func (c *APIClient) SetBootOrder(entries []*schemas.BootOption) error { if err != nil { return err } - req, err := http.NewRequestWithContext(context.Background(), http.MethodPatch, fmt.Sprintf("%s/Systems/%s", c.urlPrefix, system.ID), bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, http.MethodPatch, fmt.Sprintf("%s/Systems/%s", c.urlPrefix, system.ID), bytes.NewReader(body)) if err != nil { return err } c.addHeadersAndAuth(req) - resp, err := c.Do(req) - _ = resp.Body.Close() + + resp, err := c.doWithETag(req) if err != nil { return fmt.Errorf("unable to set boot order: %w", err) } - if resp.StatusCode != http.StatusOK { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("unable to set boot order, http status: %s", resp.Status) } - return nil } @@ -549,23 +526,70 @@ 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) } defer func() { - err = resp.Body.Close() - if err != nil { - c.log.Warnw("unable to close response body", "error", err) + if closeErr := resp.Body.Close(); closeErr != nil { + c.log.Warnw("unable to close response body", "error", closeErr) } }() body, _ = io.ReadAll(resp.Body) // The response code is 202 for accepted, and we normally get no body - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - c.log.Infow("Update triggered successfully: %s\n", string(body)) - } else { - return fmt.Errorf("update failed with status %d: %s", resp.StatusCode, string(body)) + if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK { + return fmt.Errorf("update failed with status %s: %s", resp.Status, string(body)) } + c.log.Infow("update triggered successfully", "response", string(body)) return nil } + +func (c *APIClient) getETag(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + c.addHeadersAndAuth(req) + + resp, err := c.Do(req) + if err != nil { + return "", err + } + // Drain and close the body to ensure the connection can be reused + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + 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) { + 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() + } + + 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 new file mode 100644 index 0000000..6d23e68 --- /dev/null +++ b/internal/vendors/fujitsu/fujitsu.go @@ -0,0 +1,338 @@ +package fujitsu + +import ( + "fmt" + "strconv" + + "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 { + r.SetETagRequired(true) + return &outBand{ + OutBand: outband.ViaRedfish(r, board), + } +} + +// InBand +func (ib *inBand) PowerOff() error { + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerDown) +} + +func (ib *inBand) PowerCycle() error { + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlPowerCycle) +} + +func (ib *inBand) PowerReset() error { + return ib.IpmiTool.SetChassisControl(ipmi.ChassisControlHardReset) +} + +func (ib *inBand) IdentifyLEDState(state hal.IdentifyLEDState) error { + return ib.IpmiTool.SetChassisIdentifyLEDState(state) +} + +func (ib *inBand) IdentifyLEDOn() error { + return ib.IpmiTool.SetChassisIdentifyLEDOn() +} + +func (ib *inBand) IdentifyLEDOff() error { + return ib.IpmiTool.SetChassisIdentifyLEDOff() +} + +func (ib *inBand) BootFrom(bootTarget hal.BootTarget) error { + 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 c.IpmiTool.BMC() +} + +func (c *bmcConnection) PresentSuperUser() api.BMCUser { + return api.BMCUser{ + Name: "USERID", + Id: "2", + ChannelNumber: 2, + } +} + +func (c *bmcConnection) SuperUser() api.BMCUser { + return api.BMCUser{ + Name: "root", + Id: "4", + ChannelNumber: 2, + } +} + +func (c *bmcConnection) User() api.BMCUser { + return api.BMCUser{ + Name: "metal", + Id: "3", + ChannelNumber: 2, + } +} + +func (c *bmcConnection) Present() bool { + return c.IpmiTool.DevicePresent() +} + +func (c *bmcConnection) CreateUserAndPassword(user api.BMCUser, privilege api.IpmiPrivilege) (string, error) { + password_constraints := c.Board().Vendor.PasswordConstraints() + password_constraints.Length = 12 + 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) + if err != nil { + return err + } + return c.syncRedfishPermissions(user, privilege) +} + +func (c *bmcConnection) NeedsPasswordChange(user api.BMCUser, password string) (bool, error) { + return c.IpmiTool.NeedsPasswordChange(user, password) +} + +func (c *bmcConnection) ChangePassword(user api.BMCUser, newPassword string) error { + return c.IpmiTool.ChangePassword(user, newPassword, ipmi.HighLevel) +} + +func (c *bmcConnection) SetUserEnabled(user api.BMCUser, enabled bool) error { + err := c.IpmiTool.SetUserEnabled(user, enabled, ipmi.HighLevel) + if err != nil { + return err + } + privilege := api.UserPrivilege + if !enabled { + privilege = api.NoAccessPrivilege + } + return c.syncRedfishPermissions(user, privilege) +} + +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/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 0x80 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) { + // 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 ob.Redfish.PowerState() +} + +func (ob *outBand) PowerOff() error { + return ob.Redfish.PowerOff() +} + +func (ob *outBand) PowerOn() error { + return ob.Redfish.PowerOn() +} + +func (ob *outBand) PowerReset() error { + return ob.Redfish.PowerReset() +} + +func (ob *outBand) PowerCycle() error { + 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 { + return ob.Redfish.SetChassisIdentifyLEDState(state) +} + +func (ob *outBand) IdentifyLEDOn() error { + return ob.Redfish.SetChassisIdentifyLEDOn() +} + +func (ob *outBand) IdentifyLEDOff() error { + return ob.Redfish.SetChassisIdentifyLEDOff() +} + +func (ob *outBand) BootFrom(target hal.BootTarget) error { + // On Fujitsu for BootSourceOverrideTarget = "BiosSetup" BootSourceOverrideEnabled is restricted to "Once" + return ob.Redfish.SetBootTarget(target) +} + +func (ob *outBand) Describe() string { + return "OutBand connected to Fujitsu" +} + +func (ob *outBand) Console(s ssh.Session) error { + return ob.IpmiTool.OpenConsole(s) +} + +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] }