diff --git a/.github/workflows/push-pr-lint.yaml b/.github/workflows/push-pr-lint.yaml index 1f89a53..9bc2efe 100644 --- a/.github/workflows/push-pr-lint.yaml +++ b/.github/workflows/push-pr-lint.yaml @@ -7,12 +7,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Install Go uses: actions/setup-go@v5 with: go-version-file: go.mod - - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: @@ -26,13 +24,13 @@ jobs: runs-on: ubuntu-latest needs: [lint-test] steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v5 with: - go-version: "1.22" - - - name: Checkout code - uses: actions/checkout@v4 + go-version-file: go.mod - name: build binary run: make build-linux diff --git a/.golangci.yml b/.golangci.yml index 50b63bf..30b323c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,8 +5,7 @@ service: linters-settings: govet: - enable: - - fieldalignment + enable-all: true auto-fix: true shadow: true settings: @@ -79,10 +78,9 @@ linters: issues: exclude-dirs: - internal/fixtures - + - internal/model/mock exclude-files: - "(.*/)*.*_test.go" - exclude-rules: - linters: - gosec diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..7aeedcc --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,10 @@ +with-expecter: true +packages: + github.com/metal-toolbox/flipflop/internal/model: + config: + dir: "internal/model/mock" + fileName: "mock_{{.InterfaceName}}.go" + interfaces: + BMCBootMonitor: + UpdateFn: + DelayFn: diff --git a/go.mod b/go.mod index 49c0fca..70603eb 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,12 @@ toolchain go1.23.1 require ( github.com/banzaicloud/logrus-runtime-formatter v0.0.0-20190729070250-5ae5475bae5e + github.com/bmc-toolbox/bmclib/v2 v2.3.4 + github.com/bmc-toolbox/common v0.0.0-20241031162543-6b96e5981a0d github.com/bombsimon/logrusr/v4 v4.1.0 github.com/coreos/go-oidc/v3 v3.12.0 github.com/equinix-labs/otel-init-go v0.0.9 + github.com/go-logr/logr v1.4.2 github.com/google/uuid v1.6.0 github.com/hashicorp/go-retryablehttp v0.7.7 github.com/jeremywohl/flatten v1.0.1 @@ -23,6 +26,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + github.com/stmcginnis/gofish v0.20.0 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 go.opentelemetry.io/otel v1.34.0 @@ -55,7 +59,6 @@ require ( github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -115,7 +118,6 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/stmcginnis/gofish v0.20.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index 7417f0f..5b24ac9 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmc-toolbox/bmclib/v2 v2.3.4 h1:ihCC9jH8g5Racg4zy+lWpjt7vRt2aWzit3LEL1DiS7k= +github.com/bmc-toolbox/bmclib/v2 v2.3.4/go.mod h1:t8If/0fHQTRIK/yKDk2H3SgthDNNj+7z2aeftDFRFrU= +github.com/bmc-toolbox/common v0.0.0-20241031162543-6b96e5981a0d h1:dMmFDAAEpXizInaNwPSa5LM6tX/xDIPKjL6v9jYfMxo= +github.com/bmc-toolbox/common v0.0.0-20241031162543-6b96e5981a0d/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM= github.com/bombsimon/logrusr/v2 v2.0.1/go.mod h1:ByVAX+vHdLGAfdroiMg6q0zgq2FODY2lc5YJvzmOJio= github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= @@ -559,8 +563,6 @@ github.com/metal-toolbox/ctrl v1.1.1 h1:TiPm0mIzmCQlJYZJpMr7VmoP44y2BATCRrlbLwJE github.com/metal-toolbox/ctrl v1.1.1/go.mod h1:CUGY/jPGUEO4rKwPfEbFHZ2oZ0Yy5jsytkUyrWfJGpU= github.com/metal-toolbox/fleetdb v1.20.1 h1:5YHnG8hFpzmRz7Ulyqs9tWPAIfJdMrUXcuWDWk/g88k= github.com/metal-toolbox/fleetdb v1.20.1/go.mod h1:8yVOYkEbxNRAvHx02g7bP6oBDiWBvhQ9HY95zI9ZtTU= -github.com/metal-toolbox/rivets/v2 v2.0.0 h1:whXM3WLGpz1veXboXE3U/ZwwqN1mNsZ0/ElVYLoXAro= -github.com/metal-toolbox/rivets/v2 v2.0.0/go.mod h1:MzV7K//lajUWvJ0KUoGi9ctpkJ16sQOBEC6apohJd58= github.com/metal-toolbox/rivets/v2 v2.1.0 h1:3T1pPRJmX6JBZ9f4VuYZeFVcP23E35FeQaRI9eiSQCg= github.com/metal-toolbox/rivets/v2 v2.1.0/go.mod h1:LaWiaPqIhxoC9At/hSkcUUfipfiSrMBBum5e7eENR+4= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= @@ -595,16 +597,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt/v2 v2.7.2 h1:SCRjfDLJ2q8naXp8YlGJJS5/yj3wGSODFYVi4nnwVMw= -github.com/nats-io/jwt/v2 v2.7.2/go.mod h1:kB6QUmqHG6Wdrzj0KP2L+OX4xiTPBeV+NHVstFaATXU= -github.com/nats-io/nats-server/v2 v2.10.22 h1:Yt63BGu2c3DdMoBZNcR6pjGQwk/asrKU7VX846ibxDA= -github.com/nats-io/nats-server/v2 v2.10.22/go.mod h1:X/m1ye9NYansUXYFrbcDwUi/blHkrgHh2rgCJaakonk= -github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= -github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE= +github.com/nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4= +github.com/nats-io/nats-server/v2 v2.10.25 h1:J0GWLDDXo5HId7ti/lTmBfs+lzhmu8RPkoKl0eSCqwc= +github.com/nats-io/nats-server/v2 v2.10.25/go.mod h1:/YYYQO7cuoOBt+A7/8cVjuhWTaTUEAlZbJT+3sMAfFU= github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA= github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw= -github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= -github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -705,7 +703,6 @@ github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -796,8 +793,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= @@ -1099,8 +1094,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/flipflop/handler.go b/internal/flipflop/handler.go index adda134..4b073f1 100644 --- a/internal/flipflop/handler.go +++ b/internal/flipflop/handler.go @@ -2,14 +2,17 @@ package flipflop import ( "context" + "fmt" "strings" "time" + "github.com/bmc-toolbox/common" ctrl "github.com/metal-toolbox/ctrl" rctypes "github.com/metal-toolbox/rivets/v2/condition" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" + "github.com/stmcginnis/gofish/redfish" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -32,6 +35,29 @@ type ConditionTaskHandler struct { controllerID string } +var ( + ErrValidationUnsupported = errors.New("firmware validation is unsupported on this vendor") +) + +// return a live session to the BMC (or an error). The caller is responsible for closing the connection +func (cth *ConditionTaskHandler) openBMCConnection(ctx context.Context) error { + var bmc device.Queryor + if cth.cfg.Dryrun { // Fake BMC + bmc = device.NewDryRunBMCClient(cth.server) + cth.logger.Warn("using fake BMC") + } else { + bmc = device.NewBMCClient(cth.server, cth.logger) + } + + err := bmc.Open(ctx) + if err != nil { + cth.logger.WithError(err).Error("bmc: failed to connect") + return err + } + cth.bmc = bmc + return nil +} + func (cth *ConditionTaskHandler) HandleTask(ctx context.Context, genTask *rctypes.Task[any, any], publisher ctrl.Publisher) error { ctx, span := otel.Tracer(pkgName).Start( ctx, @@ -79,25 +105,15 @@ func (cth *ConditionTaskHandler) HandleTask(ctx context.Context, genTask *rctype ) cth.logger = loggerEntry - var bmc device.Queryor - if cth.cfg.Dryrun { // Fake BMC - bmc = device.NewDryRunBMCClient(server) - loggerEntry.Warn("Running BMC Device in Dryrun mode") - } else { - bmc = device.NewBMCClient(server, loggerEntry) - } - - err = bmc.Open(ctx) - if err != nil { - loggerEntry.WithError(err).Error("bmc connection failed to connect") + if err := cth.openBMCConnection(ctx); err != nil { return err } + defer func() { - if err := bmc.Close(ctx); err != nil { + if err := cth.bmc.Close(ctx); err != nil { loggerEntry.WithError(err).Error("bmc connection close error") } }() - cth.bmc = bmc return cth.Run(ctx) } @@ -216,73 +232,141 @@ func (cth *ConditionTaskHandler) setNextBootDevice(ctx context.Context, bootDevi return cth.successful(ctx, "next boot device set successfully: "+bootDevice) } +type statusUpdate func(string) + +type delayFunc func(context.Context) error + func (cth *ConditionTaskHandler) validateFirmware(ctx context.Context) error { cth.logger.Info("starting firmware validation") - deadline := time.Now().Add(cth.task.Parameters.ValidateFirmwareTimeout) - - // First reboot the BMC to ensure it's running the desired firmware - if err := cth.bmc.PowerCycleBMC(ctx); err != nil { - return cth.failedWithError(ctx, "failed to power cycle BMC", err) + // confirm server vendor + if !strings.EqualFold(cth.server.Vendor, common.VendorSupermicro) { + cth.logger.WithField("vendor", cth.server.Vendor).Warn("unsupported vendor for firmware validation") + return cth.failedWithError(ctx, "", fmt.Errorf("%w : %s", ErrValidationUnsupported, cth.server.Vendor)) } - var err error + // get the correct handle to the BMC, we let bmclib deal with the differences between X11/X12/X13. + handle := newSMCValidationHandle(cth.server) - // Next we want to cycle the host, but the BMC will take some - // time to reboot, so retry once every 30 seconds up to our - // timeout deadline (ideally we'd have a way to distinguish - // failures that are due to the BMC not being back online yet - // from ones that aren't going to be resolved by waiting and - // retrying...) - for time.Now().Before(deadline) { - if errDelay := sleepInContext(ctx, 30*time.Second); errDelay != nil { - return cth.failedWithError(ctx, "failed to cycle host power after BMC power cycle", errDelay) + // updateFn publishes status messages back to the KV. Because these are status messages, they are + // advisory. We don't care too much if we miss a status update provided it's not the last one. + updateFn := func(payload string) { + err := cth.publishActive(ctx, payload) + if err != nil { + cth.logger. + WithError(err). + WithField("payload", payload). + Warn("updating condition status") } + } - err = cth.bmc.SetPowerState(ctx, "cycle") - if err == nil { - break + waitForBMC := func(ctx context.Context) error { + var err error + select { + case <-time.After(30 * time.Second): + case <-ctx.Done(): + err = ctx.Err() } + return err } - if err != nil { - return cth.failedWithError(ctx, "failed to cycle host power after BMC power cycle", err) + deadlineCtx, cancel := context.WithTimeout(ctx, cth.task.Parameters.ValidateFirmwareTimeout) + validateErr := validateFirmwareInternal(deadlineCtx, handle, updateFn, waitForBMC) + cancel() + + if clErr := handle.Close(context.Background()); clErr != nil { + cth.logger. + WithError(clErr). + Warn("closing bmc handle") + } + + if validateErr != nil { + cth.logger.WithError(validateErr).Warn("error validating firmware") + return cth.failed(ctx, validateErr.Error()) } - // Finally, wait for the host to boot successfully - for time.Now().Before(deadline) { - // sleep before checking to (hopefully) avoid seeing a - // stale POST code from a previous boot before the - // power-cycle has actually started happening - if errDelay := sleepInContext(ctx, 30*time.Second); errDelay != nil { - return cth.failedWithError(ctx, "failed to retrieve host boot status", errDelay) + done := time.Now() + srvID := cth.task.Parameters.AssetID + fwID := cth.task.Parameters.ValidateFirmwareID + if dbErr := cth.store.ValidateFirmwareSet(ctx, srvID, fwID, done); dbErr != nil { + return cth.failedWithError(ctx, "marking firmware set validated", dbErr) + } + return cth.successful(ctx, "firmware set validated: "+fwID.String()) +} + +// XXX: It is incumbent on the caller to close the BMC handle. +// +//nolint:gocyclo // yeah, I know +func validateFirmwareInternal(ctx context.Context, mon model.BMCBootMonitor, update statusUpdate, delay delayFunc) error { + if err := mon.Open(ctx); err != nil { + return fmt.Errorf("opening bmc connection: %w", err) + } + + // First reset the BMC to ensure it's running the desired firmware + if _, err := mon.BmcReset(ctx, string(redfish.PowerCycleResetType)); err != nil { + return fmt.Errorf("doing bmc reset: %w", err) + } + + update("bmc power cycle sent") + _ = mon.Close(ctx) + + // Next we want to cycle the host, but the BMC will take some time to reboot + bmcConnected := false + for !bmcConnected { + if err := delay(ctx); err != nil { + return fmt.Errorf("context error: %w", err) } - booted, err := cth.bmc.HostBooted(ctx) + if err := mon.Open(ctx); err != nil { + payload := fmt.Sprintf("failed to connect to bmc: %s", err.Error()) + update(payload) + continue + } + bmcConnected = true + update("bmc connection re-established") + } + + bmcPowerStateSet := false + for !bmcPowerStateSet { + if err := delay(ctx); err != nil { + return fmt.Errorf("context error: %w", err) + } + + currentState, err := mon.PowerStateGet(ctx) if err != nil { - return cth.failedWithError(ctx, "failed to retrieve host boot status", err) + payload := fmt.Sprintf("getting bmc power state: %s", err.Error()) + update(payload) + continue } - if booted { - done := time.Now() - srvID := cth.task.Parameters.AssetID - fwID := cth.task.Parameters.ValidateFirmwareID - if dbErr := cth.store.ValidateFirmwareSet(ctx, srvID, fwID, done); dbErr != nil { - return cth.failedWithError(ctx, "marking firmware set validated", dbErr) - } - return cth.successful(ctx, "firmware set validated: "+fwID.String()) + + newDeviceState := "cycle" + if strings.Contains(strings.ToLower(currentState), "off") { + newDeviceState = "on" } - } - return cth.failed(ctx, "host failed to boot successfully before deadline") -} + if _, err := mon.PowerSet(ctx, newDeviceState); err != nil { + update(fmt.Sprintf("bmc set power state to %s: %s", newDeviceState, err.Error())) + continue + } + bmcPowerStateSet = true + update(fmt.Sprintf("device power state set to %s", newDeviceState)) + } -func sleepInContext(ctx context.Context, t time.Duration) error { - select { - case <-time.After(t): - return nil - case <-ctx.Done(): - return context.Canceled + hostBooted := false + var err error + for !hostBooted { + // now we've reset the server, give it a chance to come back + if err = delay(ctx); err != nil { + return fmt.Errorf("context error: %w", err) + } + hostBooted, err = mon.BootComplete() + if err != nil { + update(fmt.Sprintf("checking host boot state: %s", err.Error())) + continue + } + update("host boot complete") } + return nil } // pxeBootPersistent sets up the server to pxe boot persistently @@ -294,6 +378,16 @@ func (cth *ConditionTaskHandler) pxeBootPersistent(ctx context.Context) error { return cth.bmc.SetPowerState(ctx, "on") } +func sleepInContext(ctx context.Context, td time.Duration) error { + var err error + select { + case <-time.After(td): + case <-ctx.Done(): + err = ctx.Err() + } + return err +} + func (cth *ConditionTaskHandler) publish(ctx context.Context, status string, state rctypes.State) error { cth.task.State = state cth.task.Status.Append(status) diff --git a/internal/flipflop/handler_test.go b/internal/flipflop/handler_test.go new file mode 100644 index 0000000..9292ca7 --- /dev/null +++ b/internal/flipflop/handler_test.go @@ -0,0 +1,232 @@ +package flipflop + +import ( + "context" + "errors" + "testing" + + "github.com/stmcginnis/gofish/redfish" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + ffmock "github.com/metal-toolbox/flipflop/internal/model/mock" +) + +// test the internal parts of a firmware validation task +func TestValidateFirmwareInternal(t *testing.T) { + t.Parallel() + t.Run("fail to open monitor", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := func(_ context.Context) error { + return nil + } + update := func(_ string) {} + + mon.EXPECT().Open( + mock.IsType(context.TODO()), + ).Return(errors.New("pound sand")).Times(1) + err := validateFirmwareInternal(context.TODO(), mon, update, delay) + require.Error(t, err, "expected error on Open") + }) + t.Run("fail to do BmcReset", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := func(_ context.Context) error { + return nil + } + update := func(_ string) {} + + mon.EXPECT().Open( + mock.IsType(context.TODO()), + ).Return(nil).Times(1) + + mon.EXPECT().BmcReset( + mock.IsType(context.TODO()), + string(redfish.PowerCycleResetType), + ).Return(false, errors.New("pound sand")).Times(1) + err := validateFirmwareInternal(context.TODO(), mon, update, delay) + require.Error(t, err, "expected error on Open") + }) + t.Run("timeout reconnecting to bmc", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := func(_ context.Context) error { + return context.Canceled + } + update := func(_ string) {} + + mon.EXPECT().Open( + mock.IsType(context.TODO()), + ).Return(nil).Times(1) + + mon.EXPECT().Close( + mock.IsType(context.TODO()), + ).Return(nil).Times(1) + + mon.EXPECT().BmcReset( + mock.IsType(context.TODO()), + string(redfish.PowerCycleResetType), + ).Return(true, nil).Times(1) + err := validateFirmwareInternal(context.TODO(), mon, update, delay) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("bmc reopen timeout", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(context.Canceled).Times(1) + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(1) + mon.On("Open", mock.IsType(context.TODO())).Return(errors.New("pound sand")).Times(1) + + mon.EXPECT().Close( + mock.IsType(context.TODO()), + ).Return(nil).Times(1) + + mon.EXPECT().BmcReset( + mock.IsType(context.TODO()), + string(redfish.PowerCycleResetType), + ).Return(true, nil).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "failed to connect to bmc: pound sand").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("timeout on get power-state", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + // the scenario: open the connection to the BMC + // do a BMC reset + // close/reopen + // get the power state and fail + // timeout + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(2) + mon.On("BmcReset", mock.IsType(context.TODO()), string(redfish.PowerCycleResetType)).Return(true, nil).Times(1) + mon.On("Close", mock.IsType(context.TODO())).Return(nil).Times(1) + + mon.On("PowerStateGet", mock.IsType(context.TODO())).Return("", errors.New("pound sand")).Times(1) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(context.Canceled).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "bmc connection re-established").Return().Times(1) + update.On("Execute", "getting bmc power state: pound sand").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("timeout on power set -- power cycle", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(2) + mon.On("BmcReset", mock.IsType(context.TODO()), string(redfish.PowerCycleResetType)).Return(true, nil).Times(1) + mon.On("Close", mock.IsType(context.TODO())).Return(nil).Times(1) + + mon.On("PowerStateGet", mock.IsType(context.TODO())).Return("on", nil).Times(1) + mon.On("PowerSet", mock.IsType(context.TODO()), "cycle").Return(false, errors.New("pound sand")).Times(1) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(context.Canceled).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "bmc connection re-established").Return().Times(1) + update.On("Execute", "bmc set power state to cycle: pound sand").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("timeout on power set -- power on", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(2) + mon.On("BmcReset", mock.IsType(context.TODO()), string(redfish.PowerCycleResetType)).Return(true, nil).Times(1) + mon.On("Close", mock.IsType(context.TODO())).Return(nil).Times(1) + + mon.On("PowerStateGet", mock.IsType(context.TODO())).Return("off", nil).Times(1) + mon.On("PowerSet", mock.IsType(context.TODO()), "on").Return(false, errors.New("pound sand")).Times(1) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(context.Canceled).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "bmc connection re-established").Return().Times(1) + update.On("Execute", "bmc set power state to on: pound sand").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("timeout on boot complete", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(2) + mon.On("BmcReset", mock.IsType(context.TODO()), string(redfish.PowerCycleResetType)).Return(true, nil).Times(1) + mon.On("Close", mock.IsType(context.TODO())).Return(nil).Times(1) + + mon.On("PowerStateGet", mock.IsType(context.TODO())).Return("off", nil).Times(1) + mon.On("PowerSet", mock.IsType(context.TODO()), "on").Return(true, nil).Times(1) + mon.On("BootComplete").Return(false, errors.New("pound sand")).Times(1) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(context.Canceled).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "bmc connection re-established").Return().Times(1) + update.On("Execute", "device power state set to on").Return().Times(1) + update.On("Execute", "checking host boot state: pound sand").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("complete validation", func(t *testing.T) { + t.Parallel() + mon := ffmock.NewMockBMCBootMonitor(t) + delay := ffmock.NewMockDelayFn(t) + update := ffmock.NewMockUpdateFn(t) + + mon.On("Open", mock.IsType(context.TODO())).Return(nil).Times(2) + mon.On("BmcReset", mock.IsType(context.TODO()), string(redfish.PowerCycleResetType)).Return(true, nil).Times(1) + mon.On("Close", mock.IsType(context.TODO())).Return(nil).Times(1) + + mon.On("PowerStateGet", mock.IsType(context.TODO())).Return("off", nil).Times(1) + mon.On("PowerSet", mock.IsType(context.TODO()), "on").Return(true, nil).Times(1) + mon.On("BootComplete").Return(true, nil).Times(1) + + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + delay.On("Execute", mock.IsType(context.TODO())).Return(nil).Times(1) + + update.On("Execute", "bmc power cycle sent").Return().Times(1) + update.On("Execute", "bmc connection re-established").Return().Times(1) + update.On("Execute", "device power state set to on").Return().Times(1) + update.On("Execute", "host boot complete").Return().Times(1) + + err := validateFirmwareInternal(context.TODO(), mon, update.Execute, delay.Execute) + require.NoError(t, err) + }) + +} diff --git a/internal/flipflop/validation_client.go b/internal/flipflop/validation_client.go new file mode 100644 index 0000000..c982787 --- /dev/null +++ b/internal/flipflop/validation_client.go @@ -0,0 +1,64 @@ +package flipflop + +/* These functions exist to provide a little nicer client experience around using raw bmclib + types without going through the hassle of trying to get the default client of bmclib to + play nice. */ + +import ( + "crypto/tls" + "net" + "net/http" + "net/http/cookiejar" + "time" + + "github.com/bmc-toolbox/bmclib/v2/providers/supermicro" + "github.com/go-logr/logr" + "github.com/metal-toolbox/flipflop/internal/model" + "golang.org/x/net/publicsuffix" +) + +func defaultBMCTransport() *http.Transport { + return &http.Transport{ + //nolint:gosec // BMCs use self-signed certs + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DisableKeepAlives: true, + Dial: (&net.Dialer{ + Timeout: 120 * time.Second, + KeepAlive: 120 * time.Second, + }).Dial, + TLSHandshakeTimeout: 120 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + } +} + +func newHTTPClient(opts ...func(*http.Client)) *http.Client { + // we ignore the error here because cookiejar.New always returns nil + jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + + client := &http.Client{ + Timeout: time.Second * 120, + Transport: defaultBMCTransport(), + Jar: jar, + } + + for _, opt := range opts { + if opt != nil { + opt(client) + } + } + + return client +} + +func newSMCValidationHandle(srv *model.Asset) model.BMCBootMonitor { + httpClient := newHTTPClient() + hdl := supermicro.NewClient( + srv.BmcAddress.String(), + srv.BmcUsername, + srv.BmcPassword, + logr.Discard(), + supermicro.WithHttpClient(httpClient), + ) + + return hdl +} diff --git a/internal/model/mock/mock_BMCBootMonitor.go b/internal/model/mock/mock_BMCBootMonitor.go new file mode 100644 index 0000000..a1f399a --- /dev/null +++ b/internal/model/mock/mock_BMCBootMonitor.go @@ -0,0 +1,412 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package model + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + redfish "github.com/stmcginnis/gofish/redfish" +) + +// MockBMCBootMonitor is an autogenerated mock type for the BMCBootMonitor type +type MockBMCBootMonitor struct { + mock.Mock +} + +type MockBMCBootMonitor_Expecter struct { + mock *mock.Mock +} + +func (_m *MockBMCBootMonitor) EXPECT() *MockBMCBootMonitor_Expecter { + return &MockBMCBootMonitor_Expecter{mock: &_m.Mock} +} + +// BmcReset provides a mock function with given fields: _a0, _a1 +func (_m *MockBMCBootMonitor) BmcReset(_a0 context.Context, _a1 string) (bool, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for BmcReset") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBMCBootMonitor_BmcReset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BmcReset' +type MockBMCBootMonitor_BmcReset_Call struct { + *mock.Call +} + +// BmcReset is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *MockBMCBootMonitor_Expecter) BmcReset(_a0 interface{}, _a1 interface{}) *MockBMCBootMonitor_BmcReset_Call { + return &MockBMCBootMonitor_BmcReset_Call{Call: _e.mock.On("BmcReset", _a0, _a1)} +} + +func (_c *MockBMCBootMonitor_BmcReset_Call) Run(run func(_a0 context.Context, _a1 string)) *MockBMCBootMonitor_BmcReset_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockBMCBootMonitor_BmcReset_Call) Return(_a0 bool, _a1 error) *MockBMCBootMonitor_BmcReset_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBMCBootMonitor_BmcReset_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockBMCBootMonitor_BmcReset_Call { + _c.Call.Return(run) + return _c +} + +// BootComplete provides a mock function with given fields: +func (_m *MockBMCBootMonitor) BootComplete() (bool, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for BootComplete") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func() (bool, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBMCBootMonitor_BootComplete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BootComplete' +type MockBMCBootMonitor_BootComplete_Call struct { + *mock.Call +} + +// BootComplete is a helper method to define mock.On call +func (_e *MockBMCBootMonitor_Expecter) BootComplete() *MockBMCBootMonitor_BootComplete_Call { + return &MockBMCBootMonitor_BootComplete_Call{Call: _e.mock.On("BootComplete")} +} + +func (_c *MockBMCBootMonitor_BootComplete_Call) Run(run func()) *MockBMCBootMonitor_BootComplete_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockBMCBootMonitor_BootComplete_Call) Return(_a0 bool, _a1 error) *MockBMCBootMonitor_BootComplete_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBMCBootMonitor_BootComplete_Call) RunAndReturn(run func() (bool, error)) *MockBMCBootMonitor_BootComplete_Call { + _c.Call.Return(run) + return _c +} + +// Close provides a mock function with given fields: _a0 +func (_m *MockBMCBootMonitor) Close(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockBMCBootMonitor_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockBMCBootMonitor_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockBMCBootMonitor_Expecter) Close(_a0 interface{}) *MockBMCBootMonitor_Close_Call { + return &MockBMCBootMonitor_Close_Call{Call: _e.mock.On("Close", _a0)} +} + +func (_c *MockBMCBootMonitor_Close_Call) Run(run func(_a0 context.Context)) *MockBMCBootMonitor_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockBMCBootMonitor_Close_Call) Return(_a0 error) *MockBMCBootMonitor_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockBMCBootMonitor_Close_Call) RunAndReturn(run func(context.Context) error) *MockBMCBootMonitor_Close_Call { + _c.Call.Return(run) + return _c +} + +// GetBootProgress provides a mock function with given fields: +func (_m *MockBMCBootMonitor) GetBootProgress() (*redfish.BootProgress, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetBootProgress") + } + + var r0 *redfish.BootProgress + var r1 error + if rf, ok := ret.Get(0).(func() (*redfish.BootProgress, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *redfish.BootProgress); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redfish.BootProgress) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBMCBootMonitor_GetBootProgress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBootProgress' +type MockBMCBootMonitor_GetBootProgress_Call struct { + *mock.Call +} + +// GetBootProgress is a helper method to define mock.On call +func (_e *MockBMCBootMonitor_Expecter) GetBootProgress() *MockBMCBootMonitor_GetBootProgress_Call { + return &MockBMCBootMonitor_GetBootProgress_Call{Call: _e.mock.On("GetBootProgress")} +} + +func (_c *MockBMCBootMonitor_GetBootProgress_Call) Run(run func()) *MockBMCBootMonitor_GetBootProgress_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockBMCBootMonitor_GetBootProgress_Call) Return(_a0 *redfish.BootProgress, _a1 error) *MockBMCBootMonitor_GetBootProgress_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBMCBootMonitor_GetBootProgress_Call) RunAndReturn(run func() (*redfish.BootProgress, error)) *MockBMCBootMonitor_GetBootProgress_Call { + _c.Call.Return(run) + return _c +} + +// Open provides a mock function with given fields: _a0 +func (_m *MockBMCBootMonitor) Open(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Open") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockBMCBootMonitor_Open_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Open' +type MockBMCBootMonitor_Open_Call struct { + *mock.Call +} + +// Open is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockBMCBootMonitor_Expecter) Open(_a0 interface{}) *MockBMCBootMonitor_Open_Call { + return &MockBMCBootMonitor_Open_Call{Call: _e.mock.On("Open", _a0)} +} + +func (_c *MockBMCBootMonitor_Open_Call) Run(run func(_a0 context.Context)) *MockBMCBootMonitor_Open_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockBMCBootMonitor_Open_Call) Return(_a0 error) *MockBMCBootMonitor_Open_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockBMCBootMonitor_Open_Call) RunAndReturn(run func(context.Context) error) *MockBMCBootMonitor_Open_Call { + _c.Call.Return(run) + return _c +} + +// PowerSet provides a mock function with given fields: _a0, _a1 +func (_m *MockBMCBootMonitor) PowerSet(_a0 context.Context, _a1 string) (bool, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for PowerSet") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBMCBootMonitor_PowerSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerSet' +type MockBMCBootMonitor_PowerSet_Call struct { + *mock.Call +} + +// PowerSet is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 string +func (_e *MockBMCBootMonitor_Expecter) PowerSet(_a0 interface{}, _a1 interface{}) *MockBMCBootMonitor_PowerSet_Call { + return &MockBMCBootMonitor_PowerSet_Call{Call: _e.mock.On("PowerSet", _a0, _a1)} +} + +func (_c *MockBMCBootMonitor_PowerSet_Call) Run(run func(_a0 context.Context, _a1 string)) *MockBMCBootMonitor_PowerSet_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockBMCBootMonitor_PowerSet_Call) Return(_a0 bool, _a1 error) *MockBMCBootMonitor_PowerSet_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBMCBootMonitor_PowerSet_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockBMCBootMonitor_PowerSet_Call { + _c.Call.Return(run) + return _c +} + +// PowerStateGet provides a mock function with given fields: _a0 +func (_m *MockBMCBootMonitor) PowerStateGet(_a0 context.Context) (string, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for PowerStateGet") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBMCBootMonitor_PowerStateGet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PowerStateGet' +type MockBMCBootMonitor_PowerStateGet_Call struct { + *mock.Call +} + +// PowerStateGet is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockBMCBootMonitor_Expecter) PowerStateGet(_a0 interface{}) *MockBMCBootMonitor_PowerStateGet_Call { + return &MockBMCBootMonitor_PowerStateGet_Call{Call: _e.mock.On("PowerStateGet", _a0)} +} + +func (_c *MockBMCBootMonitor_PowerStateGet_Call) Run(run func(_a0 context.Context)) *MockBMCBootMonitor_PowerStateGet_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockBMCBootMonitor_PowerStateGet_Call) Return(_a0 string, _a1 error) *MockBMCBootMonitor_PowerStateGet_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBMCBootMonitor_PowerStateGet_Call) RunAndReturn(run func(context.Context) (string, error)) *MockBMCBootMonitor_PowerStateGet_Call { + _c.Call.Return(run) + return _c +} + +// NewMockBMCBootMonitor creates a new instance of MockBMCBootMonitor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBMCBootMonitor(t interface { + mock.TestingT + Cleanup(func()) +}) *MockBMCBootMonitor { + mock := &MockBMCBootMonitor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/model/mock/mock_DelayFn.go b/internal/model/mock/mock_DelayFn.go new file mode 100644 index 0000000..6ce3f74 --- /dev/null +++ b/internal/model/mock/mock_DelayFn.go @@ -0,0 +1,82 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package model + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockDelayFn is an autogenerated mock type for the DelayFn type +type MockDelayFn struct { + mock.Mock +} + +type MockDelayFn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDelayFn) EXPECT() *MockDelayFn_Expecter { + return &MockDelayFn_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0 +func (_m *MockDelayFn) Execute(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Execute") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockDelayFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MockDelayFn_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockDelayFn_Expecter) Execute(_a0 interface{}) *MockDelayFn_Execute_Call { + return &MockDelayFn_Execute_Call{Call: _e.mock.On("Execute", _a0)} +} + +func (_c *MockDelayFn_Execute_Call) Run(run func(_a0 context.Context)) *MockDelayFn_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockDelayFn_Execute_Call) Return(_a0 error) *MockDelayFn_Execute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockDelayFn_Execute_Call) RunAndReturn(run func(context.Context) error) *MockDelayFn_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDelayFn creates a new instance of MockDelayFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDelayFn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDelayFn { + mock := &MockDelayFn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/model/mock/mock_UpdateFn.go b/internal/model/mock/mock_UpdateFn.go new file mode 100644 index 0000000..7cf96c6 --- /dev/null +++ b/internal/model/mock/mock_UpdateFn.go @@ -0,0 +1,65 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package model + +import mock "github.com/stretchr/testify/mock" + +// MockUpdateFn is an autogenerated mock type for the UpdateFn type +type MockUpdateFn struct { + mock.Mock +} + +type MockUpdateFn_Expecter struct { + mock *mock.Mock +} + +func (_m *MockUpdateFn) EXPECT() *MockUpdateFn_Expecter { + return &MockUpdateFn_Expecter{mock: &_m.Mock} +} + +// Execute provides a mock function with given fields: _a0 +func (_m *MockUpdateFn) Execute(_a0 string) { + _m.Called(_a0) +} + +// MockUpdateFn_Execute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Execute' +type MockUpdateFn_Execute_Call struct { + *mock.Call +} + +// Execute is a helper method to define mock.On call +// - _a0 string +func (_e *MockUpdateFn_Expecter) Execute(_a0 interface{}) *MockUpdateFn_Execute_Call { + return &MockUpdateFn_Execute_Call{Call: _e.mock.On("Execute", _a0)} +} + +func (_c *MockUpdateFn_Execute_Call) Run(run func(_a0 string)) *MockUpdateFn_Execute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *MockUpdateFn_Execute_Call) Return() *MockUpdateFn_Execute_Call { + _c.Call.Return() + return _c +} + +func (_c *MockUpdateFn_Execute_Call) RunAndReturn(run func(string)) *MockUpdateFn_Execute_Call { + _c.Call.Return(run) + return _c +} + +// NewMockUpdateFn creates a new instance of MockUpdateFn. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockUpdateFn(t interface { + mock.TestingT + Cleanup(func()) +}) *MockUpdateFn { + mock := &MockUpdateFn{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/model/types.go b/internal/model/types.go index 71a588e..cf9ad75 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -1,9 +1,11 @@ package model import ( + "context" "net" "github.com/google/uuid" + "github.com/stmcginnis/gofish/redfish" ) type ( @@ -46,3 +48,34 @@ type Asset struct { // Facility this Asset is hosted in. FacilityCode string } + +// UpdateFn is a function that publishes the given string as a condition status message +type UpdateFn func(string) + +type DelayFn func(context.Context) error + +type OpenCloser interface { + Open(context.Context) error + Close(context.Context) error +} + +type BMCResetter interface { + BmcReset(context.Context, string) (bool, error) +} + +type PowerMonitor interface { + PowerStateGet(context.Context) (string, error) + PowerSet(context.Context, string) (bool, error) +} + +type BootMonitor interface { + GetBootProgress() (*redfish.BootProgress, error) + BootComplete() (bool, error) +} + +type BMCBootMonitor interface { + OpenCloser + BMCResetter + BootMonitor + PowerMonitor +}