From 18f106fa70a140dd2da7a43400db1eddc0f838f5 Mon Sep 17 00:00:00 2001 From: Doctor Vince Date: Fri, 4 Oct 2024 13:36:44 -0400 Subject: [PATCH] refactor validate firmware action I imported a pre-release version of bmclib to get access to the new versions of the supermicro client object. Using that I defined some minimalist interfaces and used mockery to provide test collateral. --- .mockery.yaml | 10 + go.mod | 30 +- go.sum | 48 +-- internal/flipflop/handler.go | 188 ++++++---- internal/flipflop/handler_test.go | 232 ++++++++++++ internal/flipflop/validation_client.go | 63 ++++ internal/model/mock/mock_BMCBootMonitor.go | 412 +++++++++++++++++++++ internal/model/mock/mock_DelayFn.go | 82 ++++ internal/model/mock/mock_UpdateFn.go | 65 ++++ internal/model/types.go | 33 ++ 10 files changed, 1059 insertions(+), 104 deletions(-) create mode 100644 .mockery.yaml create mode 100644 internal/flipflop/handler_test.go create mode 100644 internal/flipflop/validation_client.go create mode 100644 internal/model/mock/mock_BMCBootMonitor.go create mode 100644 internal/model/mock/mock_DelayFn.go create mode 100644 internal/model/mock/mock_UpdateFn.go 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 eceb88d..8abbdfc 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,17 @@ module github.com/metal-toolbox/flipflop -go 1.22 +go 1.23 -toolchain go1.22.1 +toolchain go1.23.1 require ( github.com/banzaicloud/logrus-runtime-formatter v0.0.0-20190729070250-5ae5475bae5e - github.com/bmc-toolbox/bmclib/v2 v2.2.2 + github.com/bmc-toolbox/bmclib/v2 v2.3.1-0.20240927204409-62fb8e22434a + github.com/bmc-toolbox/common v0.0.0-20240926143744-8c478be881d7 github.com/bombsimon/logrusr/v2 v2.0.1 github.com/coreos/go-oidc v2.2.1+incompatible 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,22 +25,22 @@ 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.19.0 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 - go.opentelemetry.io/otel v1.28.0 - go.opentelemetry.io/otel/trace v1.28.0 - golang.org/x/net v0.28.0 + go.opentelemetry.io/otel v1.30.0 + go.opentelemetry.io/otel/trace v1.30.0 + golang.org/x/net v0.29.0 golang.org/x/oauth2 v0.22.0 ) require ( cloud.google.com/go/kms v1.17.1 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/Jeffail/gabs/v2 v2.7.0 // indirect github.com/VictorLowther/simplexml v0.0.0-20180716164440-0bff93621230 // indirect github.com/VictorLowther/soap v0.0.0-20150314151524-8e36fca84b22 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b // indirect github.com/bytedance/sonic v1.12.1 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect @@ -55,7 +57,6 @@ require ( github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.10.0 // 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 @@ -114,7 +115,6 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stmcginnis/gofish v0.15.1-0.20231121142100-22a60a77be91 // 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 @@ -130,17 +130,17 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect gocloud.dev v0.38.0 // indirect golang.org/x/arch v0.9.0 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect google.golang.org/api v0.189.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240730163845-b1a4ccb954bf // indirect diff --git a/go.sum b/go.sum index 2c58e45..bca9f3f 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= @@ -111,10 +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.2.2 h1:JGkYQPplHSZsfJOjkRBCGUHGU8YdiFMRfJzwzOSUMhU= -github.com/bmc-toolbox/bmclib/v2 v2.2.2/go.mod h1:gFF4iD468hbW1JUdJJx3mbhNGzoLsG47epbMa++grp8= -github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b h1:0LHjikaGWlqEMczrCEZ6w1N/ZqcYlx6WRHkhabRUQEk= -github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= +github.com/bmc-toolbox/bmclib/v2 v2.3.1-0.20240927204409-62fb8e22434a h1:fff+bV2nG0lldfpiyrM0KN6HSUv4NgEniovOBaV8fWk= +github.com/bmc-toolbox/bmclib/v2 v2.3.1-0.20240927204409-62fb8e22434a/go.mod h1:t8If/0fHQTRIK/yKDk2H3SgthDNNj+7z2aeftDFRFrU= +github.com/bmc-toolbox/common v0.0.0-20240926143744-8c478be881d7 h1:Xe6j3oMwe82buwBwEpok9wr+v21Io59pqMTZ5rKRVn8= +github.com/bmc-toolbox/common v0.0.0-20240926143744-8c478be881d7/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/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24= @@ -701,8 +701,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stmcginnis/gofish v0.15.1-0.20231121142100-22a60a77be91 h1:WmABtU8y6kTgzoVUn3FWCQGAfyodve3uz3xno28BrRs= -github.com/stmcginnis/gofish v0.15.1-0.20231121142100-22a60a77be91/go.mod h1:BLDSFTp8pDlf/xDbLZa+F7f7eW0E/CHCboggsu8CznI= +github.com/stmcginnis/gofish v0.19.0 h1:fmxdRZ5WHfs+4ExArMYoeRfoh+SAxLELKtmoVplBkU4= +github.com/stmcginnis/gofish v0.19.0/go.mod h1:lq2jHj2t8Krg0Gx02ABk8MbK7Dz9jvWpO/TGnVksn00= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -777,18 +777,18 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= +go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= +go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= +go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= @@ -837,8 +837,8 @@ golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -849,8 +849,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -931,8 +931,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1062,8 +1062,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1082,8 +1082,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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= diff --git a/internal/flipflop/handler.go b/internal/flipflop/handler.go index 300ef76..e0c6b8a 100644 --- a/internal/flipflop/handler.go +++ b/internal/flipflop/handler.go @@ -2,9 +2,11 @@ package flipflop import ( "context" + "fmt" "strings" "time" + "github.com/bmc-toolbox/common" ctrl "github.com/metal-toolbox/ctrl" "github.com/metal-toolbox/flipflop/internal/app" "github.com/metal-toolbox/flipflop/internal/device" @@ -15,6 +17,7 @@ import ( "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" ) @@ -31,6 +34,10 @@ 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 @@ -224,98 +231,139 @@ 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) + // 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)) + } + + // get the correct handle to the BMC, we let bmclib deal with the differences between X11/X12/X13. + handle := newSMCValidationHandle(cth.server) - // 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) + // 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") + } + } + + waitForBMC := func(ctx context.Context) error { + var err error + select { + case <-time.After(30 * time.Second): + case <-ctx.Done(): + err = ctx.Err() + } + return err } - cth.publishActive(ctx, "bmc power cycle complete") + deadlineCtx, cancel := context.WithTimeout(ctx, cth.task.Parameters.ValidateFirmwareTimeout) + validateErr := validateFirmwareInternal(deadlineCtx, handle, updateFn, waitForBMC) + cancel() - // 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...) + 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()) + } + + 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. +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 - var psErr error - for time.Now().Before(deadline) { - if errDelay := sleepInContext(ctx, 30*time.Second); errDelay != nil { - return cth.failedWithError(context.Background(), "context error", errDelay) + for !bmcConnected { + if err := delay(ctx); err != nil { + return fmt.Errorf("context error: %w", err) } - if !bmcConnected { - _ = cth.bmc.Close(ctx) - if err := cth.openBMCConnection(ctx); err != nil { - cth.logger.WithError(err).Warn("bmc: failed to connect") - continue - } - bmcConnected = true - // we have a deferred close for the BMC session queued up when we return from this function + 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") + } - newDeviceState := "cycle" - currentState, err := cth.bmc.GetPowerState(ctx) + 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 { - cth.logger.WithError(err).Debug("getting device current power state") + payload := fmt.Sprintf("getting bmc power state: %s", err.Error()) + update(payload) continue } + newDeviceState := "cycle" if strings.Contains(strings.ToLower(currentState), "off") { newDeviceState = "on" } - psErr = cth.bmc.SetPowerState(ctx, newDeviceState) - if psErr == nil { - cth.publishActive(ctx, "device power cycle complete") - break + if _, err := mon.PowerSet(ctx, newDeviceState); err != nil { + update(fmt.Sprintf("bmc set power state to %s: %s", newDeviceState, err.Error())) + continue } - cth.logger.WithError(err).WithField("state", newDeviceState).Debug("bmc set power state") + bmcPowerStateSet = true + update(fmt.Sprintf("device power state set to %s", newDeviceState)) } - if psErr != nil { - return cth.failedWithError(ctx, "failed to cycle host power after BMC power cycle", psErr) - } - - // 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(context.Background(), "failed to retrieve host boot status", errDelay) + 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) } - - booted, err := cth.bmc.HostBooted(ctx) + hostBooted, err = mon.BootComplete() if err != nil { - return cth.failedWithError(ctx, "failed to retrieve host boot status", err) - } - 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()) + update(fmt.Sprintf("checking host boot state: %s", err.Error())) + continue } + update("host boot complete") } - - return cth.failed(ctx, "host failed to boot successfully before deadline") -} - -func sleepInContext(ctx context.Context, t time.Duration) error { - select { - case <-time.After(t): - return nil - case <-ctx.Done(): - return ctx.Err() - } + return nil } // pxeBootPersistent sets up the server to pxe boot persistently @@ -327,6 +375,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..9ebd898 --- /dev/null +++ b/internal/flipflop/validation_client.go @@ -0,0 +1,63 @@ +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{ + 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 +}