From 3ecd32dea5ac6c083d746af1b77161f154d864e8 Mon Sep 17 00:00:00 2001 From: Guillaume Carre Date: Mon, 12 Oct 2026 16:09:02 +0200 Subject: [PATCH 1/3] test: describe hardware scenarios in Gherkin, play them on MegaRAID Add github.com/scality/raidmgmt/scenario, to test what raidmgmt reports when a drive fails, is replaced and gets a new volume, without the hardware. It is a separate module, using the raidmgmt code of this repository through a replace, so that godog and the captures stay out of raidmgmt's dependencies: - features/*.feature: Gherkin scenarios tagged with their controller family. The Background names the capture of a real controller; each scenario plays events on its drives (a drive fails, is replaced, gets a new volume), the ones it depends on written as Given steps, and gives the table of disks expected. When a bug makes it differ today, a known bug step gives the table reported because of it. - scenario: the step definitions, run by godog strictly (undefined steps fail, empty tables and duplicate slots are rejected), and the comparison of what is reported with a table. A known bug passes only while its table is reported; it fails once the expected table is (the bug is fixed) and when neither is (the behaviour changed). A drive reported twice is a difference. Run plays the features of a controller family through a player; Report collects the disks an adapter reports, listing the controllers first as disk-management-agent does. - steps_internal_test.go: checks on the features of testdata/ that the steps fail when they should, for the right reason. - megaraidsim: answers storcli64 commands from a capture with events applied as storcli showed them on RD-2154, and shows the /dev/disk/by-id links of exposed volumes. Drive groups are written as storcli prints them: a number, or "-" for an unconfigured drive. Events in an order that cannot happen (a volume created on a drive never replaced, a healthy drive replaced) are rejected, and a capture listing an unknown drive or volume is an error, not a panic. - megaraidsim/adapter_test.go: plays every @megaraid scenario through the megaraid adapter of this repository. The first feature, megaraid-disk-failure, replays real storcli output of a lab node, kept to its first 4 single-drive RAID0 volumes and with fake serials and WWNs: all drives healthy, then drive 251:3 failing, being replaced and getting a new volume. The failure scenario documents ARTESCA-17960: the offline volume of the failed drive makes LogicalVolumes fail for the whole controller, so no drive gets a path. CI and the release gate now test and lint every module. Refs: ARTESCA-17960 --- .github/workflows/pre-merge.yaml | 6 +- .github/workflows/release.yaml | 3 +- Makefile | 2 + scenario/README.md | 93 +++ .../features/megaraid-disk-failure.feature | 61 ++ scenario/go.mod | 27 + scenario/go.sum | 43 + scenario/megaraidsim/adapter_test.go | 106 +++ .../captures/megaraid9560-4hdd/c0.json | 778 ++++++++++++++++++ .../megaraid9560-4hdd/c0_e251_s1.json | 99 +++ .../megaraid9560-4hdd/c0_e251_s2.json | 99 +++ .../megaraid9560-4hdd/c0_e251_s3.json | 99 +++ .../megaraid9560-4hdd/c0_e251_s4.json | 99 +++ .../captures/megaraid9560-4hdd/c0_v236.json | 68 ++ .../captures/megaraid9560-4hdd/c0_v237.json | 68 ++ .../captures/megaraid9560-4hdd/c0_v238.json | 68 ++ .../captures/megaraid9560-4hdd/c0_v239.json | 68 ++ .../captures/megaraid9560-4hdd/show_all.json | 56 ++ scenario/megaraidsim/megaraidsim.go | 567 +++++++++++++ .../megaraidsim/megaraidsim_internal_test.go | 38 + scenario/report.go | 80 ++ scenario/scenario.go | 304 +++++++ scenario/steps.go | 209 +++++ scenario/steps_internal_test.go | 80 ++ scenario/testdata/differs.feature | 10 + scenario/testdata/duplicate-slot.feature | 11 + scenario/testdata/known-bug-changed.feature | 13 + scenario/testdata/known-bug-fixed.feature | 13 + scenario/testdata/known-bug-misplaced.feature | 14 + .../testdata/known-bug-reproduced.feature | 13 + scenario/testdata/no-capture.feature | 7 + scenario/testdata/not-reported.feature | 11 + scenario/testdata/passes.feature | 16 + scenario/testdata/undefined-step.feature | 11 + scenario/testdata/wrong-header.feature | 10 + 35 files changed, 3247 insertions(+), 3 deletions(-) create mode 100644 scenario/README.md create mode 100644 scenario/features/megaraid-disk-failure.feature create mode 100644 scenario/go.mod create mode 100644 scenario/go.sum create mode 100644 scenario/megaraidsim/adapter_test.go create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s1.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s2.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s3.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s4.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v236.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v237.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v238.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v239.json create mode 100644 scenario/megaraidsim/captures/megaraid9560-4hdd/show_all.json create mode 100644 scenario/megaraidsim/megaraidsim.go create mode 100644 scenario/megaraidsim/megaraidsim_internal_test.go create mode 100644 scenario/report.go create mode 100644 scenario/scenario.go create mode 100644 scenario/steps.go create mode 100644 scenario/steps_internal_test.go create mode 100644 scenario/testdata/differs.feature create mode 100644 scenario/testdata/duplicate-slot.feature create mode 100644 scenario/testdata/known-bug-changed.feature create mode 100644 scenario/testdata/known-bug-fixed.feature create mode 100644 scenario/testdata/known-bug-misplaced.feature create mode 100644 scenario/testdata/known-bug-reproduced.feature create mode 100644 scenario/testdata/no-capture.feature create mode 100644 scenario/testdata/not-reported.feature create mode 100644 scenario/testdata/passes.feature create mode 100644 scenario/testdata/undefined-step.feature create mode 100644 scenario/testdata/wrong-header.feature diff --git a/.github/workflows/pre-merge.yaml b/.github/workflows/pre-merge.yaml index 014bc7c..d72caa6 100644 --- a/.github/workflows/pre-merge.yaml +++ b/.github/workflows/pre-merge.yaml @@ -25,7 +25,8 @@ jobs: go-version: ${{ env.GO_VERSION }} - name: Set list of modules id: set-modules - run: echo "modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')" >> $GITHUB_OUTPUT + # The root module and the nested ones, such as scenario/. + run: echo "modules=$(find "$PWD" -name go.mod -not -path '*/bin/*' -printf '%h\n' | sort | jq -R . | jq -sc .)" >> $GITHUB_OUTPUT golangci-lint: needs: detect-modules @@ -57,7 +58,8 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - name: Run tests - run: go test -v ${{ matrix.modules }}/... + working-directory: ${{ matrix.modules }} + run: go test -v ./... build-e2e: # The on-hardware integration harnesses are not run in CI (they need real diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index de15d9d..2c5bcdf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -41,7 +41,8 @@ jobs: with: go-version-file: go.mod - name: Run tests - run: go test -v ./... + # Every module, including the nested scenario/ one. + run: make tests release: needs: quality-gate diff --git a/Makefile b/Makefile index b743e1c..cd69a6c 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ E2E_BUILD_ENV := CGO_ENABLED=0 GOOS=$(E2E_GOOS) GOARCH=$(E2E_GOARCH) lint: @echo "Linting..." golangci-lint run -c .golangci.yaml ./... + cd scenario && golangci-lint run -c ../.golangci.yaml ./... @echo "Lint done" # TODO @@ -23,6 +24,7 @@ lint: tests: @echo "Running tests..." go test -v ./... + cd scenario && go test -v ./... @echo "Tests done" # Build the on-hardware integration harnesses as static linux/amd64 binaries diff --git a/scenario/README.md b/scenario/README.md new file mode 100644 index 0000000..97dbbee --- /dev/null +++ b/scenario/README.md @@ -0,0 +1,93 @@ +# Hardware scenarios + +Replay what happens on a RAID controller, such as a drive failing and being +replaced, from real controller output, and check what raidmgmt reports, +without the hardware. + +Scenarios are Gherkin features in [`features/`](features), one per +controller family and story, tagged with the family (`@megaraid`). The +`Background` names the capture of a real controller; each `Scenario` plays +events on its drives and gives, as a table, the disks expected: + +```gherkin +@megaraid +Feature: MegaRAID disk failure and replacement + + Background: + Given the controller captured as "megaraid9560-4hdd" + + Scenario: A failed drive does not take the paths of the others + When drive 251:3 fails + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | /dev/sdl | /dev/disk/by-id/wwn-0x6000… | + | 251:3 | Failed | SIMD0003 | | | + But known bug ARTESCA-17960 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | | | + | 251:3 | Failed | SIMD0003 | | | +``` + +## Steps + +| Step | Event | +|---|---| +| `Given the controller captured as ""` | Start from a capture | +| `drive fails` / `has failed` | The drive fails; its volume goes offline, no longer a block device | +| `drive is replaced` / `has been replaced by a drive with serial "" and WWN ""` | Its volume is deleted and a new drive, in no volume, takes the slot | +| `a volume is created` / `has been created on drive , exposed as "" with WWN ""` | The volume the drive had in the capture is created again on it | +| `Then the drives are reported as:` | Check the table: status, serial, and the device and permanent paths of the volume holding each drive. An empty cell is an empty value | +| `But known bug reports instead:` | Right after a `Then`: the table reported today because of the bug | + +- Each scenario stands alone: it replays its own events on the capture, the + ones it depends on written as `Given` steps. +- Events are written the same for every controller; each backend applies + them as its controller shows them (states, serials, drive groups), as + documented in its package. A backend rejects events in an order that + cannot happen: only a drive in a volume fails, only a failed drive is + replaced, and a volume is created only once the old one is deleted. +- A known bug passes while its table is reported. It fails when the + expected table is reported (the bug is fixed: remove the step) and when + neither is (the behaviour changed). +- Steps are run by [godog](https://github.com/cucumber/godog), strictly: a + step with no definition fails. When a table differs, the error prints the + table reported, ready to paste. + +## Where scenarios are played + +- `megaraidsim/adapter_test.go` plays every `@megaraid` scenario (storcli64) + through the raidmgmt megaraid adapter of this repository, with + `scenario.Run`. +- `steps_internal_test.go` checks the steps themselves on the features of + [`testdata/`](testdata), in particular that they fail when they should. + +This is a separate module (`github.com/scality/raidmgmt/scenario`), which +uses the raidmgmt code of this repository through a `replace`: godog and +the captures stay out of the dependencies and the downloads of raidmgmt. + +## Adding a scenario + +Add a `Scenario` to a feature, or a `.feature` file in `features/` tagged +with its controller family, on an existing capture, and run `go test ./...` +in this directory. A new kind of event needs a step in `steps.go` and its +handling in every backend. + +## Adding a controller + +1. Capture its CLI output on a lab machine, one file per command the adapter + runs, in `/captures//`. +2. Write a backend that answers those commands from the capture with the + events applied, as [`megaraidsim`](megaraidsim) does for storcli64, and + gives the host files the adapter checks. +3. Add a test calling `scenario.Run` with the tag of that controller and a + player reporting through its adapter; `scenario.Report` turns what an + adapter reports into the disks of a table. + +## Captures + +- `megaraidsim/captures/megaraid9560-4hdd`: MegaRAID 9560-8i, SEAGATE + ST18000NM000D drives in enclosure 251, one RAID0 volume each (storcli64 + 007.1616). Captured with `storcli64 show all J` on 12 drives, + kept to the first 4 (251:1-4, volumes 239-236) to keep scenarios short. + Serials, WWNs and controller identifiers are replaced by fake ones, also in + the hex inquiry data; keep it so when adding a capture. diff --git a/scenario/features/megaraid-disk-failure.feature b/scenario/features/megaraid-disk-failure.feature new file mode 100644 index 0000000..cddad8b --- /dev/null +++ b/scenario/features/megaraid-disk-failure.feature @@ -0,0 +1,61 @@ +@megaraid +Feature: MegaRAID disk failure and replacement + A MegaRAID 9560-8i with HDDs in enclosure 251, each in its own RAID0 + volume, as ARTESCA deploys them. Drive 251:3 fails, is replaced, then gets a + new volume (RD-2154, ARTESCA-17960). + + The capture is real storcli output of a lab node while all its drives + were healthy, kept to its first 4 drives, with serials and WWNs replaced by + fake ones. + + Each table lists, per drive, what raidmgmt must report: the drive, and + the paths of the volume holding it. + + Background: + Given the controller captured as "megaraid9560-4hdd" + + Scenario: All drives are healthy + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | /dev/sdl | /dev/disk/by-id/wwn-0x600062b2000000000000000000000239 | + | 251:2 | Used | SIMD0002000000000000 | /dev/sdk | /dev/disk/by-id/wwn-0x600062b2000000000000000000000238 | + | 251:3 | Used | SIMD0003000000000000 | /dev/sdj | /dev/disk/by-id/wwn-0x600062b2000000000000000000000237 | + | 251:4 | Used | SIMD0004000000000000 | /dev/sdi | /dev/disk/by-id/wwn-0x600062b2000000000000000000000236 | + + Scenario: A failed drive does not take the paths of the others + When drive 251:3 fails + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | /dev/sdl | /dev/disk/by-id/wwn-0x600062b2000000000000000000000239 | + | 251:2 | Used | SIMD0002000000000000 | /dev/sdk | /dev/disk/by-id/wwn-0x600062b2000000000000000000000238 | + | 251:3 | Failed | SIMD0003 | | | + | 251:4 | Used | SIMD0004000000000000 | /dev/sdi | /dev/disk/by-id/wwn-0x600062b2000000000000000000000236 | + # The offline volume has no by-id link: its path cannot be resolved, which + # fails the listing of all the volumes of the controller. + But known bug ARTESCA-17960 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | | | + | 251:2 | Used | SIMD0002000000000000 | | | + | 251:3 | Failed | SIMD0003 | | | + | 251:4 | Used | SIMD0004000000000000 | | | + + Scenario: A replacement drive is reported as a new, available drive + Given drive 251:3 has failed + When drive 251:3 is replaced by a drive with serial "SIMD0013000000000000" and WWN "5000C50000000013" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | /dev/sdl | /dev/disk/by-id/wwn-0x600062b2000000000000000000000239 | + | 251:2 | Used | SIMD0002000000000000 | /dev/sdk | /dev/disk/by-id/wwn-0x600062b2000000000000000000000238 | + | 251:3 | UnassignedGood | SIMD0013000000000000 | | | + | 251:4 | Used | SIMD0004000000000000 | /dev/sdi | /dev/disk/by-id/wwn-0x600062b2000000000000000000000236 | + + Scenario: A replacement drive gets the paths of its new volume + Given drive 251:3 has failed + And drive 251:3 has been replaced by a drive with serial "SIMD0013000000000000" and WWN "5000C50000000013" + When a volume is created on drive 251:3, exposed as "/dev/sdm" with WWN "600062b2000000000000000000001237" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | SIMD0001000000000000 | /dev/sdl | /dev/disk/by-id/wwn-0x600062b2000000000000000000000239 | + | 251:2 | Used | SIMD0002000000000000 | /dev/sdk | /dev/disk/by-id/wwn-0x600062b2000000000000000000000238 | + | 251:3 | Used | SIMD0013000000000000 | /dev/sdm | /dev/disk/by-id/wwn-0x600062b2000000000000000000001237 | + | 251:4 | Used | SIMD0004000000000000 | /dev/sdi | /dev/disk/by-id/wwn-0x600062b2000000000000000000000236 | diff --git a/scenario/go.mod b/scenario/go.mod new file mode 100644 index 0000000..51589be --- /dev/null +++ b/scenario/go.mod @@ -0,0 +1,27 @@ +module github.com/scality/raidmgmt/scenario + +go 1.25.6 + +require ( + github.com/cucumber/godog v0.16.0 + github.com/pkg/errors v0.9.1 + github.com/scality/raidmgmt v0.14.0 + github.com/stretchr/testify v1.11.1 +) + +require ( + github.com/cucumber/gherkin/go/v42 v42.0.0 // indirect + github.com/cucumber/messages/go/v34 v34.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-memdb v1.3.5 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +// Play the scenarios on the adapters of this repository. +replace github.com/scality/raidmgmt => ../ diff --git a/scenario/go.sum b/scenario/go.sum new file mode 100644 index 0000000..037db41 --- /dev/null +++ b/scenario/go.sum @@ -0,0 +1,43 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cucumber/gherkin/go/v42 v42.0.0 h1:Ulh3E2awUUSSja+wonP/IOQ+ycmiZwZbgmzqk5H8JNI= +github.com/cucumber/gherkin/go/v42 v42.0.0/go.mod h1:CsaumaO2dR9XvBc6ZyiGLMhWCKtTRDxgoxqJigSjSSg= +github.com/cucumber/godog v0.16.0 h1:ezQbgItuWqZrjPUQwLJ3muwIlvzXBOfZso5QZfG7efE= +github.com/cucumber/godog v0.16.0/go.mod h1:EDUX9yCqANK+GpbftMDeu61sUDtdLuo1JJgXD2n3bbM= +github.com/cucumber/messages/go/v34 v34.2.0 h1:VCbcNOMz+f8ccjjOOx1NLBNhwvE7/X49Atc8klJa+i8= +github.com/cucumber/messages/go/v34 v34.2.0/go.mod h1:LYUPjqlTS1kS0pdkdf6sS5uirnjwiIzEGyXPezXNhL8= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scenario/megaraidsim/adapter_test.go b/scenario/megaraidsim/adapter_test.go new file mode 100644 index 0000000..98b1d35 --- /dev/null +++ b/scenario/megaraidsim/adapter_test.go @@ -0,0 +1,106 @@ +package megaraidsim_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/cucumber/godog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" + "github.com/scality/raidmgmt/pkg/implementation/raidcontroller/megaraid" + "github.com/scality/raidmgmt/scenario" + "github.com/scality/raidmgmt/scenario/megaraidsim" +) + +const capture = "megaraid9560-4hdd" + +// TestScenarios plays every @megaraid scenario through the raidmgmt megaraid +// adapter of this repository and checks the drives it reports, with the +// device and permanent paths of the volume holding each drive. +func TestScenarios(t *testing.T) { + scenario.Run(t, "megaraid", func(ctx context.Context, capture string, events []scenario.Event) ([]scenario.Disk, error) { + ctrl, err := megaraidsim.New(capture, events...) + if err != nil { + return nil, err + } + + defer ctrl.UseHost()() + + disks, volumesErr, err := scenario.Report(megaraid.New(ctrl), &raidcontroller.Metadata{ID: 0}) + if volumesErr != nil { + godog.Logf(ctx, "LogicalVolumes: %v", volumesErr) + } + + return disks, err + }) +} + +// TestNewRejects checks that New fails on an unknown capture or drive, and on +// events in an order that cannot happen to a drive. +func TestNewRejects(t *testing.T) { + t.Parallel() + + fails := scenario.Event{Kind: scenario.DriveFails, Slot: "251:3"} + replaced := scenario.Event{Kind: scenario.DriveReplaced, Slot: "251:3", Serial: "S", WWN: "W"} + created := scenario.Event{Kind: scenario.VolumeCreated, Slot: "251:3", Device: "/dev/sdz", VolumeWWN: "1"} + + tests := []struct { + name string + capture string + events []scenario.Event + wantErr string + }{ + {name: "unknown capture", capture: "no-such-capture", wantErr: "failed to load capture"}, + {name: "unknown drive", capture: capture, events: []scenario.Event{{Kind: scenario.DriveFails, Slot: "9:9"}}, wantErr: "unknown drive"}, + {name: "fails twice", capture: capture, events: []scenario.Event{fails, fails}, wantErr: "cannot fail"}, + {name: "replaced while healthy", capture: capture, events: []scenario.Event{replaced}, wantErr: "cannot be replaced"}, + {name: "volume created on a failed drive", capture: capture, events: []scenario.Event{fails, created}, wantErr: "still exists"}, + {name: "replacement fails before its volume", capture: capture, events: []scenario.Event{fails, replaced, fails}, wantErr: "cannot fail"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := megaraidsim.New(tt.capture, tt.events...) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +// TestDriveGroupIsNumeric checks that a drive group is written as storcli +// prints it: "-" for an unconfigured drive, else a number. +func TestDriveGroupIsNumeric(t *testing.T) { + fails := scenario.Event{Kind: scenario.DriveFails, Slot: "251:3"} + replaced := scenario.Event{Kind: scenario.DriveReplaced, Slot: "251:3", Serial: "S", WWN: "W"} + created := scenario.Event{Kind: scenario.VolumeCreated, Slot: "251:3", Device: "/dev/sdm"} + + tests := []struct { + name string + events []scenario.Event + want any + }{ + {name: "replaced", events: []scenario.Event{fails, replaced}, want: "-"}, + {name: "recreated", events: []scenario.Event{fails, replaced, created}, want: float64(2)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl, err := megaraidsim.New(capture, tt.events...) + require.NoError(t, err) + + out, err := ctrl.Run([]string{"/c0/e251/s3", "show", "all"}) + require.NoError(t, err) + + var data struct { + Drive []map[string]any `json:"Drive /c0/e251/s3"` + } + require.NoError(t, json.Unmarshal(out.Controllers[0].ResponseData, &data)) + require.NotEmpty(t, data.Drive) + assert.Equal(t, tt.want, data.Drive[0]["DG"]) + }) + } +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0.json new file mode 100644 index 0000000..9681052 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0.json @@ -0,0 +1,778 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Basics": { + "Controller": 0, + "Model": "MegaRAID 9560-8i 4GB", + "Serial Number": "SIM0000001", + "Current Controller Date/Time": "11/08/2024, 14:35:09", + "Current System Date/time": "11/08/2024, 14:35:10", + "SAS Address": "500062b200000000", + "PCI Address": "00:c1:00:00", + "Mfg Date": "12/23/22", + "Rework Date": "00/00/00", + "Revision No": "01006" + }, + "Version": { + "Firmware Package Build": "52.24.0-4763", + "Firmware Version": "5.240.02-3768", + "NVDATA Version": "5.2400.00-0634", + "CBB Version": "24.250.10.00", + "Bios Version": "7.24.01.0_0x07180100", + "HII Version": "07.24.03.00", + "HIIA Version": "07.24.03.00", + "Driver Name": "megaraid_sas", + "Driver Version": "07.727.03.00-rc1" + }, + "Bus": { + "Vendor Id": 4096, + "Device Id": 4322, + "SubVendor Id": 4096, + "SubDevice Id": 16400, + "Host Interface": "PCI-E", + "Device Interface": "SAS-12G", + "Bus Number": 193, + "Device Number": 0, + "Function Number": 0, + "Domain ID": 0 + }, + "Pending Images in Flash": { + "Image name": "No pending images" + }, + "Status": { + "Controller Status": "Optimal", + "Memory Correctable Errors": 0, + "Memory Uncorrectable Errors": 0, + "ECC Bucket Count": 0, + "Any Offline VD Cache Preserved": "No", + "BBU Status": 0, + "PD Firmware Download in progress": "No", + "Support PD Firmware Download": "Yes", + "Lock Key Assigned": "No", + "Failed to get lock key on bootup": "No", + "Lock key has not been backed up": "No", + "Bios was not detected during boot": "No", + "Controller must be rebooted to complete security operation": "No", + "A rollback operation is in progress": "No", + "At least one PFK exists in NVRAM": "No", + "SSC Policy is WB": "No", + "Controller has booted into safe mode": "No", + "Controller shutdown required": "No", + "Controller has booted into certificate provision mode": "No", + "Current Personality": "RAID-Mode " + }, + "Supported Adapter Operations": { + "Rebuild Rate": "Yes", + "CC Rate": "Yes", + "BGI Rate ": "Yes", + "Reconstruct Rate": "Yes", + "Patrol Read Rate": "Yes", + "Alarm Control": "No", + "Cluster Support": "No", + "BBU": "Yes", + "Spanning": "Yes", + "Dedicated Hot Spare": "Yes", + "Revertible Hot Spares": "Yes", + "Foreign Config Import": "Yes", + "Self Diagnostic": "Yes", + "Allow Mixed Redundancy on Array": "No", + "Global Hot Spares": "Yes", + "Deny SCSI Passthrough": "No", + "Deny SMP Passthrough": "No", + "Deny STP Passthrough": "No", + "Support more than 8 Phys": "Yes", + "FW and Event Time in GMT": "No", + "Support Enhanced Foreign Import": "Yes", + "Support Enclosure Enumeration": "Yes", + "Support Allowed Operations": "Yes", + "Abort CC on Error": "Yes", + "Support Multipath": "Yes", + "Support Odd & Even Drive count in RAID1E": "No", + "Support Security": "Yes", + "Support Config Page Model": "Yes", + "Support the OCE without adding drives": "Yes", + "Support EKM": "Yes", + "Snapshot Enabled": "No", + "Support PFK": "Yes", + "Support PI": "No", + "Support Ld BBM Info": "No", + "Support Shield State": "Yes", + "Block SSD Write Disk Cache Change": "No", + "Support Suspend Resume BG ops": "Yes", + "Support Emergency Spares": "Yes", + "Support Set Link Speed": "Yes", + "Support Boot Time PFK Change": "No", + "Support JBOD": "No", + "Disable Online PFK Change": "No", + "Support Perf Tuning": "Yes", + "Support SSD PatrolRead": "Yes", + "Real Time Scheduler": "Yes", + "Support Reset Now": "Yes", + "Support Emulated Drives": "Yes", + "Headless Mode": "Yes", + "Dedicated HotSpares Limited": "No", + "Point In Time Progress": "Yes", + "Extended LD": "Yes", + "Support Uneven span ": "No", + "Support Config Auto Balance": "No", + "Support Maintenance Mode": "No", + "Support Diagnostic results": "Yes", + "Support Ext Enclosure": "Yes", + "Support Sesmonitoring": "Yes", + "Support SecurityonJBOD": "Yes", + "Support ForceFlash": "Yes", + "Support DisableImmediateIO": "Yes", + "Support LargeIOSupport": "No", + "Support DrvActivityLEDSetting": "Yes", + "Support FlushWriteVerify": "Yes", + "Support CPLDUpdate": "No", + "Support ForceTo512e": "Yes", + "Support discardCacheDuringLDDelete": "Yes", + "Support JBOD Write cache": "No", + "Support Large QD Support": "No", + "Support Ctrl Info Extended": "Yes", + "Support IButton less": "Yes", + "Support AES Encryption Algorithm": "No", + "Support Encrypted MFC": "Yes", + "Support Snapdump": "Yes", + "Support Force Personality Change": "Yes", + "Support Dual Fw Image": "No", + "Support PSOC Update": "Yes", + "Support Secure Boot": "Yes", + "Support Debug Queue": "No", + "Support Least Latency Mode": "No", + "Support OnDemand Snapdump": "Yes", + "Support Clear Snapdump": "Yes", + "Support FW Triggered Snapdump": "Yes", + "Support PHY current speed": "Yes", + "Support Lane current speed": "Yes", + "Support NVMe Width": "Yes", + "Support Lane DeviceType": "Yes", + "Support Extended Drive performance Monitoring": "Yes", + "Support NVMe Repair": "Yes", + "Support Platform Security": "No", + "Support None Mode Params": "Yes", + "Support Extended Controller Property": "Yes", + "Support Smart Poll Interval for DirectAttached": "Yes", + "Support Write Journal Pinning": "Yes", + "Support SMP Passthru with Port Number": "Yes", + "Support NVMe Init Error Device ConnectorIndex": "No" + }, + "Enterprise Key management": { + "Capability": "Supported", + "Boot Agent": "Not Available", + "Configured": "No" + }, + "Supported PD Operations": { + "Force Online": "Yes", + "Force Offline": "Yes", + "Force Rebuild": "Yes", + "Deny Force Failed": "No", + "Deny Force Good/Bad": "No", + "Deny Missing Replace": "No", + "Deny Clear": "No", + "Deny Locate": "No", + "Support Power State": "Yes", + "Set Power State For Cfg": "No", + "Support T10 Power State": "No", + "Support Temperature": "Yes", + "NCQ": "Yes", + "Support Max Rate SATA": "No", + "Support Degraded Media": "No", + "Support Parallel FW Update": "Yes", + "Support Drive Crypto Erase": "Yes", + "Support SSD Wear Gauge": "No" + }, + "Supported VD Operations": { + "Read Policy": "Yes", + "Write Policy": "Yes", + "IO Policy": "No", + "Access Policy": "Yes", + "Disk Cache Policy": "Yes", + "Reconstruction": "Yes", + "Deny Locate": "No", + "Deny CC": "No", + "Allow Ctrl Encryption": "No", + "Enable LDBBM": "Yes", + "Support FastPath": "Yes", + "Performance Metrics": "Yes", + "Power Savings": "No", + "Support Powersave Max With Cache": "No", + "Support Breakmirror": "No", + "Support SSC WriteBack": "No", + "Support SSC Association": "No", + "Support VD Hide": "Yes", + "Support VD Cachebypass": "Yes", + "Support VD discardCacheDuringLDDelete": "Yes", + "Support VD Scsi Unmap": "Yes" + }, + "Advanced Software Option": [ + { + "Adv S/W Opt": "MegaRAID FastPath", + " Time Remaining": " Unlimited", + " Mode": " -" + }, + { + "Adv S/W Opt": "MegaRAID SafeStore", + " Time Remaining": " Unlimited", + " Mode": " -" + }, + { + "Adv S/W Opt": "MegaRAID RAID6", + " Time Remaining": " Unlimited", + " Mode": " -" + }, + { + "Adv S/W Opt": "MegaRAID RAID5", + " Time Remaining": " Unlimited", + " Mode": " -" + } + ], + "Safe ID": " SIMULATEDSAFEID0000000000000000000000000", + "HwCfg": { + "ChipRevision": " A0", + "BatteryFRU": "N/A", + "Front End Port Count": 0, + "Backend Port Count": 8, + "BBU": "Present", + "Alarm": "Absent", + "Serial Debugger": "Present", + "NVRAM Size": "128KB", + "Flash Size": "16MB", + "On Board Memory Size": "4096MB", + "CacheVault Flash Size": "17.320 GB", + "TPM": "Absent", + "Upgrade Key": "Absent", + "On Board Expander": "Absent", + "Temperature Sensor for ROC": "Present", + "Temperature Sensor for Controller": "Absent", + "Upgradable CPLD": "Absent", + "Upgradable PSOC": "Present", + "Current Size of CacheCade (GB)": 0, + "Current Size of FW Cache (MB)": 3142, + "ROC temperature(Degree Celsius)": 49 + }, + "Policies": { + "Policies Table": [ + { + "Policy": "Predictive Fail Poll Interval", + "Current": "300 sec", + "Default": "" + }, + { + "Policy": "Interrupt Throttle Active Count", + "Current": "16", + "Default": "" + }, + { + "Policy": "Interrupt Throttle Completion", + "Current": "50 us", + "Default": "" + }, + { + "Policy": "Rebuild Rate", + "Current": "30 %", + "Default": "30%" + }, + { + "Policy": "PR Rate", + "Current": "30 %", + "Default": "30%" + }, + { + "Policy": "BGI Rate", + "Current": "30 %", + "Default": "30%" + }, + { + "Policy": "Check Consistency Rate", + "Current": "30 %", + "Default": "30%" + }, + { + "Policy": "Reconstruction Rate", + "Current": "30 %", + "Default": "30%" + }, + { + "Policy": "Cache Flush Interval", + "Current": "4s", + "Default": "" + } + ], + "Flush Time(Default)": "4s", + "Drive Coercion Mode": "none", + "Auto Rebuild": "On", + "Battery Warning": "On", + "ECC Bucket Size": 15, + "ECC Bucket Leak Rate (hrs)": 24, + "Restore Hot Spare on Insertion": "Off", + "Expose Enclosure Devices": "On", + "Maintain PD Fail History": "On", + "Reorder Host Requests": "On", + "Auto detect BackPlane": "SGPIO/i2c SEP", + "Load Balance Mode": "Auto", + "Security Key Assigned": "Off", + "Disable Online Controller Reset": "Off", + "Use drive activity for locate": "Off" + }, + "Boot": { + "BIOS Enumerate VDs": 1, + "Stop BIOS on Error": "Off", + "Delay during POST": 0, + "Spin Down Mode": "None", + "Enable Ctrl-R": "No", + "Enable Web BIOS": "No", + "Enable PreBoot CLI": "No", + "Enable BIOS": "Yes", + "Max Drives to Spinup at One Time": 2, + "Maximum number of direct attached drives to spin up in 1 min": 60, + "Delay Among Spinup Groups (sec)": 2, + "Allow Boot with Preserved Cache": "Off" + }, + "High Availability": { + "Topology Type": "None", + "Cluster Permitted": "No", + "Cluster Active": "No" + }, + "Defaults": { + "Phy Polarity": 0, + "Phy PolaritySplit": 0, + "Strip Size": "256 KB", + "Write Policy": "WB", + "Read Policy": "RA", + "Cache When BBU Bad": "Off", + "Cached IO": "Off", + "VD PowerSave Policy": "Controller Defined", + "Default spin down time (mins)": 30, + "Coercion Mode": "None", + "ZCR Config": "Unknown", + "Max Chained Enclosures": 16, + "Direct PD Mapping": "No", + "Restore Hot Spare on Insertion": "No", + "Expose Enclosure Devices": "Yes", + "Maintain PD Fail History": "Yes", + "Zero Based Enclosure Enumeration": "No", + "Disable Puncturing": "No", + "EnableLDBBM": "Yes", + "DisableHII": "No", + "Un-Certified Hard Disk Drives": "Allow", + "SMART Mode": "Mode 6", + "Enable LED Header": "Yes", + "LED Show Drive Activity": "Yes", + "Dirty LED Shows Drive Activity": "No", + "EnableCrashDump": "No", + "Disable Online Controller Reset": "No", + "Treat Single span R1E as R10": "No", + "Power Saving option": "Enabled", + "TTY Log In Flash": "No", + "Auto Enhanced Import": "Yes", + "BreakMirror RAID Support": "No", + "Disable Join Mirror": "Yes", + "Enable Shield State": "Yes", + "Time taken to detect CME": "60 sec" + }, + "Capabilities": { + "Supported Drives": "SAS, SATA, NVMe", + "RAID Level Supported": "RAID0, RAID1(2 or more drives), RAID5, RAID6, RAID00, RAID10(2 or more drives per span), RAID50, RAID60", + "Enable JBOD": "No", + "Mix in Enclosure": "Allowed", + "Mix of SAS/SATA of HDD type in VD": "Allowed", + "Mix of SAS/SATA of SSD type in VD": "Not Allowed", + "Mix of SSD/HDD in VD": "Not Allowed", + "SAS Disable": "No", + "Max Arms Per VD": 32, + "Max Spans Per VD": 8, + "Max Arrays": 240, + "Max VD per array": 16, + "Max Number of VDs": 240, + "Max Parallel Commands": 5101, + "Max SGE Count": 60, + "Max Data Transfer Size": "2048 sectors", + "Max Strips PerIO": 42, + "Max Configurable CacheCade Size(GB)": 0, + "Max Transportable DGs": 0, + "Enable Snapdump": "Yes", + "Enable SCSI Unmap": "Yes", + "FDE Drive Mix Support": "No", + "Min Strip Size": "64 KB", + "Max Strip Size": "1.000 MB" + }, + "Scheduled Tasks": { + "Consistency Check Reoccurrence": "168 hrs", + "Next Consistency check launch": "11/09/2024, 03:00:00", + "Patrol Read Reoccurrence": "168 hrs", + "Next Patrol Read launch": "11/09/2024, 03:00:00", + "Battery learn Reoccurrence": "670 hrs", + "Next Battery Learn": "11/13/2024, 04:00:00", + "OEMID": "Broadcom" + }, + "Secure Boot": { + "Secure Boot Enabled": "Yes", + "Controller in Soft Secure Mode": "No", + "Controller in Hard Secure Mode": "Yes", + "Key Update Pending": "No", + "Remaining Secure Boot Key Slots": 7 + }, + "Security Protocol properties": { + "Security Protocol": "None" + }, + "Drive Groups": 4, + "TOPOLOGY": [ + { + "DG": 0, + "Arr": "-", + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 0, + "Arr": 0, + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 0, + "Arr": 0, + "Row": 0, + "EID:Slot": "251:1", + "DID": 10, + "Type": "DRIVE", + "State": "Onln", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "-", + "TR": "N" + }, + { + "DG": 1, + "Arr": "-", + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 1, + "Arr": 0, + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 1, + "Arr": 0, + "Row": 0, + "EID:Slot": "251:2", + "DID": 5, + "Type": "DRIVE", + "State": "Onln", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "-", + "TR": "N" + }, + { + "DG": 2, + "Arr": "-", + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 2, + "Arr": 0, + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 2, + "Arr": 0, + "Row": 0, + "EID:Slot": "251:3", + "DID": 6, + "Type": "DRIVE", + "State": "Onln", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "-", + "TR": "N" + }, + { + "DG": 3, + "Arr": "-", + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 3, + "Arr": 0, + "Row": "-", + "EID:Slot": "-", + "DID": "-", + "Type": "RAID0", + "State": "Optl", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "N", + "TR": "N" + }, + { + "DG": 3, + "Arr": 0, + "Row": 0, + "EID:Slot": "251:4", + "DID": 11, + "Type": "DRIVE", + "State": "Onln", + "BT": "N", + "Size": "16.370 TB", + "PDC": "dflt", + "PI": "N", + "SED": "N", + "DS3": "dflt", + "FSpace": "-", + "TR": "N" + } + ], + "Virtual Drives": 4, + "VD LIST": [ + { + "DG/VD": "0/239", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + }, + { + "DG/VD": "1/238", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + }, + { + "DG/VD": "2/237", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + }, + { + "DG/VD": "3/236", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + } + ], + "Physical Drives": 4, + "PD LIST": [ + { + "EID:Slt": "251:1", + "DID": 10, + "State": "Onln", + "DG": 0, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + }, + { + "EID:Slt": "251:2", + "DID": 5, + "State": "Onln", + "DG": 1, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + }, + { + "EID:Slt": "251:3", + "DID": 6, + "State": "Onln", + "DG": 2, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + }, + { + "EID:Slt": "251:4", + "DID": 11, + "State": "Onln", + "DG": 3, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "Enclosures": 1, + "Enclosure LIST": [ + { + "EID": 251, + "State": "OK", + "Slots": 12, + "PD": 4, + "PS": 1, + "Fans": 0, + "TSs": 3, + "Alms": 0, + "SIM": 0, + "Port#": "C0.0 & C0.1 x8", + "ProdID": "2U12SAS35EOB", + "VendorSpecific": "exp-1.12.60.4" + } + ], + "Cachevault_Info": [ + { + "Model": "CVPM05", + "State": "Optimal", + "Temp": "29C", + "Mode": "-", + "MfgDate": "2022/11/23" + } + ] + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s1.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s1.json new file mode 100644 index 0000000..ad46040 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s1.json @@ -0,0 +1,99 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "Show Drive Information Succeeded." + }, + "Response Data": { + "Drive /c0/e251/s1": [ + { + "EID:Slt": "251:1", + "DID": 10, + "State": "Onln", + "DG": 0, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "Drive /c0/e251/s1 - Detailed Information": { + "Drive /c0/e251/s1 State": { + "Shield Counter": 0, + "Media Error Count": 0, + "Other Error Count": 0, + "Drive Temperature": " 32C (89.60 F)", + "Predictive Failure Count": 0, + "S.M.A.R.T alert flagged by drive": "No" + }, + "Drive /c0/e251/s1 Device attributes": { + "SN": "SIMD0001000000000000", + "Manufacturer Id": "SEAGATE ", + "Model Number": "ST18000NM000D ", + "NAND Vendor": "NA", + "WWN": "5000C50000000001", + "Firmware Revision": "E003", + "Firmware Release Number": "N/A", + "Raw size": "16.371 TB [0x82f800000 Sectors]", + "Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Non Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Device Speed": "12.0Gb/s", + "Link Speed": "12.0Gb/s", + "Write Cache": "Enabled", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KB", + "Connector Name": "C0.0 & C0.1 " + }, + "Drive /c0/e251/s1 Policies/Settings": { + "Drive position": "DriveGroup:0, Span:0, Row:0", + "Enclosure position": "1", + "Connected Port Number": "0(path0) ", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Last Predictive Failure Event Sequence Number": 0, + "Successful diagnostics completion on": "N/A", + "FDE Type": "None", + "SED Capable": "No", + "SED Enabled": "No", + "Secured": "No", + "Cryptographic Erase Capable": "No", + "Sanitize Support": "OverWrite", + "Locked": "No", + "Needs EKM Attention": "No", + "PI Eligible": "No", + "Certified": "No", + "Wide Port Capable": "No", + "Unmap capable": "No", + "Unmap capable for LDs": "No", + "Multipath": "No", + "Port Information": [ + { + "Port": 0, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x5000c500d99b25b1" + }, + { + "Port": 1, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x0" + } + ] + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 38 30 30 30 4e 4d 30 30 30 44 20 20 20 45 30 30 33 53 49 4d 44 30 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 80 21 00 05 c0 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 32 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s2.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s2.json new file mode 100644 index 0000000..391e1cd --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s2.json @@ -0,0 +1,99 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "Show Drive Information Succeeded." + }, + "Response Data": { + "Drive /c0/e251/s2": [ + { + "EID:Slt": "251:2", + "DID": 5, + "State": "Onln", + "DG": 1, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "Drive /c0/e251/s2 - Detailed Information": { + "Drive /c0/e251/s2 State": { + "Shield Counter": 0, + "Media Error Count": 0, + "Other Error Count": 0, + "Drive Temperature": " 33C (91.40 F)", + "Predictive Failure Count": 0, + "S.M.A.R.T alert flagged by drive": "No" + }, + "Drive /c0/e251/s2 Device attributes": { + "SN": "SIMD0002000000000000", + "Manufacturer Id": "SEAGATE ", + "Model Number": "ST18000NM000D ", + "NAND Vendor": "NA", + "WWN": "5000C50000000002", + "Firmware Revision": "E003", + "Firmware Release Number": "N/A", + "Raw size": "16.371 TB [0x82f800000 Sectors]", + "Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Non Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Device Speed": "12.0Gb/s", + "Link Speed": "12.0Gb/s", + "Write Cache": "Enabled", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KB", + "Connector Name": "C0.0 & C0.1 " + }, + "Drive /c0/e251/s2 Policies/Settings": { + "Drive position": "DriveGroup:1, Span:0, Row:0", + "Enclosure position": "1", + "Connected Port Number": "0(path0) ", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Last Predictive Failure Event Sequence Number": 0, + "Successful diagnostics completion on": "N/A", + "FDE Type": "None", + "SED Capable": "No", + "SED Enabled": "No", + "Secured": "No", + "Cryptographic Erase Capable": "No", + "Sanitize Support": "OverWrite", + "Locked": "No", + "Needs EKM Attention": "No", + "PI Eligible": "No", + "Certified": "No", + "Wide Port Capable": "No", + "Unmap capable": "No", + "Unmap capable for LDs": "No", + "Multipath": "No", + "Port Information": [ + { + "Port": 0, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x5000c500d993e119" + }, + { + "Port": 1, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x0" + } + ] + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 38 30 30 30 4e 4d 30 30 30 44 20 20 20 45 30 30 33 53 49 4d 44 30 30 30 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 80 21 00 05 c0 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 32 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s3.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s3.json new file mode 100644 index 0000000..77c2aea --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s3.json @@ -0,0 +1,99 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "Show Drive Information Succeeded." + }, + "Response Data": { + "Drive /c0/e251/s3": [ + { + "EID:Slt": "251:3", + "DID": 6, + "State": "Onln", + "DG": 2, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "Drive /c0/e251/s3 - Detailed Information": { + "Drive /c0/e251/s3 State": { + "Shield Counter": 0, + "Media Error Count": 0, + "Other Error Count": 0, + "Drive Temperature": " 33C (91.40 F)", + "Predictive Failure Count": 0, + "S.M.A.R.T alert flagged by drive": "No" + }, + "Drive /c0/e251/s3 Device attributes": { + "SN": "SIMD0003000000000000", + "Manufacturer Id": "SEAGATE ", + "Model Number": "ST18000NM000D ", + "NAND Vendor": "NA", + "WWN": "5000C50000000003", + "Firmware Revision": "E003", + "Firmware Release Number": "N/A", + "Raw size": "16.371 TB [0x82f800000 Sectors]", + "Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Non Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Device Speed": "12.0Gb/s", + "Link Speed": "12.0Gb/s", + "Write Cache": "Enabled", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KB", + "Connector Name": "C0.0 & C0.1 " + }, + "Drive /c0/e251/s3 Policies/Settings": { + "Drive position": "DriveGroup:2, Span:0, Row:0", + "Enclosure position": "1", + "Connected Port Number": "0(path0) ", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Last Predictive Failure Event Sequence Number": 0, + "Successful diagnostics completion on": "N/A", + "FDE Type": "None", + "SED Capable": "No", + "SED Enabled": "No", + "Secured": "No", + "Cryptographic Erase Capable": "No", + "Sanitize Support": "OverWrite", + "Locked": "No", + "Needs EKM Attention": "No", + "PI Eligible": "No", + "Certified": "No", + "Wide Port Capable": "No", + "Unmap capable": "No", + "Unmap capable for LDs": "No", + "Multipath": "No", + "Port Information": [ + { + "Port": 0, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x5000c500d9829cb9" + }, + { + "Port": 1, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x0" + } + ] + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 38 30 30 30 4e 4d 30 30 30 44 20 20 20 45 30 30 33 53 49 4d 44 30 30 30 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 80 21 00 05 c0 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 32 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s4.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s4.json new file mode 100644 index 0000000..b7fe493 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_e251_s4.json @@ -0,0 +1,99 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "Show Drive Information Succeeded." + }, + "Response Data": { + "Drive /c0/e251/s4": [ + { + "EID:Slt": "251:4", + "DID": 11, + "State": "Onln", + "DG": 3, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "Drive /c0/e251/s4 - Detailed Information": { + "Drive /c0/e251/s4 State": { + "Shield Counter": 0, + "Media Error Count": 0, + "Other Error Count": 0, + "Drive Temperature": " 33C (91.40 F)", + "Predictive Failure Count": 0, + "S.M.A.R.T alert flagged by drive": "No" + }, + "Drive /c0/e251/s4 Device attributes": { + "SN": "SIMD0004000000000000", + "Manufacturer Id": "SEAGATE ", + "Model Number": "ST18000NM000D ", + "NAND Vendor": "NA", + "WWN": "5000C50000000004", + "Firmware Revision": "E003", + "Firmware Release Number": "N/A", + "Raw size": "16.371 TB [0x82f800000 Sectors]", + "Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Non Coerced size": "16.370 TB [0x82f700000 Sectors]", + "Device Speed": "12.0Gb/s", + "Link Speed": "12.0Gb/s", + "Write Cache": "Enabled", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KB", + "Connector Name": "C0.0 & C0.1 " + }, + "Drive /c0/e251/s4 Policies/Settings": { + "Drive position": "DriveGroup:3, Span:0, Row:0", + "Enclosure position": "1", + "Connected Port Number": "0(path0) ", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Last Predictive Failure Event Sequence Number": 0, + "Successful diagnostics completion on": "N/A", + "FDE Type": "None", + "SED Capable": "No", + "SED Enabled": "No", + "Secured": "No", + "Cryptographic Erase Capable": "No", + "Sanitize Support": "OverWrite", + "Locked": "No", + "Needs EKM Attention": "No", + "PI Eligible": "No", + "Certified": "No", + "Wide Port Capable": "No", + "Unmap capable": "No", + "Unmap capable for LDs": "No", + "Multipath": "No", + "Port Information": [ + { + "Port": 0, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x5000c500d99f97f9" + }, + { + "Port": 1, + "Status": "Active", + "Linkspeed": "12.0Gb/s", + "SAS address": "0x0" + } + ] + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 38 30 30 30 4e 4d 30 30 30 44 20 20 20 45 30 30 33 53 49 4d 44 30 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 80 21 00 05 c0 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 32 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v236.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v236.json new file mode 100644 index 0000000..9050475 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v236.json @@ -0,0 +1,68 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "/c0/v236": [ + { + "DG/VD": "3/236", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + } + ], + "PDs for VD 236": [ + { + "EID:Slt": "251:4", + "DID": 11, + "State": "Onln", + "DG": 3, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "VD236 Properties": { + "Strip Size": "256 KB", + "Number of Blocks": 35155607552, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives Per Span": 1, + "Write Cache(initial setting)": "WriteThrough", + "Disk Cache Policy": "Disk's Default", + "Encryption": "None", + "Data Protection": "None", + "Active Operations": "None", + "Exposed to OS": "Yes", + "OS Drive Name": "/dev/sdi", + "Creation Date": "20-04-2023", + "Creation Time": "08:29:53 AM", + "Emulation type": "default", + "Cachebypass size": "Cachebypass-64k", + "Cachebypass Mode": "Cachebypass Intelligent", + "Is LD Ready for OS Requests": "Yes", + "SCSI NAA Id": "600062b2000000000000000000000236", + "Unmap Enabled": "No" + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v237.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v237.json new file mode 100644 index 0000000..966a532 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v237.json @@ -0,0 +1,68 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "/c0/v237": [ + { + "DG/VD": "2/237", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + } + ], + "PDs for VD 237": [ + { + "EID:Slt": "251:3", + "DID": 6, + "State": "Onln", + "DG": 2, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "VD237 Properties": { + "Strip Size": "256 KB", + "Number of Blocks": 35155607552, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives Per Span": 1, + "Write Cache(initial setting)": "WriteThrough", + "Disk Cache Policy": "Disk's Default", + "Encryption": "None", + "Data Protection": "None", + "Active Operations": "None", + "Exposed to OS": "Yes", + "OS Drive Name": "/dev/sdj", + "Creation Date": "20-04-2023", + "Creation Time": "08:29:51 AM", + "Emulation type": "default", + "Cachebypass size": "Cachebypass-64k", + "Cachebypass Mode": "Cachebypass Intelligent", + "Is LD Ready for OS Requests": "Yes", + "SCSI NAA Id": "600062b2000000000000000000000237", + "Unmap Enabled": "No" + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v238.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v238.json new file mode 100644 index 0000000..53e4445 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v238.json @@ -0,0 +1,68 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "/c0/v238": [ + { + "DG/VD": "1/238", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + } + ], + "PDs for VD 238": [ + { + "EID:Slt": "251:2", + "DID": 5, + "State": "Onln", + "DG": 1, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "VD238 Properties": { + "Strip Size": "256 KB", + "Number of Blocks": 35155607552, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives Per Span": 1, + "Write Cache(initial setting)": "WriteThrough", + "Disk Cache Policy": "Disk's Default", + "Encryption": "None", + "Data Protection": "None", + "Active Operations": "None", + "Exposed to OS": "Yes", + "OS Drive Name": "/dev/sdk", + "Creation Date": "20-04-2023", + "Creation Time": "08:29:49 AM", + "Emulation type": "default", + "Cachebypass size": "Cachebypass-64k", + "Cachebypass Mode": "Cachebypass Intelligent", + "Is LD Ready for OS Requests": "Yes", + "SCSI NAA Id": "600062b2000000000000000000000238", + "Unmap Enabled": "No" + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v239.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v239.json new file mode 100644 index 0000000..ebb2ae1 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/c0_v239.json @@ -0,0 +1,68 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Controller": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "/c0/v239": [ + { + "DG/VD": "0/239", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "Consist": "Yes", + "Cache": "RWTD", + "Cac": "-", + "sCC": "ON", + "Size": "16.370 TB", + "Name": "" + } + ], + "PDs for VD 239": [ + { + "EID:Slt": "251:1", + "DID": 10, + "State": "Onln", + "DG": 0, + "Size": "16.370 TB", + "Intf": "SAS", + "Med": "HDD", + "SED": "N", + "PI": "N", + "SeSz": "512B", + "Model": "ST18000NM000D ", + "Sp": "U", + "Type": "-" + } + ], + "VD239 Properties": { + "Strip Size": "256 KB", + "Number of Blocks": 35155607552, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives Per Span": 1, + "Write Cache(initial setting)": "WriteThrough", + "Disk Cache Policy": "Disk's Default", + "Encryption": "None", + "Data Protection": "None", + "Active Operations": "None", + "Exposed to OS": "Yes", + "OS Drive Name": "/dev/sdl", + "Creation Date": "20-04-2023", + "Creation Time": "08:28:35 AM", + "Emulation type": "default", + "Cachebypass size": "Cachebypass-64k", + "Cachebypass Mode": "Cachebypass Intelligent", + "Is LD Ready for OS Requests": "Yes", + "SCSI NAA Id": "600062b2000000000000000000000239", + "Unmap Enabled": "No" + } + } + } + ] +} diff --git a/scenario/megaraidsim/captures/megaraid9560-4hdd/show_all.json b/scenario/megaraidsim/captures/megaraid9560-4hdd/show_all.json new file mode 100644 index 0000000..8520e25 --- /dev/null +++ b/scenario/megaraidsim/captures/megaraid9560-4hdd/show_all.json @@ -0,0 +1,56 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "007.1616.0000.0000 Dec 24, 2020", + "Operating system": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "Status Code": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Number of Controllers": 1, + "Host Name": "default-node", + "Operating System ": "Linux 4.18.0-553.22.1.el8_10.x86_64", + "System Overview": [ + { + "Ctl": 0, + "Model": "MegaRAID9560-8i4GB", + "Ports": 8, + "PDs": 4, + "DGs": 4, + "DNOpt": 0, + "VDs": 4, + "VNOpt": 0, + "BBU": "Opt", + "sPR": "On", + "DS": "1&2", + "EHS": "Y", + "ASOs": 4, + "Hlth": "Opt" + } + ], + "ASO": [ + { + "Ctl": 0, + "Cl": "X", + "SAS": "U", + "MD": "X", + "R6": "U", + "WC": "U", + "R5": "U", + "SS": "U", + "FP": "U", + "Re": "X", + "CR": "X", + "RF": "X", + "CO": "X", + "CW": "X", + "HA": "X", + "SSHA": "X" + } + ] + } + } + ] +} diff --git a/scenario/megaraidsim/megaraidsim.go b/scenario/megaraidsim/megaraidsim.go new file mode 100644 index 0000000..a833d63 --- /dev/null +++ b/scenario/megaraidsim/megaraidsim.go @@ -0,0 +1,567 @@ +// Package megaraidsim is the scenario backend for MegaRAID controllers driven +// by storcli64/perccli64: it answers the storcli commands of the raidmgmt +// megaraid adapter from a real capture with the events of a scenario applied, +// and makes the host show the /dev/disk/by-id links of the exposed volumes. +// +// Events are applied as storcli showed them on a lab node (RD-2154): a +// failed drive is Failed with its serial cut to 8 characters and its volume +// OfLn, not exposed to the OS; a replacement drive is UGood, in no drive +// group, with a new device ID; a new volume is Optl and its drive Onln. +// +// A capture is a directory under captures/ holding one JSON file per storcli +// command, named after it: "show all" -> show_all.json, "/c0 show all" -> +// c0.json, "/c0/e251/s3 show all" -> c0_e251_s3.json, "/c0/v237 show all" -> +// c0_v237.json. +package megaraidsim + +import ( + "embed" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "sync" + + "github.com/pkg/errors" + + "github.com/scality/raidmgmt/pkg/implementation/raidcontroller/megaraid" + "github.com/scality/raidmgmt/scenario" +) + +//go:embed captures +var captures embed.FS //nolint:gochecknoglobals // embedded captures + +const ( + byIDPrefix = "/dev/disk/by-id/wwn-0x" + yes, no = "Yes", "No" + // noGroup is the drive group of an unconfigured drive. + noGroup = "-" + // failed is the state of a failed drive. + failed = "Failed" + // failedSerialLength is the length of the serial of a failed drive. + failedSerialLength = 8 +) + +var errVolumeDeleted = errors.New("virtual drive does not exist") + +type ( + // Controller replays a capture with events applied. It implements + // megaraid.Runner. + Controller struct { + capture string + drives map[string]*drive // by "enclosure:slot" + volumes map[string]*volume // by virtual drive ID + } + + drive struct { + state, serial, wwn string + deviceID int + group any + // volume is the virtual drive holding the drive in the capture, and + // capturedGroup its drive group there. + volume string + capturedGroup any + } + + volume struct { + state, device, wwn string + exposed, deleted bool + } + + // doc is a storcli JSON output. + doc = map[string]any +) + +var _ megaraid.Runner = &Controller{} + +// New returns the controller of a capture once events happened, in order. +func New(capture string, events ...scenario.Event) (*Controller, error) { + c := &Controller{ + capture: path.Join("captures", capture), + drives: map[string]*drive{}, + volumes: map[string]*volume{}, + } + + if err := c.loadCapture(); err != nil { + return nil, errors.Wrapf(err, "failed to load capture %s", capture) + } + + for i, e := range events { + if err := c.apply(e); err != nil { + return nil, errors.Wrapf(err, "failed to apply event %d", i+1) + } + } + + return c, nil +} + +// Run answers a storcli command. +func (c *Controller) Run(args []string) (*megaraid.CmdOutput, error) { + cmd := strings.Join(args, " ") + + selector, ok := strings.CutSuffix(cmd, " show all") + if cmd != "show all" && !ok { + return nil, errors.Errorf("storcli %s: not replayed", cmd) + } + + out, err := c.render(selector) + if err != nil { + return nil, errors.Wrapf(err, "storcli %s", cmd) + } + + data, err := json.Marshal(out) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal output") + } + + var parsed megaraid.CmdOutput + if err := json.Unmarshal(data, &parsed); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal output") + } + + return &parsed, nil +} + +// Links returns the /dev/disk/by-id links of the exposed volumes, with the +// block device each one points to. +func (c *Controller) Links() map[string]string { + links := map[string]string{} + + for _, v := range c.volumes { + if !v.deleted && v.exposed && v.wwn != "" { + links[byIDPrefix+v.wwn] = v.device + } + } + + return links +} + +// hostMu serializes UseHost: the megaraid package reads the host through +// package-level hooks, so only one controller can use them at a time. +var hostMu sync.Mutex //nolint:gochecknoglobals // guards package-level hooks + +// UseHost makes the raidmgmt megaraid package see the links of this controller +// instead of the ones of the host, and returns a function restoring the host. +// The megaraid package reads the host through package-level hooks: UseHost +// blocks until the previous controller has restored the host, so scenarios +// running in parallel are played one after the other. +func (c *Controller) UseHost() (restore func()) { + hostMu.Lock() + + links := c.Links() + fileExists, evalSymlinks := megaraid.CustomFileExists, megaraid.CustomEvalSymlinks + + megaraid.CustomFileExists = func(p string) bool { + _, ok := links[p] + + return ok + } + megaraid.CustomEvalSymlinks = func(p string) (string, error) { + if dev, ok := links[p]; ok { + return dev, nil + } + + return "", os.ErrNotExist + } + + return func() { + megaraid.CustomFileExists, megaraid.CustomEvalSymlinks = fileExists, evalSymlinks + + hostMu.Unlock() + } +} + +func (c *Controller) loadCapture() error { + ctrl, err := c.read("c0") + if err != nil { + return err + } + + for _, row := range rows(responseData(ctrl), "PD LIST") { + slot := str(row["EID:Slt"]) + + pd, err := c.read(driveFile(slot)) + if err != nil { + return err + } + + attrs := driveAttributes(pd, slot) + c.drives[slot] = &drive{ + state: str(row["State"]), group: row["DG"], capturedGroup: row["DG"], + deviceID: int(num(row["DID"])), serial: str(attrs["SN"]), wwn: str(attrs["WWN"]), + } + } + + for _, row := range rows(responseData(ctrl), "VD LIST") { + if err := c.loadVolume(row); err != nil { + return err + } + } + + return nil +} + +// loadVolume reads a virtual drive of the VD LIST and marks its drives. +func (c *Controller) loadVolume(row doc) error { + id := volumeID(row) + + vd, err := c.read("c0_v" + id) + if err != nil { + return err + } + + for _, pd := range rows(responseData(vd), "PDs for VD "+id) { + if d, ok := c.drives[str(pd["EID:Slt"])]; ok { + d.volume = id + } + } + + props := object(responseData(vd)["VD"+id+" Properties"]) + c.volumes[id] = &volume{ + state: str(row["State"]), device: str(props["OS Drive Name"]), + wwn: str(props["SCSI NAA Id"]), exposed: str(props["Exposed to OS"]) == yes, + } + + return nil +} + +func (c *Controller) apply(e scenario.Event) error { + d, ok := c.drives[e.Slot] + if !ok { + return errors.Errorf("unknown drive %s", e.Slot) + } + + v, ok := c.volumes[d.volume] + if !ok { + return errors.Errorf("drive %s is in no volume of the capture", e.Slot) + } + + if err := checkOrder(e, d.state == failed, v.deleted); err != nil { + return err + } + + switch e.Kind { + case scenario.DriveFails: + d.state, d.serial = failed, d.serial[:min(len(d.serial), failedSerialLength)] + v.state, v.exposed = "OfLn", false + case scenario.DriveReplaced: + v.deleted = true + d.state, d.serial, d.wwn, d.group = "UGood", e.Serial, e.WWN, noGroup + d.deviceID = c.newDeviceID() + case scenario.VolumeCreated: + d.state, d.group = "Onln", d.capturedGroup + v.deleted, v.state, v.exposed, v.device, v.wwn = false, "Optl", true, e.Device, e.VolumeWWN + default: + return errors.Errorf("event %d not replayed", e.Kind) + } + + return nil +} + +// checkOrder rejects an event that cannot happen to a drive in its current +// state: only a drive in a volume can fail, only a failed drive can be +// replaced, and a volume can only be created once the old one is deleted. +func checkOrder(e scenario.Event, failed, volumeDeleted bool) error { + switch { + case e.Kind == scenario.DriveFails && (failed || volumeDeleted): + return errors.Errorf("drive %s cannot fail: it has failed or is in no volume", e.Slot) + case e.Kind == scenario.DriveReplaced && !failed: + return errors.Errorf("drive %s cannot be replaced: it has not failed", e.Slot) + case e.Kind == scenario.VolumeCreated && !volumeDeleted: + return errors.Errorf("no volume can be created on drive %s: its volume still exists", e.Slot) + } + + return nil +} + +// newDeviceID is the device ID the controller gives a new drive. +func (c *Controller) newDeviceID() int { + id := 0 + for _, d := range c.drives { + id = max(id, d.deviceID+1) + } + + return id +} + +func (c *Controller) render(selector string) (doc, error) { + switch { + case selector == "show all": + return c.renderOverview() + case selector == "/c0": + return c.renderController() + case strings.HasPrefix(selector, "/c0/v"): + return c.renderVolume(strings.TrimPrefix(selector, "/c0/v")) + case strings.HasPrefix(selector, "/c0/e"): + return c.renderDrive(selector) + default: + return nil, errors.Errorf("selector %s not replayed", selector) + } +} + +func (c *Controller) renderOverview() (doc, error) { + out, err := c.read("show_all") + if err != nil { + return nil, err + } + + volumes, notOptimal, failedDrives := c.counts() + + for _, row := range rows(responseData(out), "System Overview") { + row["VDs"], row["DGs"], row["VNOpt"], row["DNOpt"] = volumes, volumes, notOptimal, failedDrives + } + + return out, nil +} + +// counts returns the number of volumes, of volumes not optimal and of failed +// drives, as shown by the system overview. +func (c *Controller) counts() (volumes, notOptimal, failedDrives int) { + for _, v := range c.volumes { + if v.deleted { + continue + } + + volumes++ + + if v.state != "Optl" { + notOptimal++ + } + } + + for _, d := range c.drives { + if d.state == failed { + failedDrives++ + } + } + + return volumes, notOptimal, failedDrives +} + +func (c *Controller) renderController() (doc, error) { + out, err := c.read("c0") + if err != nil { + return nil, err + } + + data := responseData(out) + + for _, row := range rows(data, "PD LIST") { + d, err := c.drive(str(row["EID:Slt"])) + if err != nil { + return nil, err + } + + d.update(row) + } + + var vds []any + + for _, row := range rows(data, "VD LIST") { + v, err := c.volume(volumeID(row)) + if err != nil { + return nil, err + } + + if !v.deleted { + row["State"] = v.state + vds = append(vds, row) + } + } + + data["VD LIST"] = vds + + return out, nil +} + +func (c *Controller) renderDrive(selector string) (doc, error) { + // "/c0/e251/s3" -> "251:3" + parts := strings.Split(selector, "/") + if len(parts) != 4 { //nolint:mnd // "", "c0", "eE", "sS" + return nil, errors.Errorf("selector %s not replayed", selector) + } + + slot := strings.TrimPrefix(parts[2], "e") + ":" + strings.TrimPrefix(parts[3], "s") + + d, err := c.drive(slot) + if err != nil { + return nil, err + } + + out, err := c.read(driveFile(slot)) + if err != nil { + return nil, err + } + + for _, row := range rows(responseData(out), "Drive "+selector) { + d.update(row) + } + + attrs := driveAttributes(out, slot) + attrs["SN"], attrs["WWN"] = d.serial, d.wwn + + return out, nil +} + +func (c *Controller) renderVolume(id string) (doc, error) { + v, ok := c.volumes[id] + if !ok || v.deleted { + return nil, errors.Wrapf(errVolumeDeleted, "VD %s", id) + } + + out, err := c.read("c0_v" + id) + if err != nil { + return nil, err + } + + data := responseData(out) + + for _, row := range rows(data, "/c0/v"+id) { + row["State"] = v.state + } + + for _, row := range rows(data, "PDs for VD "+id) { + d, err := c.drive(str(row["EID:Slt"])) + if err != nil { + return nil, err + } + + d.update(row) + } + + props := object(data["VD"+id+" Properties"]) + props["SCSI NAA Id"] = v.wwn + + exposed := no + if v.exposed { + exposed = yes + props["OS Drive Name"] = v.device + } else { + delete(props, "OS Drive Name") + } + + props["Exposed to OS"], props["Is LD Ready for OS Requests"] = exposed, exposed + + return out, nil +} + +// drive returns a drive of the capture. A drive missing from it means an +// inconsistent capture. +func (c *Controller) drive(slot string) (*drive, error) { + d, ok := c.drives[slot] + if !ok { + return nil, errors.Errorf("unknown drive %s", slot) + } + + return d, nil +} + +// volume returns a virtual drive of the capture. A volume missing from it +// means an inconsistent capture. +func (c *Controller) volume(id string) (*volume, error) { + v, ok := c.volumes[id] + if !ok { + return nil, errors.Errorf("unknown virtual drive %s", id) + } + + return v, nil +} + +// update writes the drive state into a storcli drive row. +func (d *drive) update(row doc) { + row["State"], row["DG"], row["DID"] = d.state, d.group, d.deviceID +} + +func (c *Controller) read(name string) (doc, error) { + data, err := captures.ReadFile(path.Join(c.capture, name+".json")) + if err != nil { + return nil, errors.Wrapf(err, "no capture for %s", name) + } + + var out doc + if err := json.Unmarshal(data, &out); err != nil { + return nil, errors.Wrapf(err, "failed to parse capture %s", name) + } + + return out, nil +} + +func responseData(out doc) doc { + controllers := array(out["Controllers"]) + if len(controllers) == 0 { + return doc{} + } + + ctrl := object(controllers[0]) + data := object(ctrl["Response Data"]) + + return data +} + +func driveAttributes(out doc, slot string) doc { + sel := strings.Replace("/c0/e"+slot, ":", "/s", 1) + details := object(responseData(out)["Drive "+sel+" - Detailed Information"]) + attrs := object(details["Drive "+sel+" Device attributes"]) + + return attrs +} + +func rows(data doc, key string) []doc { + items := array(data[key]) + out := make([]doc, 0, len(items)) + + for _, item := range items { + if row, ok := item.(doc); ok { + out = append(out, row) + } + } + + return out +} + +// driveFile is the capture file of "/c0/eE/sS show all". +func driveFile(slot string) string { + return "c0_e" + strings.Replace(slot, ":", "_s", 1) +} + +// volumeID reads the virtual drive ID of a "DG/VD" row. +func volumeID(row doc) string { + dgvd := str(row["DG/VD"]) + + return dgvd[strings.Index(dgvd, "/")+1:] +} + +func str(v any) string { + if v == nil { + return "" + } + + return fmt.Sprint(v) +} + +func num(v any) float64 { + if f, ok := v.(float64); ok { + return f + } + + return 0 +} + +// object returns v as a JSON object, or an empty one. +func object(v any) doc { + if o, ok := v.(doc); ok { + return o + } + + return doc{} +} + +// array returns v as a JSON array, or nil. +func array(v any) []any { + if a, ok := v.([]any); ok { + return a + } + + return nil +} diff --git a/scenario/megaraidsim/megaraidsim_internal_test.go b/scenario/megaraidsim/megaraidsim_internal_test.go new file mode 100644 index 0000000..32367eb --- /dev/null +++ b/scenario/megaraidsim/megaraidsim_internal_test.go @@ -0,0 +1,38 @@ +package megaraidsim + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestRunInconsistentCapture checks that a capture listing a drive or a +// volume the controller does not know fails with an error, not a panic. +func TestRunInconsistentCapture(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + forget func(c *Controller) + args []string + wantErr string + }{ + {name: "drive", forget: func(c *Controller) { delete(c.drives, "251:3") }, args: []string{"/c0", "show", "all"}, wantErr: "unknown drive 251:3"}, + {name: "volume", forget: func(c *Controller) { delete(c.volumes, "237") }, args: []string{"/c0", "show", "all"}, wantErr: "unknown virtual drive 237"}, + {name: "drive of a volume", forget: func(c *Controller) { delete(c.drives, "251:3") }, args: []string{"/c0/v237", "show", "all"}, wantErr: "unknown drive 251:3"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, err := New("megaraid9560-4hdd") + require.NoError(t, err) + + tt.forget(c) + + _, err = c.Run(tt.args) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} diff --git a/scenario/report.go b/scenario/report.go new file mode 100644 index 0000000..e6bf899 --- /dev/null +++ b/scenario/report.go @@ -0,0 +1,80 @@ +package scenario + +import ( + "github.com/pkg/errors" + + "github.com/scality/raidmgmt/pkg/domain/entities/logicalvolume" + "github.com/scality/raidmgmt/pkg/domain/entities/physicaldrive" + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" +) + +// Inventory is what a RAID controller adapter reports. +type Inventory interface { + Controllers() ([]*raidcontroller.RAIDController, error) + PhysicalDrives(metadata *raidcontroller.Metadata) ([]*physicaldrive.PhysicalDrive, error) + LogicalVolumes(metadata *raidcontroller.Metadata) ([]*logicalvolume.LogicalVolume, error) +} + +// Report returns what an adapter reports for a controller, as scenario disks. +// The controller must be listed first, as disk-management-agent discovers +// controllers before their drives. Each drive comes with the paths of the +// volume holding it, or its own paths when it is in no volume (JBOD). When the +// volumes cannot be listed, the drives are reported without volume paths, as +// disk-management-agent does, and the error is returned as volumesErr. +func Report(inv Inventory, ctrl *raidcontroller.Metadata) (disks []Disk, volumesErr, err error) { + ctrls, err := inv.Controllers() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to list controllers") + } + + if !hasController(ctrls, ctrl.ID) { + return nil, nil, errors.Errorf("controller %d not listed", ctrl.ID) + } + + pds, err := inv.PhysicalDrives(ctrl) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to list physical drives") + } + + lvs, err := inv.LogicalVolumes(ctrl) + if err != nil { + volumesErr = errors.Wrap(err, "failed to list logical volumes") + } + + paths := map[string][2]string{} + + for _, lv := range lvs { + for _, pd := range lv.PDrivesMetadata { + paths[pd.ID] = [2]string{lv.DevicePath, lv.PermanentPath} + } + } + + disks = make([]Disk, 0, len(pds)) + + for _, pd := range pds { + devicePath, permanentPath := pd.DevicePath, pd.PermanentPath + if p, ok := paths[pd.ID]; ok { + devicePath, permanentPath = p[0], p[1] + } + + disks = append(disks, Disk{ + Slot: pd.ID, + Status: pd.Status.String(), + Serial: pd.Serial, + DevicePath: devicePath, + PermanentPath: permanentPath, + }) + } + + return disks, volumesErr, nil +} + +func hasController(ctrls []*raidcontroller.RAIDController, id int) bool { + for _, c := range ctrls { + if c.Metadata != nil && c.ID == id { + return true + } + } + + return false +} diff --git a/scenario/scenario.go b/scenario/scenario.go new file mode 100644 index 0000000..9a996f9 --- /dev/null +++ b/scenario/scenario.go @@ -0,0 +1,304 @@ +// Package scenario replays hardware scenarios, such as a drive failing and +// being replaced, from real RAID controller output, so that what raidmgmt +// reports can be tested without the hardware. +// +// Scenarios are Gherkin features under features/, tagged with the controller +// family playing them. Each scenario starts from a capture of a real +// controller, plays events on its drives (a drive fails, is replaced, gets a +// new volume) and gives, as a table, the disks expected. A backend per +// controller family (see megaraidsim) replays the capture with the events +// applied, as that controller shows them; Run plays the features through it. +// +// An expected table can be followed by a known bug step giving the table +// reported today because of it: the step passes while that table is +// reported, and fails both when the expected table is reported (the bug is +// fixed, remove the known bug step) and when neither is (the behaviour +// changed). +package scenario + +import ( + "fmt" + "sort" + "strings" + + "github.com/pkg/errors" +) + +type ( + // Expectation is the table of disks expected after some events. + Expectation struct { + Expect Table + // KnownBug documents a bug that makes what is reported differ from + // Expect. + KnownBug *KnownBug + } + + // KnownBug is a bug that makes an expectation report something else than + // its expected table. + KnownBug struct { + Ticket string + // Reported is what is reported today because of the bug. + Reported Table + } + + // Event is something happening to a drive. A backend applies it as its + // controller shows it, in the controller's own vocabulary. + Event struct { + Kind EventKind + // Slot is the drive, as the controller names it. + Slot string + // Serial and WWN are the ones of the new drive, for DriveReplaced. + Serial, WWN string + // Device and VolumeWWN are the block device of the new volume and the + // identifier behind its /dev/disk/by-id/wwn-* link, for + // VolumeCreated. + Device, VolumeWWN string + } + + // EventKind is what happens to a drive. + EventKind int + + // Disk is what is reported for one physical drive. + Disk struct { + Slot string + Status string + Serial string + DevicePath string + PermanentPath string + } + + // Table is a list of disks, written in a feature as a data table with + // the columns of tableColumns. An empty cell is an empty value. + Table []Disk +) + +const ( + // DriveFails: the drive fails and the volume holding it goes offline, + // no longer a block device of the host. + DriveFails EventKind = iota + 1 + // DriveReplaced: the volume of the drive is deleted and a new drive, + // in no volume, is inserted in its slot. + DriveReplaced + // VolumeCreated: the volume the drive had in the capture is created + // again on it. + VolumeCreated +) + +// tableColumns are the columns of a Table, in order. +var tableColumns = []string{ //nolint:gochecknoglobals // constant list + "slot", "status", "serial", "devicePath", "permanentPath", +} + +// ParseTable reads a table of disks from the rows of a data table, the first +// one being the header. A table without disk is rejected: it would accept an +// adapter reporting no drive at all. +func ParseTable(rows [][]string) (Table, error) { + if len(rows) == 0 || strings.Join(rows[0], " ") != strings.Join(tableColumns, " ") { + return nil, errors.Errorf("table header must be | %s |", strings.Join(tableColumns, " | ")) + } + + if len(rows) == 1 { + return nil, errors.New("table has no disk") + } + + table := make(Table, 0, len(rows)-1) + seen := map[string]bool{} + + for _, row := range rows[1:] { + disk := Disk{ + Slot: row[0], Status: row[1], Serial: row[2], + DevicePath: row[3], PermanentPath: row[4], + } + + if disk.Slot == "" { + return nil, errors.New("row without slot") + } + + if seen[disk.Slot] { + return nil, errors.Errorf("slot %s already in the table", disk.Slot) + } + + seen[disk.Slot] = true + + table = append(table, disk) + } + + return table, nil +} + +// Diff lists the differences between the disks of a table and the disks +// reported, one line per field, ordered by slot. +func (t Table) Diff(got []Disk) []string { + want := indexBySlot(t) + reported := indexBySlot(got) + + var diffs []string + + // indexBySlot keeps one disk per slot: a drive reported twice must show. + counts := map[string]int{} + for _, d := range got { + counts[d.Slot]++ + } + + for _, slot := range sortedSlots(want, reported) { + if counts[slot] > 1 { + diffs = append(diffs, fmt.Sprintf("%s: reported %d times", slot, counts[slot])) + } + + w, inWant := want[slot] + g, inGot := reported[slot] + + switch { + case !inGot: + diffs = append(diffs, slot+": not reported") + case !inWant: + diffs = append(diffs, slot+": reported but not in the table") + default: + diffs = append(diffs, diffDisk(slot, w, g)...) + } + } + + return diffs +} + +// Check compares the disks reported with the tables of an expectation and +// returns an error when the outcome is not the expected one. Without known +// bug, the expected table must be reported. With a known bug, the table of +// the bug must be reported; the differences with the expected table are then +// returned as notes. +func (e *Expectation) Check(got []Disk) (notes []string, err error) { + diffs := e.Expect.Diff(got) + + if e.KnownBug == nil { + if len(diffs) > 0 { + return nil, errors.Errorf("the disks reported differ from the expected table:\n%s\n"+ + "reported:\n%s", strings.Join(diffs, "\n"), FormatTable(got)) + } + + return nil, nil + } + + if len(diffs) == 0 { + return nil, errors.Errorf("the expected table is reported, not the one of %s: "+ + "the bug looks fixed, remove its known bug step", e.KnownBug.Ticket) + } + + if bugDiffs := e.KnownBug.Reported.Diff(got); len(bugDiffs) > 0 { + return nil, errors.Errorf("the disks reported match neither the expected table "+ + "nor the one of %s:\ndifferences with the table of %s:\n%s\nreported:\n%s", + e.KnownBug.Ticket, e.KnownBug.Ticket, strings.Join(bugDiffs, "\n"), FormatTable(got)) + } + + return diffs, nil +} + +// FormatTable writes disks as a data table, ready to paste in a feature. +func FormatTable(disks []Disk) string { + rows := make([][]string, 0, len(disks)+1) + rows = append(rows, tableColumns) + + for _, d := range indexedSorted(disks) { + rows = append(rows, []string{d.Slot, d.Status, d.Serial, d.DevicePath, d.PermanentPath}) + } + + widths := make([]int, len(tableColumns)) + + for _, row := range rows { + for i, cell := range row { + widths[i] = max(widths[i], len(cell)) + } + } + + lines := make([]string, 0, len(rows)) + + for _, row := range rows { + cells := make([]string, len(row)) + for i, cell := range row { + cells[i] = fmt.Sprintf("%-*s", widths[i], cell) + } + + lines = append(lines, "| "+strings.Join(cells, " | ")+" |") + } + + return strings.Join(lines, "\n") + "\n" +} + +func diffDisk(slot string, want, got Disk) []string { + var diffs []string + + for _, f := range []struct{ name, want, got string }{ + {"status", want.Status, got.Status}, + {"serial", want.Serial, got.Serial}, + {"devicePath", want.DevicePath, got.DevicePath}, + {"permanentPath", want.PermanentPath, got.PermanentPath}, + } { + if f.want != f.got { + diffs = append(diffs, fmt.Sprintf("%s %s: got %s, want %s", + slot, f.name, quote(f.got), quote(f.want))) + } + } + + return diffs +} + +func indexBySlot(disks []Disk) map[string]Disk { + bySlot := make(map[string]Disk, len(disks)) + for _, d := range disks { + bySlot[d.Slot] = d + } + + return bySlot +} + +func sortedSlots(maps ...map[string]Disk) []string { + seen := map[string]bool{} + + var slots []string + + for _, m := range maps { + for slot := range m { + if !seen[slot] { + seen[slot] = true + + slots = append(slots, slot) + } + } + } + + sort.Slice(slots, func(i, j int) bool { return slotLess(slots[i], slots[j]) }) + + return slots +} + +func indexedSorted(disks []Disk) []Disk { + sorted := append([]Disk(nil), disks...) + sort.Slice(sorted, func(i, j int) bool { return slotLess(sorted[i].Slot, sorted[j].Slot) }) + + return sorted +} + +// slotLess orders slots such as "251:2" and "251:10" numerically per part. +func slotLess(a, b string) bool { + pa, pb := strings.Split(a, ":"), strings.Split(b, ":") + + for i := 0; i < len(pa) && i < len(pb); i++ { + if pa[i] != pb[i] { + if len(pa[i]) != len(pb[i]) { + return len(pa[i]) < len(pb[i]) + } + + return pa[i] < pb[i] + } + } + + return len(pa) < len(pb) +} + +// quote shows an empty value as "" in a difference. +func quote(s string) string { + if s == "" { + return `""` + } + + return s +} diff --git a/scenario/steps.go b/scenario/steps.go new file mode 100644 index 0000000..63a4a47 --- /dev/null +++ b/scenario/steps.go @@ -0,0 +1,209 @@ +package scenario + +import ( + "context" + "embed" + "regexp" + "strings" + "testing" + + "github.com/cucumber/godog" + "github.com/pkg/errors" +) + +//go:embed features/*.feature +var features embed.FS //nolint:gochecknoglobals // embedded feature files + +// Player returns what is reported for a captured controller once events +// happened, in order. +type Player func(ctx context.Context, capture string, events []Event) ([]Disk, error) + +// Run plays, as subtests of t, the scenarios of the features tagged +// @: each "Then the drives are reported as:" step calls play with +// the capture and the events of its scenario so far. It fails when no +// scenario is tagged for the controller. +func Run(t *testing.T, controller string, play Player) { + t.Helper() + + ts, played := suite(controller, play, &godog.Options{ + Format: "pretty", TestingT: t, FS: features, Paths: []string{"features"}, + }) + status := ts.Run() + + if status != 0 && !t.Failed() { + t.Fatalf("godog exited with status %d", status) + } + + if *played == 0 { + t.Fatalf("no scenario tagged @%s", controller) + } +} + +// suite returns the godog suite of the scenarios of opts.Paths tagged +// @, and the number of scenarios it played once run. Undefined +// steps fail. +func suite(controller string, play Player, opts *godog.Options) (godog.TestSuite, *int) { + played := new(int) + + opts.Tags = "@" + controller + opts.Strict = true + + return godog.TestSuite{ + Name: controller, + ScenarioInitializer: func(sc *godog.ScenarioContext) { + (&player{play: play, played: played}).register(sc) + }, + Options: opts, + }, played +} + +// Steps. Events read the same as a precondition ("Given drive 251:3 has +// failed") and as an action ("When drive 251:3 fails"). +// +//nolint:gochecknoglobals // compiled once +var ( + capturedStep = regexp.MustCompile(`^the controller captured as "([^"]+)"$`) + failsStep = regexp.MustCompile(`^drive (\S+) (?:fails|has failed)$`) + replacedStep = regexp.MustCompile( + `^drive (\S+) (?:is|has been) replaced by a drive with serial "([^"]+)" and WWN "([^"]+)"$`) + volumeStep = regexp.MustCompile( + `^a volume (?:is|has been) created on drive (\S+), exposed as "([^"]+)" with WWN "([^"]+)"$`) + reportedStep = regexp.MustCompile(`^the drives are reported as:$`) + knownBugStep = regexp.MustCompile(`^known bug ([A-Z][A-Z0-9]*-[0-9]+) reports instead:$`) +) + +// player holds the state of one scenario. +type player struct { + play Player + played *int + capture string + events []Event + // knownBugs are the known bugs following a reported step, by step ID. + knownBugs map[string]*KnownBug + // step is the ID of the step being run. + step string +} + +func (p *player) register(sc *godog.ScenarioContext) { + sc.Before(func(ctx context.Context, s *godog.Scenario) (context.Context, error) { + *p.played++ + + return ctx, withoutStack(p.collectKnownBugs(s)) + }) + sc.StepContext().Before(func(ctx context.Context, st *godog.Step) (context.Context, error) { + p.step = st.Id + + return ctx, nil + }) + + sc.Given(capturedStep, func(capture string) { p.capture = capture }) + sc.Step(failsStep, func(slot string) { + p.events = append(p.events, Event{Kind: DriveFails, Slot: slot}) + }) + sc.Step(replacedStep, func(slot, serial, wwn string) { + p.events = append(p.events, Event{Kind: DriveReplaced, Slot: slot, Serial: serial, WWN: wwn}) + }) + sc.Step(volumeStep, func(slot, device, wwn string) { + p.events = append(p.events, + Event{Kind: VolumeCreated, Slot: slot, Device: device, VolumeWWN: wwn}) + }) + sc.Then(reportedStep, func(ctx context.Context, table *godog.Table) error { + return withoutStack(p.drivesReported(ctx, table)) + }) + // Checked with the reported step before it. + sc.Then(knownBugStep, func(string, *godog.Table) {}) +} + +// collectKnownBugs reads the known bug steps of a scenario, each one checked +// by the reported step right before it. +func (p *player) collectKnownBugs(s *godog.Scenario) error { + p.knownBugs = map[string]*KnownBug{} + + for i, st := range s.Steps { + m := knownBugStep.FindStringSubmatch(st.Text) + if m == nil { + continue + } + + if i == 0 || !reportedStep.MatchString(s.Steps[i-1].Text) { + return errors.Errorf("%q must follow a %q step", st.Text, "the drives are reported as:") + } + + reported, err := ParseTable(stepRows(st)) + if err != nil { + return errors.Wrapf(err, "known bug %s", m[1]) + } + + p.knownBugs[s.Steps[i-1].Id] = &KnownBug{Ticket: m[1], Reported: reported} + } + + return nil +} + +func (p *player) drivesReported(ctx context.Context, table *godog.Table) error { + if p.capture == "" { + return errors.New("no capture: start with \"Given the controller captured as\"") + } + + expect, err := ParseTable(tableRows(table)) + if err != nil { + return err + } + + got, err := p.play(ctx, p.capture, p.events) + if err != nil { + return err + } + + e := Expectation{Expect: expect, KnownBug: p.knownBugs[p.step]} + + notes, err := e.Check(got) + if err != nil { + return err + } + + if len(notes) > 0 { + godog.Logf(ctx, "known bug %s reproduced, differences with the expected table:\n%s", + e.KnownBug.Ticket, strings.Join(notes, "\n")) + } + + return nil +} + +// messageError is an error without stack trace, which godog would print below +// its message. +type messageError string + +func (e messageError) Error() string { return string(e) } + +// withoutStack drops the stack trace of an error. +func withoutStack(err error) error { + if err == nil { + return nil + } + + return messageError(err.Error()) +} + +func tableRows(table *godog.Table) [][]string { + out := make([][]string, 0, len(table.Rows)) + + for _, row := range table.Rows { + cells := make([]string, 0, len(row.Cells)) + for _, cell := range row.Cells { + cells = append(cells, cell.Value) + } + + out = append(out, cells) + } + + return out +} + +func stepRows(st *godog.Step) [][]string { + if st.Argument == nil || st.Argument.DataTable == nil { + return nil + } + + return tableRows(st.Argument.DataTable) +} diff --git a/scenario/steps_internal_test.go b/scenario/steps_internal_test.go new file mode 100644 index 0000000..82169a3 --- /dev/null +++ b/scenario/steps_internal_test.go @@ -0,0 +1,80 @@ +package scenario + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/cucumber/godog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// reported is what the fake controller of the testdata features reports. +var reported = []Disk{{Slot: "251:1", Status: "Failed", Serial: "A"}} //nolint:gochecknoglobals // test data + +// playFixture runs testdata/.feature on a fake controller reporting +// got, and returns the godog status, its output and the events of each play. +func playFixture(t *testing.T, name string, got []Disk) (status int, output string, plays [][]Event) { + t.Helper() + + play := func(_ context.Context, capture string, events []Event) ([]Disk, error) { + assert.Equal(t, "fake", capture) + + plays = append(plays, events) + + return got, nil + } + + var out strings.Builder + + ts, played := suite("fake", play, &godog.Options{ + FS: os.DirFS("testdata"), Paths: []string{name + ".feature"}, Output: &out, Format: "progress", + }) + status = ts.Run() + + require.NotZero(t, *played, "%s: no scenario played", name) + + return status, out.String(), plays +} + +// TestStepsPass checks the features that must pass, and that each scenario +// plays its own events only. +func TestStepsPass(t *testing.T) { + status, out, plays := playFixture(t, "passes", reported) + require.Zero(t, status, out) + assert.Equal(t, [][]Event{{{Kind: DriveFails, Slot: "251:1"}}, nil}, plays) + + status, out, _ = playFixture(t, "known-bug-reproduced", reported) + require.Zero(t, status, "known bug reproduced:\n%s", out) +} + +// TestStepsFail checks that the harness fails when it should: otherwise a +// broken check would pass every scenario. +func TestStepsFail(t *testing.T) { + tests := []struct { + fixture string + got []Disk + wantErr string + }{ + {"differs", reported, "251:1 status: got Failed, want Used"}, + {"not-reported", reported, "251:2: not reported"}, + {"passes", append(reported, reported...), "251:1: reported 2 times"}, + {"known-bug-fixed", reported, "the bug looks fixed"}, + {"known-bug-changed", reported, "251:1 serial: got A, want B"}, + {"known-bug-misplaced", reported, "must follow"}, + {"undefined-step", reported, "undefined"}, + {"duplicate-slot", reported, "slot 251:1 already in the table"}, + {"wrong-header", reported, "table header must be"}, + {"no-capture", reported, "no capture"}, + } + + for _, tt := range tests { + t.Run(tt.fixture, func(t *testing.T) { + status, out, _ := playFixture(t, tt.fixture, tt.got) + require.NotZero(t, status, "want a failure, got status 0:\n%s", out) + assert.Contains(t, out, tt.wantErr) + }) + } +} diff --git a/scenario/testdata/differs.feature b/scenario/testdata/differs.feature new file mode 100644 index 0000000..7596d50 --- /dev/null +++ b/scenario/testdata/differs.feature @@ -0,0 +1,10 @@ +@fake +Feature: differs + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | A | | | diff --git a/scenario/testdata/duplicate-slot.feature b/scenario/testdata/duplicate-slot.feature new file mode 100644 index 0000000..9ae5ba8 --- /dev/null +++ b/scenario/testdata/duplicate-slot.feature @@ -0,0 +1,11 @@ +@fake +Feature: duplicate slot + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/known-bug-changed.feature b/scenario/testdata/known-bug-changed.feature new file mode 100644 index 0000000..5b33fae --- /dev/null +++ b/scenario/testdata/known-bug-changed.feature @@ -0,0 +1,13 @@ +@fake +Feature: known bug changed + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | A | | | + But known bug BUG-1 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | B | | | diff --git a/scenario/testdata/known-bug-fixed.feature b/scenario/testdata/known-bug-fixed.feature new file mode 100644 index 0000000..01759a4 --- /dev/null +++ b/scenario/testdata/known-bug-fixed.feature @@ -0,0 +1,13 @@ +@fake +Feature: known bug fixed + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | + But known bug BUG-1 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | A | | | diff --git a/scenario/testdata/known-bug-misplaced.feature b/scenario/testdata/known-bug-misplaced.feature new file mode 100644 index 0000000..3c6b81e --- /dev/null +++ b/scenario/testdata/known-bug-misplaced.feature @@ -0,0 +1,14 @@ +@fake +Feature: known bug misplaced + + Background: + Given the controller captured as "fake" + + Scenario: x + When drive 251:1 fails + But known bug BUG-1 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/known-bug-reproduced.feature b/scenario/testdata/known-bug-reproduced.feature new file mode 100644 index 0000000..f2b1513 --- /dev/null +++ b/scenario/testdata/known-bug-reproduced.feature @@ -0,0 +1,13 @@ +@fake +Feature: known bug reproduced + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Used | A | | | + But known bug BUG-1 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/no-capture.feature b/scenario/testdata/no-capture.feature new file mode 100644 index 0000000..f3c2c77 --- /dev/null +++ b/scenario/testdata/no-capture.feature @@ -0,0 +1,7 @@ +@fake +Feature: no capture + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/not-reported.feature b/scenario/testdata/not-reported.feature new file mode 100644 index 0000000..fe87164 --- /dev/null +++ b/scenario/testdata/not-reported.feature @@ -0,0 +1,11 @@ +@fake +Feature: not reported + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | + | 251:2 | Used | B | | | diff --git a/scenario/testdata/passes.feature b/scenario/testdata/passes.feature new file mode 100644 index 0000000..f4204c4 --- /dev/null +++ b/scenario/testdata/passes.feature @@ -0,0 +1,16 @@ +@fake +Feature: passes + + Background: + Given the controller captured as "fake" + + Scenario: a drive fails + When drive 251:1 fails + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | + + Scenario: nothing happens + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/undefined-step.feature b/scenario/testdata/undefined-step.feature new file mode 100644 index 0000000..39dde65 --- /dev/null +++ b/scenario/testdata/undefined-step.feature @@ -0,0 +1,11 @@ +@fake +Feature: undefined step + + Background: + Given the controller captured as "fake" + + Scenario: x + When drive 251:1 burns + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 251:1 | Failed | A | | | diff --git a/scenario/testdata/wrong-header.feature b/scenario/testdata/wrong-header.feature new file mode 100644 index 0000000..c3537d1 --- /dev/null +++ b/scenario/testdata/wrong-header.feature @@ -0,0 +1,10 @@ +@fake +Feature: wrong header + + Background: + Given the controller captured as "fake" + + Scenario: x + Then the drives are reported as: + | slot | status | + | 251:1 | Failed | From 61c993baf68e352d9311ce1ac5d1dd92af0b4202 Mon Sep 17 00:00:00 2001 From: Guillaume Carre Date: Mon, 12 Oct 2026 16:09:29 +0200 Subject: [PATCH 2/3] test(scenario): play the disk failure scenarios on storcli2 controllers Add storcli2sim, the backend of storcli2/perccli2 controllers: it answers the commands of the raidmgmt storcli2 adapter (controller listing, drives and volumes) from a capture with events applied. storcli2 reports a drive status apart from its state: a failed drive keeps state Conf with status Failed, a replacement drive is UConf with status Good. Drive groups are written as numbers, or "-" for an unconfigured drive. The capture is the storcli2 test data of this repository, kept to 4 RAID0 drives, with fake serials, WWNs, SAS addresses, volume and controller identifiers. The storcli2-disk-failure feature plays the megaraid-disk-failure scenarios with the same steps. The healthy drives keep their paths when 306:2 fails, but the offline volume of the failed drive is still reported with a permanent path built from its NAA Id, although it has no block device: documented as a known bug of ARTESCA-17960. Refs: ARTESCA-17960 --- scenario/README.md | 14 +- .../features/storcli2-disk-failure.feature | 59 ++ scenario/storcli2sim/adapter_test.go | 125 +++ .../captures/storcli2-4hdd/c0.json | 821 ++++++++++++++++++ .../captures/storcli2-4hdd/c0_aso.json | 82 ++ .../captures/storcli2-4hdd/c0_autoconfig.json | 33 + .../captures/storcli2-4hdd/c0_eall_sall.json | 461 ++++++++++ .../captures/storcli2-4hdd/c0_vall.json | 269 ++++++ .../captures/storcli2-4hdd/show_all.json | 43 + scenario/storcli2sim/storcli2sim.go | 436 ++++++++++ .../storcli2sim/storcli2sim_internal_test.go | 43 + 11 files changed, 2383 insertions(+), 3 deletions(-) create mode 100644 scenario/features/storcli2-disk-failure.feature create mode 100644 scenario/storcli2sim/adapter_test.go create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/c0.json create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/c0_aso.json create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/c0_autoconfig.json create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/c0_eall_sall.json create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/c0_vall.json create mode 100644 scenario/storcli2sim/captures/storcli2-4hdd/show_all.json create mode 100644 scenario/storcli2sim/storcli2sim.go create mode 100644 scenario/storcli2sim/storcli2sim_internal_test.go diff --git a/scenario/README.md b/scenario/README.md index 97dbbee..422f821 100644 --- a/scenario/README.md +++ b/scenario/README.md @@ -5,9 +5,10 @@ replaced, from real controller output, and check what raidmgmt reports, without the hardware. Scenarios are Gherkin features in [`features/`](features), one per -controller family and story, tagged with the family (`@megaraid`). The -`Background` names the capture of a real controller; each `Scenario` plays -events on its drives and gives, as a table, the disks expected: +controller family and story, tagged with the family (`@megaraid`, +`@storcli2`). The `Background` names the capture of a real controller; +each `Scenario` plays events on its drives and gives, as a table, the disks +expected: ```gherkin @megaraid @@ -58,6 +59,8 @@ Feature: MegaRAID disk failure and replacement - `megaraidsim/adapter_test.go` plays every `@megaraid` scenario (storcli64) through the raidmgmt megaraid adapter of this repository, with `scenario.Run`. +- `storcli2sim/adapter_test.go` plays every `@storcli2` scenario through the + raidmgmt storcli2 adapter. - `steps_internal_test.go` checks the steps themselves on the features of [`testdata/`](testdata), in particular that they fail when they should. @@ -91,3 +94,8 @@ handling in every backend. kept to the first 4 (251:1-4, volumes 239-236) to keep scenarios short. Serials, WWNs and controller identifiers are replaced by fake ones, also in the hex inquiry data; keep it so when adding a capture. +- `storcli2sim/captures/storcli2-4hdd`: storcli2 controller, SEAGATE + ST10000NM018B drives in enclosure 306, one RAID0 volume each (storcli2 + 008.0005). Captured with `storcli2 show all J` on 24 drives, + kept to the first 4 (306:0-3, volumes 1-4), with the same fake identifiers + (drive serials, WWNs and SAS addresses, volume NAA Ids and serials). diff --git a/scenario/features/storcli2-disk-failure.feature b/scenario/features/storcli2-disk-failure.feature new file mode 100644 index 0000000..f581d07 --- /dev/null +++ b/scenario/features/storcli2-disk-failure.feature @@ -0,0 +1,59 @@ +@storcli2 +Feature: storcli2 disk failure and replacement + The storcli2 counterpart of megaraid-disk-failure: a controller driven by + storcli2 with HDDs in enclosure 306, each in its own RAID0 volume, as + ARTESCA deploys them. Drive 306:2 fails, is replaced, then gets a new + volume (ARTESCA-17960). + + The capture is real storcli2 output of a lab node while all its drives + were healthy, kept to its first 4 drives, with serials, WWNs and volume + identifiers replaced by fake ones. + + Background: + Given the controller captured as "storcli2-4hdd" + + Scenario: All drives are healthy + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 306:0 | Used | SIMD2001000000000000 | /dev/sdb | /dev/disk/by-id/wwn-0x600062b2000000000000000000000001 | + | 306:1 | Used | SIMD2002000000000000 | /dev/sdc | /dev/disk/by-id/wwn-0x600062b2000000000000000000000002 | + | 306:2 | Used | SIMD2003000000000000 | /dev/sdd | /dev/disk/by-id/wwn-0x600062b2000000000000000000000003 | + | 306:3 | Used | SIMD2004000000000000 | /dev/sde | /dev/disk/by-id/wwn-0x600062b2000000000000000000000004 | + + Scenario: A failed drive does not take the paths of the others + When drive 306:2 fails + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 306:0 | Used | SIMD2001000000000000 | /dev/sdb | /dev/disk/by-id/wwn-0x600062b2000000000000000000000001 | + | 306:1 | Used | SIMD2002000000000000 | /dev/sdc | /dev/disk/by-id/wwn-0x600062b2000000000000000000000002 | + | 306:2 | Failed | SIMD2003000000000000 | | | + | 306:3 | Used | SIMD2004000000000000 | /dev/sde | /dev/disk/by-id/wwn-0x600062b2000000000000000000000004 | + # The permanent path of the offline volume is still built from its NAA + # Id, although the volume has no block device and no by-id link. + But known bug ARTESCA-17960 reports instead: + | slot | status | serial | devicePath | permanentPath | + | 306:0 | Used | SIMD2001000000000000 | /dev/sdb | /dev/disk/by-id/wwn-0x600062b2000000000000000000000001 | + | 306:1 | Used | SIMD2002000000000000 | /dev/sdc | /dev/disk/by-id/wwn-0x600062b2000000000000000000000002 | + | 306:2 | Failed | SIMD2003000000000000 | | /dev/disk/by-id/wwn-0x600062b2000000000000000000000003 | + | 306:3 | Used | SIMD2004000000000000 | /dev/sde | /dev/disk/by-id/wwn-0x600062b2000000000000000000000004 | + + Scenario: A replacement drive is reported as a new, available drive + Given drive 306:2 has failed + When drive 306:2 is replaced by a drive with serial "SIMD2013000000000000" and WWN "5000C50020000013" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 306:0 | Used | SIMD2001000000000000 | /dev/sdb | /dev/disk/by-id/wwn-0x600062b2000000000000000000000001 | + | 306:1 | Used | SIMD2002000000000000 | /dev/sdc | /dev/disk/by-id/wwn-0x600062b2000000000000000000000002 | + | 306:2 | UnassignedGood | SIMD2013000000000000 | | | + | 306:3 | Used | SIMD2004000000000000 | /dev/sde | /dev/disk/by-id/wwn-0x600062b2000000000000000000000004 | + + Scenario: A replacement drive gets the paths of its new volume + Given drive 306:2 has failed + And drive 306:2 has been replaced by a drive with serial "SIMD2013000000000000" and WWN "5000C50020000013" + When a volume is created on drive 306:2, exposed as "/dev/sdf" with WWN "600062b2000000000000000000001003" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 306:0 | Used | SIMD2001000000000000 | /dev/sdb | /dev/disk/by-id/wwn-0x600062b2000000000000000000000001 | + | 306:1 | Used | SIMD2002000000000000 | /dev/sdc | /dev/disk/by-id/wwn-0x600062b2000000000000000000000002 | + | 306:2 | Used | SIMD2013000000000000 | /dev/sdf | /dev/disk/by-id/wwn-0x600062b2000000000000000000001003 | + | 306:3 | Used | SIMD2004000000000000 | /dev/sde | /dev/disk/by-id/wwn-0x600062b2000000000000000000000004 | diff --git a/scenario/storcli2sim/adapter_test.go b/scenario/storcli2sim/adapter_test.go new file mode 100644 index 0000000..4280ce3 --- /dev/null +++ b/scenario/storcli2sim/adapter_test.go @@ -0,0 +1,125 @@ +package storcli2sim_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/cucumber/godog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" + raidcontrollers "github.com/scality/raidmgmt/pkg/implementation/raidcontroller" + "github.com/scality/raidmgmt/scenario" + "github.com/scality/raidmgmt/scenario/storcli2sim" +) + +const capture = "storcli2-4hdd" + +// TestScenarios plays every @storcli2 scenario through the raidmgmt storcli2 +// adapter of this repository and checks the drives it reports, with the +// device and permanent paths of the volume holding each drive. +func TestScenarios(t *testing.T) { + scenario.Run(t, "storcli2", func(ctx context.Context, capture string, events []scenario.Event) ([]scenario.Disk, error) { + ctrl, err := storcli2sim.New(capture, events...) + if err != nil { + return nil, err + } + + disks, volumesErr, err := scenario.Report(raidcontrollers.NewStorCLI2(ctrl), &raidcontroller.Metadata{ID: 0}) + if volumesErr != nil { + godog.Logf(ctx, "LogicalVolumes: %v", volumesErr) + } + + return disks, err + }) +} + +// TestNewRejects checks that New fails on an unknown capture or drive, and on +// events in an order that cannot happen to a drive. +func TestNewRejects(t *testing.T) { + t.Parallel() + + fails := scenario.Event{Kind: scenario.DriveFails, Slot: "306:2"} + replaced := scenario.Event{Kind: scenario.DriveReplaced, Slot: "306:2", Serial: "S", WWN: "W"} + created := scenario.Event{Kind: scenario.VolumeCreated, Slot: "306:2", Device: "/dev/sdz", VolumeWWN: "1"} + + tests := []struct { + name string + capture string + events []scenario.Event + wantErr string + }{ + {name: "unknown capture", capture: "no-such-capture", wantErr: "failed to load capture"}, + {name: "unknown drive", capture: capture, events: []scenario.Event{{Kind: scenario.DriveFails, Slot: "9:9"}}, wantErr: "unknown drive"}, + {name: "fails twice", capture: capture, events: []scenario.Event{fails, fails}, wantErr: "cannot fail"}, + {name: "replaced while healthy", capture: capture, events: []scenario.Event{replaced}, wantErr: "cannot be replaced"}, + {name: "volume created on a failed drive", capture: capture, events: []scenario.Event{fails, created}, wantErr: "still exists"}, + {name: "replacement fails before its volume", capture: capture, events: []scenario.Event{fails, replaced, fails}, wantErr: "cannot fail"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := storcli2sim.New(tt.capture, tt.events...) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +// TestDriveGroupIsNumeric checks that a drive group is written as storcli2 +// prints it: "-" for an unconfigured drive, else a number. +func TestDriveGroupIsNumeric(t *testing.T) { + t.Parallel() + + fails := scenario.Event{Kind: scenario.DriveFails, Slot: "306:2"} + replaced := scenario.Event{Kind: scenario.DriveReplaced, Slot: "306:2", Serial: "S", WWN: "W"} + created := scenario.Event{Kind: scenario.VolumeCreated, Slot: "306:2", Device: "/dev/sdf"} + + tests := []struct { + name string + events []scenario.Event + want any + }{ + {name: "replaced", events: []scenario.Event{fails, replaced}, want: "-"}, + {name: "recreated", events: []scenario.Event{fails, replaced, created}, want: float64(2)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ctrl, err := storcli2sim.New(capture, tt.events...) + require.NoError(t, err) + + out, err := ctrl.Run([]string{"/c0/eall/sall", "show", "all"}) + require.NoError(t, err) + + var doc struct { + Controllers []struct { + ResponseData struct { + Drives []struct { + Information map[string]any `json:"Drive Information"` + } `json:"Drives List"` + } `json:"Response Data"` + } + } + require.NoError(t, json.Unmarshal(out, &doc)) + require.NotEmpty(t, doc.Controllers) + + var found bool + + for _, d := range doc.Controllers[0].ResponseData.Drives { + if d.Information["EID:Slt"] == "306:2" { + found = true + + assert.Equal(t, tt.want, d.Information["DG"]) + } + } + + assert.True(t, found, "drive 306:2 not listed") + }) + } +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/c0.json b/scenario/storcli2sim/captures/storcli2-4hdd/c0.json new file mode 100644 index 0000000..260aa48 --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/c0.json @@ -0,0 +1,821 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.115.1.el8_10.x86_64", + "Controller": "0", + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Basics": { + "Product Name": "MegaRAID 9660-16i Tri-Mode Storage Adapter", + "Board Name": "MR 9660-16i ", + "Board Assembly": "03-50107-00004 ", + "Board Tracer Number": "SIM0000002", + "Board Revision": "00004 ", + "Chip Name": "SAS4116 ", + "Chip Revision": "B0 ", + "Board Mfg Date(yyyy/mm/dd)": "2024/12/12", + "Board Rework Date(yyyy/mm/dd)": "0000/00/00", + "Custom Serial Number": " ", + "SAS Address": "0x500062b200000000", + "Serial Number": "SIM0000002", + "Controller Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/29 14:06:17", + "System Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/29 14:06:17", + "OEM": "Broadcom", + "SubOEMID": 0, + "PCI Slot Number": 19 + }, + "Version": { + "Package Version": "8.12.1.0-00000-00003", + "Firmware Version": "8.12.1.0-00000-00003", + "Firmware Security Version Number": "00.00.00.00", + "FMC Version": "8.12.1.0-00000-00003", + "FMC Security Version Number": "00.00.00.00", + "BSP Version": "8.12.1.0-00000-00003", + "BSP Security Version Number": "00.00.00.00", + "BIOS Version": "0x080C0400", + "BIOS Security Version Number": "00.00.00.00", + "HIIM Version": "08.12.04.00", + "HIIM Security Version Number": "00.00.00.00", + "HIIA Version": "08.12.04.00", + "HIIA Security Version Number": "00.00.00.00", + "PSOC Hardware Version": "6.64", + "PSOC Firmware Version": "28.00", + "PSOC Part Number": "25953-280-aaa", + "NVDATA Version": "0C.0B.00.58", + "Driver Name": "mpi3mr", + "Driver Version": "8.5.0.0.50", + "SL8 Library Version": "08.0507.0000" + }, + "HostInterface": { + "Max PCIe Link Rate": "0x08 (16GT/s)", + "Max PCIe Port Width": 8, + "PCI Address": "00:16:00:0", + "PCIe Link Width": "X8 Lane(s)" + }, + "DeviceInterface": { + "SAS/SATA": "SAS/SATA-6G, SAS-12G, SAS-22.5G", + "PCIe": "PCIE-2.5GT, PCIE-5GT, PCIE-8GT, PCIE-16GT", + "PCI Vendor ID": "0x1000", + "PCI Device ID": "0x00A5", + "PCI Subsystem Vendor ID": "0x1000", + "PCI Subsystem ID": "0x4620" + }, + "Status": { + "Controller Status": "Optimal", + "VD Count": 24, + "VD Degraded Count": 0, + "VD Offline Count": 0, + "PD Count": 26, + "PD Drive Count": 24, + "Critical Predictive Failure PD Drive Count": 0, + "Failed PD Drive Count": 0, + "Memory Correctable Errors": 0, + "Memory Uncorrectable Errors": 0, + "ECC Bucket Count": 0, + "Preserved Cache Present": "No", + "System Reboot Required": "No", + "System Shutdown Required": "No", + "Controller Reset Required": "No", + "Security Key Assigned": "No", + "Security Type": "None", + "Failed to get Security key on bootup": "No", + "Bios was not detected during boot": "No", + "Boot Time Secret Key Provider": "OOB,In-band", + "EKM Key Provider": "OOB,In-band", + "Controller must be rebooted to complete security operation": "No", + "Controller has booted into safe mode": "No", + "Controller has trial features enabled": "No", + "Personality Change Pending": "No", + "Security Key Rekey Pending": "No", + "Reconfigure types": "Change the format option only for existing name-spaces", + "Controller Personality": "RAID" + }, + "Supported Controller operations": { + "Support Enclosure Affinity": "Yes", + "Support Foreign Config Import": "Yes", + "Support Foreign Config Clear": "Yes", + "Support Self Diagnostic Check": "Yes", + "Support Diag Retention Test": "Yes", + "Support Cache Offload Info": "Yes", + "FW and Event Time in GMT": "Yes", + "Support Abort CC on Error": "Yes", + "Support Multipath": "Yes", + "Support Security": "Yes", + "Support Security Suggest": "Yes", + "Support Portable Security Key": "No", + "Support Security Key Per PD": "No", + "Support LKM": "Yes, Supported using both in-band and out-of-band command", + "Support EKM": "Yes, Supported using both in-band and out-of-band command", + "Support LKM to EKM": "Yes", + "Support EKM to LKM": "No", + "Support PFK": "Yes", + "Support Electronic PFK": "No", + "Support Ldbbm": "Yes", + "Support Shield State": "Yes", + "Support SSD Ld Disk Cache Change": "Yes", + "Support Operation Suspend Resume": "Yes", + "Support Emergency Spares": "Yes", + "Support Device Mgmt PEL": "Yes", + "Support CC Schedule": "Yes", + "Support PatrolRead": "Yes", + "Support SSD PatrolRead": "Yes", + "Support Resize Array": "Yes", + "Support Drive Performance Monitoring": "Yes", + "Support Emulated Drives": "Yes", + "Support Limited Dedicated HotSpares": "Yes", + "Support Factory Defaults set": "Yes", + "Support Personality": "Yes", + "Support NVCache Info Get": "Yes", + "Support NVCache Erase": "Yes", + "Support Cache Offload Encryption": "No", + "Support CacheBypass Modes": "Yes", + "Support Purge Cache During VD Delete": "Yes", + "Support BoardLogic Update": "Yes", + "Support Ibuttonless": "Yes", + "Support Host Info": "Yes", + "Support Cache-Vault Health Info": "Yes", + "Support PCIe Devices": "Yes", + "Support Extended MPB VPD pages": "Yes", + "Support Snapdump": "Yes", + "Support Energy Pack": "Yes", + "Support Energy Pack VPD": "Yes", + "Support Multiple Security keys(One Per PD)": "No", + "Support Platform Security": "Yes", + "Support SESCtrl In Multipath Config": "No", + "Support R6 Individual PD Suspend/Resume": "No", + "Support SAS Config": "Yes", + "Support limiting of SAS Link speed": "Yes, Supports only max link speed limit control of a SAS Phy", + "Support limiting of SAS Link speed per Phy": "Yes", + "Support PCIe Config": "Yes", + "Support limiting of PCIe Link Speed": "Yes", + "Support limiting of PCIe Link Speed per Link": "Yes", + "Support PCIe Config Lane Mapping": "Yes", + "Support Immediate Auto-configure": "Yes", + "Support NVMe Recover": "Yes", + "Support NVMe Reconfigure": "No", + "Support PCIe Clock mode": "Yes", + "Support Enhanced Scheduling": "Yes" + }, + "External Key Management": { + "Capability": "Supported", + "Boot Agent Status": "Not Available", + "Configured": "No" + }, + "Supported PD operations": { + "Support Force Online": "Yes", + "Support Make Offline": "Yes", + "Support Make Failed": "No", + "Support SpinDown Unconfigured": "Yes", + "Support SpinDown HotSpare": "Yes", + "Support SpinDown Configured": "No", + "Support T10 Power State": "Yes", + "Support Temperature Monitoring": "Yes", + "Support WCE": "Yes", + "Support Degraded Media Detection": "Yes", + "Support PD Secure Erase": "Yes", + "Support SSD Wear Gauge": "No", + "Support Mark Missing": "Yes", + "Support Replace Missing": "Yes", + "Support Prepare For Removal": "Yes", + "Support Drive Power State Change": "Yes, Supports only Spin-Up" + }, + "Supported VD operations": { + "Support Changing Read Policy": "Yes", + "Support Changing Write Policy": "Yes", + "Support Changing Disk Cache Policy": "Yes", + "Support Online Capacity Expansion": "Yes, with VD Expansion and addition of Drives only", + "Support LDBBM": "Yes", + "Support Secure Erase": "Yes", + "Support Unmap for single drive RAID 0 VDs": "Yes", + "Support Unmap for RAID 0 VDs": "Yes", + "Support Unmap for RAID 1/10 VDs": "Yes", + "Support Unmap for RAID 5/50/6/60 VDs": "Yes", + "Support Writesame Unmap for single drive RAID 0 VDs": "Yes", + "Support Writesame Unmap for RAID 0 VDs": "Yes", + "Support Writesame Unmap for RAID 1/10 VDs": "Yes", + "Support Writesame Unmap for RAID 5/50/6/60 VDs": "Yes", + "Support T10 Atomicity": "No" + }, + "HwCfg": { + "NVRAM": "Present", + "NVRAM Size(KiB)": 128, + "DDR Memory Size(MiB)": 4096, + "Flash": "Present", + "NOR Flash Size(MiB)": 32, + "CacheVault Flash Size(MiB)": 22170, + "OCM Memory": "Present", + "OCM Memory Size(MiB)": 15, + "Current Size of FW Cache(MiB)": 3877, + "DDR Width": "72-bit", + "Energy Pack": "Present", + "Upgrade Key": "Absent", + "Upgrade Key Slot": "Present", + "On Board Expander": "Absent", + "Temperature Sensor for Chip": "Present", + "Temperature Sensor for Board": "Present", + "Upgradable Board Logic": "Present", + "PCI Switch": "Absent", + "Serial Debugger": "Present", + "Chip temperature(C)": 62, + "Board temperature(C)": 46, + "Controller Fan": "Absent" + }, + "Max. Supported Config": { + "Max Number of VDs": 240, + "Max Number of Physical Drives": 240, + "Max SAS/SATA Drives": 240, + "Max NVMe Drives": 32, + "Max Arrays": 240, + "Max PD Per Array": 32, + "Max Spans Per VD": 8, + "Max Dedicated HSPs": 64, + "Max Global HSPs": 64, + "Max VDs Per Array": 16, + "Max Phys": 16, + "Max JBODs": 240, + "Max RAID Configurable PDs": 240, + "Max Complex RAID VDs": 64, + "Max Data Transfer Size(Bytes)": 1048576, + "Max Parallel Commands": 8192, + "Max NS/LU per JBOD": 4, + "Max NS/LU per RAID PDs": 1, + "Max Supported LUNs for SAS PDs": 4, + "Max Namespaces": 1, + "Max Persistent Id": 1024, + "Max VD's per Array in configuration": 16, + "Max Key Id length": 256, + "Max Security Key length": 32, + "Max Secret Key length": 32 + }, + "Properties": { + "Patrol Read Rate(%)": 30, + "BGI Rate(%)": 30, + "Consistency Check Rate(%)": 30, + "OCE Rate(%)": 30, + "Drive Coercion Mode": "0 (None)", + "Auto Rebuild": "Yes", + "Energy Pack Warning": "No", + "Data Loss Warning": "Yes", + "Ecc Bucket Size(Entries)": 15, + "ECC Bucket Leak Rate(minute(s))": 1440, + "Expose Enclosure Devices": "Yes", + "Maintain Drive Fail History": "Yes", + "Maintain Drive Fail History - JBOD": "Yes", + "All Online Controller Reset": "Yes", + "Auto Online Controller Reset": "Yes", + "Abort CC on Error": "No", + "Replace Drive": "Yes", + "HDD SMARTer Enabled": "Yes", + "SSD SMARTer Enabled": "Yes", + "PR Correct UnConfigured Areas": "Yes", + "Support SSD Patrol Read": "Yes", + "SpinDown Unconfigured Drive": "Yes", + "Boot With Preserved Cache": "Yes", + "SpinDown HotSpare": "No", + "Emergency GHS": "Yes", + "Emergency UG": "Yes", + "Emergency SMARTer": "Yes", + "Fail On SMARTer": "No", + "LED management for JBOD": "Yes", + "SES VPD Association": "TargetPort", + "Base Enclosure Level": "Enclosure level enumeration start with Zero", + "SMART/Temperature PollInterval-For External PDs only(second(s))": 300, + "SMART/Temperature PollInterval-For Internal PDs only(second(s))": 300, + "SpinDown Time(minute(s))": 30, + "Spinup Drive Count": 2, + "Spinup Delay(second(s))": 2, + "Spinup Encl Drive Count": 4, + "Spinup Enclosure Delay(second(s))": 12, + "Drive Detection Type": "Disabled", + "Drive Corrective Action": "Only Log Events", + "Drive Error Threshold": "Every 8 Hours", + "Boot Mode": "COE", + "Name": "", + "SmartPoll - RAID PDs": "Yes", + "SmartPoll - JBOD PDs": "Yes", + "PD Temperature Poll": "Yes", + "Security on JBOD": "Yes", + "Host Managed Security on JBOD": "No" + }, + "Capabilities": { + "Supported Drive Interfaces": "SAS, SATA, NVMe", + "Supported RAID Levels": "RAID0, RAID1, RAID5, RAID6, RAID10, RAID50, RAID60", + "Mix of SAS-HDD/SATA-HDD in VD": "Allowed", + "Mix of SAS-SSD/SATA-SSD in VD": "Allowed", + "Mix of SSD/HDD in VD": "Not Allowed", + "Mix of SED type(Enterprise,OPAL,and RUBY) for Security Enabled Arrays": "Not Allowed", + "Mix of different NS/LU count in a VD": "Not Allowed", + "Mix of NVMe SGL and PRP in a VD": "Allowed" + }, + "NVCache Information": { + "NVCache Flash Capacity(MiB)": 22170, + "Bad Block Capacity(MiB)": 135, + "Allowable bad blocks consumed(%)": 19 + }, + "Scheduled Tasks": { + "Patrol Read Execution Frequency(hours)": "168", + "Next Patrol Read Start time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/06/04 17:00:00" + }, + "Secure Boot Details": { + "Secure Boot State": "Enabled", + "Secure Boot Mode": "Hard Secure", + "Total Number Of Key Slots": 8, + "Number Of Key Slots Used": 1, + "Remaining Key Slots": 7, + "Current Key Encryption Algorithm": "RSA2048", + "Key Hash Size": "SHA256", + "Key Hash Version": "SHA3", + "Security Version Number": "00.00.00.00" + }, + "Security Protocol Details": { + "Security Protocol": "SPDM-1.2.0,1.1.0,1.0.0" + }, + "Drive Groups": 4, + "TOPOLOGY": [ + { + "DG": 0, + "Span": "-", + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 0, + "Span": 0, + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 0, + "Span": 0, + "Row": 0, + "EID:Slot": "306:0", + "PID": 293, + "Type": "DRIVE", + "State": "Conf", + "Status": "Online", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "-" + }, + { + "DG": 1, + "Span": "-", + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 1, + "Span": 0, + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 1, + "Span": 0, + "Row": 0, + "EID:Slot": "306:1", + "PID": 294, + "Type": "DRIVE", + "State": "Conf", + "Status": "Online", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "-" + }, + { + "DG": 2, + "Span": "-", + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 2, + "Span": 0, + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 2, + "Span": 0, + "Row": 0, + "EID:Slot": "306:2", + "PID": 295, + "Type": "DRIVE", + "State": "Conf", + "Status": "Online", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "-" + }, + { + "DG": 3, + "Span": "-", + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 3, + "Span": 0, + "Row": "-", + "EID:Slot": "-", + "PID": "-", + "Type": "RAID0", + "State": "-", + "Status": "-", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "N" + }, + { + "DG": 3, + "Span": 0, + "Row": 0, + "EID:Slot": "306:3", + "PID": 296, + "Type": "DRIVE", + "State": "Conf", + "Status": "Online", + "BT": "N", + "Size": "9.094 TiB", + "PDC": "dsbl", + "Secured": "N", + "FSpace": "-" + } + ], + "Virtual Drives": 4, + "VD LIST": [ + { + "DG/VD": "0/1", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + { + "DG/VD": "1/2", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + { + "DG/VD": "2/3", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + { + "DG/VD": "3/4", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + } + ], + "Physical Drives": 4, + "PD LIST": [ + { + "EID:Slt": "306:0", + "PID": 293, + "State": "Conf", + "Status": "Online", + "DG": 0, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + { + "EID:Slt": "306:1", + "PID": 294, + "State": "Conf", + "Status": "Online", + "DG": 1, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + { + "EID:Slt": "306:2", + "PID": 295, + "State": "Conf", + "Status": "Online", + "DG": 2, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + { + "EID:Slt": "306:3", + "PID": 296, + "State": "Conf", + "Status": "Online", + "DG": 3, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + } + ], + "LU/NS LIST": [ + { + "PID": 293, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 294, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 295, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 296, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 297, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 298, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 299, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 300, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 301, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 302, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 303, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 304, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 307, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 308, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 309, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 310, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 311, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 312, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 313, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 314, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 315, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 316, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 317, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + }, + { + "PID": 318, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + } + ], + "Enclosures": 2, + "Enclosure List": [ + { + "EID": 306, + "State": "OK", + "DeviceType": "Enclosure", + "Slots": 12, + "PD": 12, + "Partner-EID": "-", + "Multipath": "No", + "PS": 1, + "Fans": 0, + "TSs": 3, + "Alms": 1, + "SIM": 0, + "ProdID": "380-23710-3001 " + }, + { + "EID": 320, + "State": "OK", + "DeviceType": "Enclosure", + "Slots": 12, + "PD": 12, + "Partner-EID": "-", + "Multipath": "No", + "PS": 1, + "Fans": 0, + "TSs": 3, + "Alms": 1, + "SIM": 0, + "ProdID": "380-23710-3001 " + } + ], + "Energy Pack Info": [ + { + "Type": "Supercap", + "SubType": "FBU345", + "Voltage(mV)": 9184, + "Temperature(C)": 38, + "Status": "Optimal" + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/c0_aso.json b/scenario/storcli2sim/captures/storcli2-4hdd/c0_aso.json new file mode 100644 index 0000000..1c58b9f --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/c0_aso.json @@ -0,0 +1,82 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.124.1.el8_10.x86_64", + "Controller": "0", + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Advanced Software options": [ + { + "Software option": "RAID Personality", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "RAID 0", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "RAID 1/10", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "RAID 6/60", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "RAID 5/50", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "JBOD", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "Max RAID Capable Drives 240", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "Max Drives 240", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "Max SAS/SATA drives 240", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "Max NVMe Drives 32", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + }, + { + "Software option": "Max Virtual Drives 240", + "Time Remaining": "Unlimited", + "Location": "Factory Installed" + } + ], + "Key Information": [ + { + "Prop": "SafeID", + "Description": "SIMULATEDSAFEID" + }, + { + "Prop": "KeyID", + "Description": "SIMULATEDKEYID" + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/c0_autoconfig.json b/scenario/storcli2sim/captures/storcli2-4hdd/c0_autoconfig.json new file mode 100644 index 0000000..554d197 --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/c0_autoconfig.json @@ -0,0 +1,33 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.124.1.el8_10.x86_64", + "Controller": "0", + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Auto-config Information": [ + { + "Auto-config property": "Primary Auto-configure behavior", + "Value": "UGood" + }, + { + "Auto-config property": "Secondary Auto-configure behavior", + "Value": "UGood" + }, + { + "Auto-config property": "Supported Primary Auto-configure behavior", + "Value": "UGood, JBOD, R0, R0WB, SecureJBOD, SecureR0, SecureR0WB" + }, + { + "Auto-config property": "Supported Secondary Auto-configure behavior", + "Value": "UGood" + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/c0_eall_sall.json b/scenario/storcli2sim/captures/storcli2-4hdd/c0_eall_sall.json new file mode 100644 index 0000000..f7861a8 --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/c0_eall_sall.json @@ -0,0 +1,461 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.115.1.el8_10.x86_64", + "Controller": "0", + "Status": "Success", + "Description": "Show Drive Information Succeeded." + }, + "Response Data": { + "Drives List": [ + { + "Drive Information": { + "EID:Slt": "306:0", + "PID": 293, + "State": "Conf", + "Status": "Online", + "DG": 0, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + "LU/NS Information": [ + { + "PID": 293, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + } + ], + "Drive Detailed Information": { + "Shield Counter": 0, + "Temperature(C)": 40, + "Serial Number": "SIMD2001000000000000", + "Vendor": "SEAGATE ", + "Model": "ST10000NM018B ", + "WWN": "5000C50020000001", + "Firmware Revision Level": "E001", + "Firmware Release Number": "01110618", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "Capable Speed": "12.0Gb/s", + "Capable Link Width": "x1", + "Negotiated Link Width": "x1", + "Drive position": "DriveGroup:0, Span:0, Row:0", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Successful Shield Diagnostics completed on(Localtime yyyy/mm/dd hh:mm:sec)": "NA", + "SED Capable": "No", + "ISE Capable": "No", + "Unmap Capable for Single Drive RAID 0 VDs": "No", + "Unmap Capable for RAID 0 VDs": "No", + "Unmap Capable for RAID 1 VDs": "No", + "Unmap Capable for RAID 5/6 VDs": "No", + "Unmap Capable": "No", + "Writesame Unmap Capable for Single Drive RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 1 VDs": "No", + "Writesame Unmap Capable for RAID 5/6 VDs": "No", + "Writesame Unmap Capable": "No", + "T10 Power Mode": "Yes", + "Needs EKM Attention": "No", + "Secured By EKM": "No", + "Certified": "Yes", + "Supported Data Format": "None", + "Hard Disk Drive RPM": 7200, + "Device port count": 1, + "Path Information": [ + { + "SASAddress": "0x5000c50020000001", + "DevicePID": 293, + "Path": "Primary", + "ConnectorIndex": 1, + "Negotiated Speed": "12.0Gb/s", + "Num Phys/Lanes used": 1 + } + ], + "Connector Information": [ + { + "ConnId": 1, + "Name": "C0.0", + "Location": "Internal", + "Type": "SFF-8654 8I" + }, + { + "ConnId": 1, + "Name": "C0.1", + "Location": "Internal", + "Type": "SFF-8654 8I" + } + ], + "LU/NS Properties": { + "Media Error Count": 0, + "Other Error Count": 0, + "Predictive Failure Count": 0, + "Last Predictive Failure Event Sequence Number": 0, + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "FW managed drive security": "No", + "Secured": "No", + "Locked": "No", + "PI Formatted": "No", + "PI type": 1, + "Number of bytes of user data in LBA": "512B", + "Current Write Cache": "Off", + "Default Write Cache": "On", + "Write Cache Changeable": "Yes" + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 30 30 30 30 4e 4d 30 31 38 42 20 20 20 45 30 30 31 53 49 4d 44 32 30 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 60 20 e0 04 60 04 c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 31 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + }, + { + "Drive Information": { + "EID:Slt": "306:1", + "PID": 294, + "State": "Conf", + "Status": "Online", + "DG": 1, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + "LU/NS Information": [ + { + "PID": 294, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + } + ], + "Drive Detailed Information": { + "Shield Counter": 0, + "Temperature(C)": 43, + "Serial Number": "SIMD2002000000000000", + "Vendor": "SEAGATE ", + "Model": "ST10000NM018B ", + "WWN": "5000C50020000002", + "Firmware Revision Level": "E001", + "Firmware Release Number": "01110618", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "Capable Speed": "12.0Gb/s", + "Capable Link Width": "x1", + "Negotiated Link Width": "x1", + "Drive position": "DriveGroup:1, Span:0, Row:0", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Successful Shield Diagnostics completed on(Localtime yyyy/mm/dd hh:mm:sec)": "NA", + "SED Capable": "No", + "ISE Capable": "No", + "Unmap Capable for Single Drive RAID 0 VDs": "No", + "Unmap Capable for RAID 0 VDs": "No", + "Unmap Capable for RAID 1 VDs": "No", + "Unmap Capable for RAID 5/6 VDs": "No", + "Unmap Capable": "No", + "Writesame Unmap Capable for Single Drive RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 1 VDs": "No", + "Writesame Unmap Capable for RAID 5/6 VDs": "No", + "Writesame Unmap Capable": "No", + "T10 Power Mode": "Yes", + "Needs EKM Attention": "No", + "Secured By EKM": "No", + "Certified": "Yes", + "Supported Data Format": "None", + "Hard Disk Drive RPM": 7200, + "Device port count": 1, + "Path Information": [ + { + "SASAddress": "0x5000c50020000002", + "DevicePID": 294, + "Path": "Primary", + "ConnectorIndex": 1, + "Negotiated Speed": "12.0Gb/s", + "Num Phys/Lanes used": 1 + } + ], + "Connector Information": [ + { + "ConnId": 1, + "Name": "C0.0", + "Location": "Internal", + "Type": "SFF-8654 8I" + }, + { + "ConnId": 1, + "Name": "C0.1", + "Location": "Internal", + "Type": "SFF-8654 8I" + } + ], + "LU/NS Properties": { + "Media Error Count": 0, + "Other Error Count": 0, + "Predictive Failure Count": 0, + "Last Predictive Failure Event Sequence Number": 0, + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "FW managed drive security": "No", + "Secured": "No", + "Locked": "No", + "PI Formatted": "No", + "PI type": 1, + "Number of bytes of user data in LBA": "512B", + "Current Write Cache": "Off", + "Default Write Cache": "On", + "Write Cache Changeable": "Yes" + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 30 30 30 30 4e 4d 30 31 38 42 20 20 20 45 30 30 31 53 49 4d 44 32 30 30 32 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 60 20 e0 04 60 04 c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 31 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + }, + { + "Drive Information": { + "EID:Slt": "306:2", + "PID": 295, + "State": "Conf", + "Status": "Online", + "DG": 2, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + "LU/NS Information": [ + { + "PID": 295, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + } + ], + "Drive Detailed Information": { + "Shield Counter": 0, + "Temperature(C)": 45, + "Serial Number": "SIMD2003000000000000", + "Vendor": "SEAGATE ", + "Model": "ST10000NM018B ", + "WWN": "5000C50020000003", + "Firmware Revision Level": "E001", + "Firmware Release Number": "01110618", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "Capable Speed": "12.0Gb/s", + "Capable Link Width": "x1", + "Negotiated Link Width": "x1", + "Drive position": "DriveGroup:2, Span:0, Row:0", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Successful Shield Diagnostics completed on(Localtime yyyy/mm/dd hh:mm:sec)": "NA", + "SED Capable": "No", + "ISE Capable": "No", + "Unmap Capable for Single Drive RAID 0 VDs": "No", + "Unmap Capable for RAID 0 VDs": "No", + "Unmap Capable for RAID 1 VDs": "No", + "Unmap Capable for RAID 5/6 VDs": "No", + "Unmap Capable": "No", + "Writesame Unmap Capable for Single Drive RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 1 VDs": "No", + "Writesame Unmap Capable for RAID 5/6 VDs": "No", + "Writesame Unmap Capable": "No", + "T10 Power Mode": "Yes", + "Needs EKM Attention": "No", + "Secured By EKM": "No", + "Certified": "Yes", + "Supported Data Format": "None", + "Hard Disk Drive RPM": 7200, + "Device port count": 1, + "Path Information": [ + { + "SASAddress": "0x5000c50020000003", + "DevicePID": 295, + "Path": "Primary", + "ConnectorIndex": 1, + "Negotiated Speed": "12.0Gb/s", + "Num Phys/Lanes used": 1 + } + ], + "Connector Information": [ + { + "ConnId": 1, + "Name": "C0.0", + "Location": "Internal", + "Type": "SFF-8654 8I" + }, + { + "ConnId": 1, + "Name": "C0.1", + "Location": "Internal", + "Type": "SFF-8654 8I" + } + ], + "LU/NS Properties": { + "Media Error Count": 0, + "Other Error Count": 0, + "Predictive Failure Count": 0, + "Last Predictive Failure Event Sequence Number": 0, + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "FW managed drive security": "No", + "Secured": "No", + "Locked": "No", + "PI Formatted": "No", + "PI type": 1, + "Number of bytes of user data in LBA": "512B", + "Current Write Cache": "Off", + "Default Write Cache": "On", + "Write Cache Changeable": "Yes" + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 30 30 30 30 4e 4d 30 31 38 42 20 20 20 45 30 30 31 53 49 4d 44 32 30 30 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 60 20 e0 04 60 04 c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 31 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + }, + { + "Drive Information": { + "EID:Slt": "306:3", + "PID": 296, + "State": "Conf", + "Status": "Online", + "DG": 3, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + }, + "LU/NS Information": [ + { + "PID": 296, + "LUN/NSID": "0/-", + "Status": "Online", + "Size": "9.094 TiB" + } + ], + "Drive Detailed Information": { + "Shield Counter": 0, + "Temperature(C)": 41, + "Serial Number": "SIMD2004000000000000", + "Vendor": "SEAGATE ", + "Model": "ST10000NM018B ", + "WWN": "5000C50020000004", + "Firmware Revision Level": "E001", + "Firmware Release Number": "01110618", + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "Capable Speed": "12.0Gb/s", + "Capable Link Width": "x1", + "Negotiated Link Width": "x1", + "Drive position": "DriveGroup:3, Span:0, Row:0", + "Sequence Number": 2, + "Commissioned Spare": "No", + "Emergency Spare": "No", + "Successful Shield Diagnostics completed on(Localtime yyyy/mm/dd hh:mm:sec)": "NA", + "SED Capable": "No", + "ISE Capable": "No", + "Unmap Capable for Single Drive RAID 0 VDs": "No", + "Unmap Capable for RAID 0 VDs": "No", + "Unmap Capable for RAID 1 VDs": "No", + "Unmap Capable for RAID 5/6 VDs": "No", + "Unmap Capable": "No", + "Writesame Unmap Capable for Single Drive RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 0 VDs": "No", + "Writesame Unmap Capable for RAID 1 VDs": "No", + "Writesame Unmap Capable for RAID 5/6 VDs": "No", + "Writesame Unmap Capable": "No", + "T10 Power Mode": "Yes", + "Needs EKM Attention": "No", + "Secured By EKM": "No", + "Certified": "Yes", + "Supported Data Format": "None", + "Hard Disk Drive RPM": 7200, + "Device port count": 1, + "Path Information": [ + { + "SASAddress": "0x5000c50020000004", + "DevicePID": 296, + "Path": "Primary", + "ConnectorIndex": 1, + "Negotiated Speed": "12.0Gb/s", + "Num Phys/Lanes used": 1 + } + ], + "Connector Information": [ + { + "ConnId": 1, + "Name": "C0.0", + "Location": "Internal", + "Type": "SFF-8654 8I" + }, + { + "ConnId": 1, + "Name": "C0.1", + "Location": "Internal", + "Type": "SFF-8654 8I" + } + ], + "LU/NS Properties": { + "Media Error Count": 0, + "Other Error Count": 0, + "Predictive Failure Count": 0, + "Last Predictive Failure Event Sequence Number": 0, + "Logical Sector Size": "512B", + "Physical Sector Size": "4 KiB", + "Raw size": "9.095 TiB [0x48c400000 Sectors]", + "Coerced size": "9.094 TiB [0x48c300000 Sectors]", + "FW managed drive security": "No", + "Secured": "No", + "Locked": "No", + "PI Formatted": "No", + "PI type": 1, + "Number of bytes of user data in LBA": "512B", + "Current Write Cache": "Off", + "Default Write Cache": "On", + "Write Cache Changeable": "Yes" + }, + "Inquiry Data": "00 00 07 12 8b 01 10 02 53 45 41 47 41 54 45 20 53 54 31 30 30 30 30 4e 4d 30 31 38 42 20 20 20 45 30 30 31 53 49 4d 44 32 30 30 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a2 0c 60 20 e0 04 60 04 c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 6f 70 79 72 69 67 68 74 20 28 63 29 20 32 30 32 31 20 53 65 61 67 61 74 65 20 41 6c 6c 20 " + } + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/c0_vall.json b/scenario/storcli2sim/captures/storcli2-4hdd/c0_vall.json new file mode 100644 index 0000000..ee54a5f --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/c0_vall.json @@ -0,0 +1,269 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.115.1.el8_10.x86_64", + "Controller": "0", + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Virtual Drives": [ + { + "VD Info": { + "DG/VD": "0/1", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + "PDs": [ + { + "EID:Slt": "306:0", + "PID": 293, + "State": "Conf", + "Status": "Online", + "DG": 0, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + } + ], + "VD Properties": { + "Strip Size": "256 KiB", + "Block Size": 4096, + "Number of Blocks": 2441478144, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives": 1, + "Drive Write Cache Policy": "Disabled", + "Default Power Save Policy": "Default", + "Current Power Save Policy": "Default", + "Access Policy Status": "VD has desired access", + "Auto BGI": "Off", + "Secured": "No", + "Init State": "No Init", + "Consistent": "Yes", + "Morphing": "No", + "Cache Preserved": "No", + "Bad Block Exists": "No", + "VD Ready for OS Requests": "Yes", + "Reached LD BBM failure threshold": "No", + "Supported Erase Types": "Simple, Normal, Thorough", + "Exposed to OS": "Yes", + "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/06 09:43:32", + "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "SCSI NAA Id": "600062b2000000000000000000000001", + "OS Drive Name": "/dev/sdb", + "Current Unmap Status": "No", + "Current WriteSame Unmap Status": "No", + "LU/NS Count used per PD": 1, + "Data Format for I/O": "None", + "Serial Number": "00000000000000000000000000000001" + } + }, + { + "VD Info": { + "DG/VD": "1/2", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + "PDs": [ + { + "EID:Slt": "306:1", + "PID": 294, + "State": "Conf", + "Status": "Online", + "DG": 1, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + } + ], + "VD Properties": { + "Strip Size": "256 KiB", + "Block Size": 4096, + "Number of Blocks": 2441478144, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives": 1, + "Drive Write Cache Policy": "Disabled", + "Default Power Save Policy": "Default", + "Current Power Save Policy": "Default", + "Access Policy Status": "VD has desired access", + "Auto BGI": "Off", + "Secured": "No", + "Init State": "No Init", + "Consistent": "Yes", + "Morphing": "No", + "Cache Preserved": "No", + "Bad Block Exists": "No", + "VD Ready for OS Requests": "Yes", + "Reached LD BBM failure threshold": "No", + "Supported Erase Types": "Simple, Normal, Thorough", + "Exposed to OS": "Yes", + "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/06 09:57:01", + "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "SCSI NAA Id": "600062b2000000000000000000000002", + "OS Drive Name": "/dev/sdc", + "Current Unmap Status": "No", + "Current WriteSame Unmap Status": "No", + "LU/NS Count used per PD": 1, + "Data Format for I/O": "None", + "Serial Number": "00000000000000000000000000000002" + } + }, + { + "VD Info": { + "DG/VD": "2/3", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + "PDs": [ + { + "EID:Slt": "306:2", + "PID": 295, + "State": "Conf", + "Status": "Online", + "DG": 2, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + } + ], + "VD Properties": { + "Strip Size": "256 KiB", + "Block Size": 4096, + "Number of Blocks": 2441478144, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives": 1, + "Drive Write Cache Policy": "Disabled", + "Default Power Save Policy": "Default", + "Current Power Save Policy": "Default", + "Access Policy Status": "VD has desired access", + "Auto BGI": "Off", + "Secured": "No", + "Init State": "No Init", + "Consistent": "Yes", + "Morphing": "No", + "Cache Preserved": "No", + "Bad Block Exists": "No", + "VD Ready for OS Requests": "Yes", + "Reached LD BBM failure threshold": "No", + "Supported Erase Types": "Simple, Normal, Thorough", + "Exposed to OS": "Yes", + "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/06 09:57:31", + "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "SCSI NAA Id": "600062b2000000000000000000000003", + "OS Drive Name": "/dev/sdd", + "Current Unmap Status": "No", + "Current WriteSame Unmap Status": "No", + "LU/NS Count used per PD": 1, + "Data Format for I/O": "None", + "Serial Number": "00000000000000000000000000000003" + } + }, + { + "VD Info": { + "DG/VD": "3/4", + "TYPE": "RAID0", + "State": "Optl", + "Access": "RW", + "CurrentCache": "NR,WB", + "DefaultCache": "NR,WB", + "Size": "9.094 TiB", + "Name": "" + }, + "PDs": [ + { + "EID:Slt": "306:3", + "PID": 296, + "State": "Conf", + "Status": "Online", + "DG": 3, + "Size": "9.094 TiB", + "Intf": "SAS", + "Med": "HDD", + "SED_Type": "-", + "SeSz": "512B", + "Model": "ST10000NM018B ", + "Sp": "U", + "LU/NS Count": 1, + "Alt-EID": "-" + } + ], + "VD Properties": { + "Strip Size": "256 KiB", + "Block Size": 4096, + "Number of Blocks": 2441478144, + "VD has Emulated PD": "Yes", + "Span Depth": 1, + "Number of Drives": 1, + "Drive Write Cache Policy": "Disabled", + "Default Power Save Policy": "Default", + "Current Power Save Policy": "Default", + "Access Policy Status": "VD has desired access", + "Auto BGI": "Off", + "Secured": "No", + "Init State": "No Init", + "Consistent": "Yes", + "Morphing": "No", + "Cache Preserved": "No", + "Bad Block Exists": "No", + "VD Ready for OS Requests": "Yes", + "Reached LD BBM failure threshold": "No", + "Supported Erase Types": "Simple, Normal, Thorough", + "Exposed to OS": "Yes", + "Creation Time(LocalTime yyyy/mm/dd hh:mm:sec)": "2026/05/06 09:58:06", + "Default Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "Current Cachebypass Mode": "Cachebypass Not Performed For Any IOs", + "SCSI NAA Id": "600062b2000000000000000000000004", + "OS Drive Name": "/dev/sde", + "Current Unmap Status": "No", + "Current WriteSame Unmap Status": "No", + "LU/NS Count used per PD": 1, + "Data Format for I/O": "None", + "Serial Number": "00000000000000000000000000000004" + } + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/captures/storcli2-4hdd/show_all.json b/scenario/storcli2sim/captures/storcli2-4hdd/show_all.json new file mode 100644 index 0000000..dac76e1 --- /dev/null +++ b/scenario/storcli2sim/captures/storcli2-4hdd/show_all.json @@ -0,0 +1,43 @@ +{ + "Controllers": [ + { + "Command Status": { + "CLI Version": "008.0005.0000.0010 Feb 22, 2023", + "Operating system": "Linux4.18.0-553.115.1.el8_10.x86_64", + "Status Code": 0, + "Status": "Success", + "Description": "None" + }, + "Response Data": { + "Number of Controllers": 1, + "Host Name": "default-node", + "Operating System ": "Linux4.18.0-553.115.1.el8_10.x86_64", + "SL8 Library Version": "08.0507.0000", + "System Overview": [ + { + "Ctrl": 0, + "Product Name": "MegaRAID 9660-16i Tri-Mode Storage Adapter", + "SASAddress": "0X500062B200000000", + "Personality": "RAID", + "Status": "Optimal", + "PD(s)": 24, + "VD(s)": 24, + "VNOpt": 0, + "EPack": "Optimal" + } + ], + "ASO": [ + { + "Ctrl": 0, + "RP": "U", + "R0/00": "U", + "R1/10": "U", + "R5/50": "U", + "R6/60": "U", + "JBOD": "U" + } + ] + } + } + ] +} diff --git a/scenario/storcli2sim/storcli2sim.go b/scenario/storcli2sim/storcli2sim.go new file mode 100644 index 0000000..778ead8 --- /dev/null +++ b/scenario/storcli2sim/storcli2sim.go @@ -0,0 +1,436 @@ +// Package storcli2sim is the scenario backend for MegaRAID controllers driven +// by storcli2/perccli2: it answers the storcli2 commands of the raidmgmt +// storcli2 adapter from a real capture with the events of a scenario applied. +// +// storcli2 reports a drive status apart from its state. A failed drive keeps +// its state (Conf) with status Failed, and its volume is OfLn, not exposed to +// the OS; a replacement drive is UConf, status Good, in no drive group; a new +// volume is Optl and its drive Conf, status Online. No storcli2 output was +// captured while a drive was failed: this follows what storcli showed on +// RD-2154. +// +// A capture is a directory under captures/ holding one JSON file per storcli2 +// command, named after it: "show all" -> show_all.json, "/c0 show all" -> +// c0.json, "/c0 show aso" -> c0_aso.json, "/c0/eall/sall show all" -> +// c0_eall_sall.json, "/c0/vall show all" -> c0_vall.json. Drives and volumes +// are rendered with the events applied; other commands are answered as +// captured. +// +// The storcli2 adapter reads the paths of a RAID volume from the controller +// output only, so a scenario needs no host file. (A JBOD drive would be +// resolved on the real filesystem; captures only hold RAID volumes.) +package storcli2sim + +import ( + "embed" + "encoding/json" + "path" + "strings" + + "github.com/pkg/errors" + + "github.com/scality/raidmgmt/pkg/implementation/commandrunner" + "github.com/scality/raidmgmt/scenario" +) + +//go:embed captures +var captures embed.FS //nolint:gochecknoglobals // embedded captures + +const ( + yes, no = "Yes", "No" + // noGroup is the drive group of an unconfigured drive. + noGroup = "-" +) + +type ( + // Controller replays a capture with events applied. It implements + // commandrunner.CommandRunner. + Controller struct { + capture string + drives map[string]*drive // by "enclosure:slot" + volumes map[string]*volume // by virtual drive ID + } + + drive struct { + state, status, serial, wwn string + group any + // volume is the virtual drive holding the drive in the capture, and + // capturedGroup its drive group there. + volume string + capturedGroup any + } + + volume struct { + state, device, wwn string + exposed, deleted bool + } + + // doc is a storcli2 JSON output. + doc = map[string]any +) + +var _ commandrunner.CommandRunner = &Controller{} + +// New returns the controller of a capture once events happened, in order. +func New(capture string, events ...scenario.Event) (*Controller, error) { + c := &Controller{ + capture: path.Join("captures", capture), + drives: map[string]*drive{}, + volumes: map[string]*volume{}, + } + + if err := c.loadCapture(); err != nil { + return nil, errors.Wrapf(err, "failed to load capture %s", capture) + } + + for i, e := range events { + if err := c.apply(e); err != nil { + return nil, errors.Wrapf(err, "failed to apply event %d", i+1) + } + } + + return c, nil +} + +// Run answers a storcli2 command with its JSON output. +func (c *Controller) Run(args []string) ([]byte, error) { + var ( + out doc + err error + ) + + cmd := strings.Join(args, " ") + + switch cmd { + case "/c0 show all": + out, err = c.renderController() + case "/c0/eall/sall show all": + out, err = c.renderDrives() + case "/c0/vall show all": + out, err = c.renderVolumes() + default: + out, err = c.read(captureName(cmd)) + } + + if err != nil { + return nil, errors.Wrapf(err, "storcli2 %s: not replayed", cmd) + } + + data, err := json.Marshal(out) + + return data, errors.Wrap(err, "failed to marshal output") +} + +func (c *Controller) loadCapture() error { + drives, err := c.read("c0_eall_sall") + if err != nil { + return err + } + + for _, entry := range rows(responseData(drives), "Drives List") { + info, details := object(entry["Drive Information"]), object(entry["Drive Detailed Information"]) + c.drives[str(info["EID:Slt"])] = &drive{ + state: str(info["State"]), status: str(info["Status"]), + group: info["DG"], capturedGroup: info["DG"], + serial: strings.TrimSpace(str(details["Serial Number"])), wwn: str(details["WWN"]), + } + } + + volumes, err := c.read("c0_vall") + if err != nil { + return err + } + + for _, entry := range rows(responseData(volumes), "Virtual Drives") { + info, props := object(entry["VD Info"]), object(entry["VD Properties"]) + + for _, pd := range rows(entry, "PDs") { + if d, ok := c.drives[str(pd["EID:Slt"])]; ok { + d.volume = volumeID(info) + } + } + + c.volumes[volumeID(info)] = &volume{ + state: str(info["State"]), device: str(props["OS Drive Name"]), + wwn: str(props["SCSI NAA Id"]), exposed: str(props["Exposed to OS"]) == yes, + } + } + + return nil +} + +func (c *Controller) apply(e scenario.Event) error { + d, ok := c.drives[e.Slot] + if !ok { + return errors.Errorf("unknown drive %s", e.Slot) + } + + v, ok := c.volumes[d.volume] + if !ok { + return errors.Errorf("drive %s is in no volume of the capture", e.Slot) + } + + if err := checkOrder(e, d.status == "Failed", v.deleted); err != nil { + return err + } + + switch e.Kind { + case scenario.DriveFails: + d.status = "Failed" + v.state, v.exposed = "OfLn", false + case scenario.DriveReplaced: + v.deleted = true + d.state, d.status, d.serial, d.wwn, d.group = "UConf", "Good", e.Serial, e.WWN, noGroup + case scenario.VolumeCreated: + d.state, d.status, d.group = "Conf", "Online", d.capturedGroup + v.deleted, v.state, v.exposed, v.device, v.wwn = false, "Optl", true, e.Device, e.VolumeWWN + default: + return errors.Errorf("event %d not replayed", e.Kind) + } + + return nil +} + +// checkOrder rejects an event that cannot happen to a drive in its current +// state: only a drive in a volume can fail, only a failed drive can be +// replaced, and a volume can only be created once the old one is deleted. +func checkOrder(e scenario.Event, failed, volumeDeleted bool) error { + switch { + case e.Kind == scenario.DriveFails && (failed || volumeDeleted): + return errors.Errorf("drive %s cannot fail: it has failed or is in no volume", e.Slot) + case e.Kind == scenario.DriveReplaced && !failed: + return errors.Errorf("drive %s cannot be replaced: it has not failed", e.Slot) + case e.Kind == scenario.VolumeCreated && !volumeDeleted: + return errors.Errorf("no volume can be created on drive %s: its volume still exists", e.Slot) + } + + return nil +} + +func (c *Controller) renderController() (doc, error) { + out, err := c.read("c0") + if err != nil { + return nil, err + } + + data := responseData(out) + + for _, row := range rows(data, "PD LIST") { + d, err := c.drive(str(row["EID:Slt"])) + if err != nil { + return nil, err + } + + d.update(row) + } + + var vds []any + + for _, row := range rows(data, "VD LIST") { + v, err := c.volume(volumeID(row)) + if err != nil { + return nil, err + } + + if !v.deleted { + row["State"] = v.state + vds = append(vds, row) + } + } + + data["VD LIST"] = vds + + return out, nil +} + +func (c *Controller) renderDrives() (doc, error) { + out, err := c.read("c0_eall_sall") + if err != nil { + return nil, err + } + + for _, entry := range rows(responseData(out), "Drives List") { + info, details := object(entry["Drive Information"]), object(entry["Drive Detailed Information"]) + + d, err := c.drive(str(info["EID:Slt"])) + if err != nil { + return nil, err + } + + d.update(info) + details["Serial Number"], details["WWN"] = d.serial, d.wwn + } + + return out, nil +} + +func (c *Controller) renderVolumes() (doc, error) { + out, err := c.read("c0_vall") + if err != nil { + return nil, err + } + + data := responseData(out) + + var vds []any + + for _, entry := range rows(data, "Virtual Drives") { + listed, err := c.renderVolume(entry) + if err != nil { + return nil, err + } + + if listed { + vds = append(vds, entry) + } + } + + data["Virtual Drives"] = vds + + return out, nil +} + +// drive returns a drive of the capture. A drive missing from it means an +// inconsistent capture. +func (c *Controller) drive(slot string) (*drive, error) { + d, ok := c.drives[slot] + if !ok { + return nil, errors.Errorf("unknown drive %s", slot) + } + + return d, nil +} + +// volume returns a virtual drive of the capture. A volume missing from it +// means an inconsistent capture. +func (c *Controller) volume(id string) (*volume, error) { + v, ok := c.volumes[id] + if !ok { + return nil, errors.Errorf("unknown virtual drive %s", id) + } + + return v, nil +} + +// renderVolume writes the state of a volume and of its drives into its entry +// of "/c0/vall show all". It returns false for a deleted volume, which is not +// listed. +func (c *Controller) renderVolume(entry doc) (listed bool, err error) { + info, props := object(entry["VD Info"]), object(entry["VD Properties"]) + + v, err := c.volume(volumeID(info)) + if err != nil || v.deleted { + return false, err + } + + info["State"] = v.state + + for _, pd := range rows(entry, "PDs") { + d, err := c.drive(str(pd["EID:Slt"])) + if err != nil { + return false, err + } + + d.update(pd) + } + + exposed := no + if v.exposed { + exposed = yes + props["OS Drive Name"] = v.device + } else { + delete(props, "OS Drive Name") + } + + props["Exposed to OS"], props["VD Ready for OS Requests"] = exposed, exposed + props["SCSI NAA Id"] = v.wwn + + return true, nil +} + +// update writes the drive state into a storcli2 drive row. +func (d *drive) update(row doc) { + row["State"], row["Status"], row["DG"] = d.state, d.status, d.group +} + +func (c *Controller) read(name string) (doc, error) { + data, err := captures.ReadFile(path.Join(c.capture, name+".json")) + if err != nil { + return nil, errors.Wrapf(err, "no capture for %s", name) + } + + var out doc + if err := json.Unmarshal(data, &out); err != nil { + return nil, errors.Wrapf(err, "failed to parse capture %s", name) + } + + return out, nil +} + +func responseData(out doc) doc { + controllers := array(out["Controllers"]) + if len(controllers) == 0 { + return doc{} + } + + return object(object(controllers[0])["Response Data"]) +} + +func rows(data doc, key string) []doc { + items := array(data[key]) + out := make([]doc, 0, len(items)) + + for _, item := range items { + if row, ok := item.(doc); ok { + out = append(out, row) + } + } + + return out +} + +// captureName is the capture file of a command: "/c0 show aso" -> "c0_aso", +// "show all" -> "show_all". +func captureName(cmd string) string { + if cmd == "show all" { + return "show_all" + } + + name := strings.TrimPrefix(cmd, "/") + name = strings.Replace(name, " show all", "", 1) + + return strings.NewReplacer(" show ", "_", "/", "_").Replace(name) +} + +// volumeID reads the virtual drive ID of a "DG/VD" field. +func volumeID(info doc) string { + dgvd := str(info["DG/VD"]) + + return dgvd[strings.Index(dgvd, "/")+1:] +} + +func str(v any) string { + if s, ok := v.(string); ok { + return s + } + + return "" +} + +// object returns v as a JSON object, or an empty one. +func object(v any) doc { + if o, ok := v.(doc); ok { + return o + } + + return doc{} +} + +// array returns v as a JSON array, or nil. +func array(v any) []any { + if a, ok := v.([]any); ok { + return a + } + + return nil +} diff --git a/scenario/storcli2sim/storcli2sim_internal_test.go b/scenario/storcli2sim/storcli2sim_internal_test.go new file mode 100644 index 0000000..a6617a9 --- /dev/null +++ b/scenario/storcli2sim/storcli2sim_internal_test.go @@ -0,0 +1,43 @@ +package storcli2sim + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestRunInconsistentCapture checks that a capture listing a drive or a +// volume the controller does not know fails with an error, not a panic. +func TestRunInconsistentCapture(t *testing.T) { + t.Parallel() + + forgetDrive := func(c *Controller) { delete(c.drives, "306:2") } + forgetVolume := func(c *Controller) { delete(c.volumes, "3") } + + tests := []struct { + name string + forget func(c *Controller) + cmd []string + wantErr string + }{ + {name: "drive, controller", forget: forgetDrive, cmd: []string{"/c0", "show", "all"}, wantErr: "unknown drive 306:2"}, + {name: "volume, controller", forget: forgetVolume, cmd: []string{"/c0", "show", "all"}, wantErr: "unknown virtual drive 3"}, + {name: "drive, drives", forget: forgetDrive, cmd: []string{"/c0/eall/sall", "show", "all"}, wantErr: "unknown drive 306:2"}, + {name: "drive, volumes", forget: forgetDrive, cmd: []string{"/c0/vall", "show", "all"}, wantErr: "unknown drive 306:2"}, + {name: "volume, volumes", forget: forgetVolume, cmd: []string{"/c0/vall", "show", "all"}, wantErr: "unknown virtual drive 3"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, err := New("storcli2-4hdd") + require.NoError(t, err) + + tt.forget(c) + + _, err = c.Run(tt.cmd) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} From 11b2a1355fe966d7fb28a787f1f1be4b5b5cd179 Mon Sep 17 00:00:00 2001 From: Guillaume Carre Date: Mon, 12 Oct 2026 16:09:37 +0200 Subject: [PATCH 3/3] test(scenario): play the disk failure scenarios on HPE Smart Array controllers Add ssaclisim, the backend of ssacli controllers: it answers the commands of the raidmgmt ssacli getters (controller listing, physical and logical drive details, show config) from a capture with events applied. ssacli prints text, so the backend edits the lines of the captured sections: drive and logical drive status, serials, disk names, and moves a replacement drive to the Unassigned section once its array is gone. The capture is the ssacli test data of this repository, kept to 4 single-drive RAID0 arrays, with fake serials, WWIDs and identifiers. The ssacli-disk-failure feature plays the megaraid-disk-failure scenarios with the same steps. The ssacli getters already report them as expected: the healthy drives keep their paths, the failed one is Failed, and the replacement drive is UnassignedGood. Refs: ARTESCA-17960 --- scenario/README.md | 14 +- scenario/features/ssacli-disk-failure.feature | 51 ++ scenario/ssaclisim/adapter_test.go | 117 ++++ .../controller_all_show_detail.txt | 89 +++ ...ler_slot0_logicaldrive_all_show_detail.txt | 89 +++ ...er_slot0_physicaldrive_all_show_detail.txt | 137 +++++ .../controller_slot0_show_config.txt | 54 ++ .../controller_slot0_show_detail.txt | 89 +++ scenario/ssaclisim/ssaclisim.go | 550 ++++++++++++++++++ scenario/ssaclisim/ssaclisim_internal_test.go | 81 +++ 10 files changed, 1268 insertions(+), 3 deletions(-) create mode 100644 scenario/features/ssacli-disk-failure.feature create mode 100644 scenario/ssaclisim/adapter_test.go create mode 100644 scenario/ssaclisim/captures/ssacli-4hdd/controller_all_show_detail.txt create mode 100644 scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_logicaldrive_all_show_detail.txt create mode 100644 scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_physicaldrive_all_show_detail.txt create mode 100644 scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_config.txt create mode 100644 scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_detail.txt create mode 100644 scenario/ssaclisim/ssaclisim.go create mode 100644 scenario/ssaclisim/ssaclisim_internal_test.go diff --git a/scenario/README.md b/scenario/README.md index 422f821..10aa106 100644 --- a/scenario/README.md +++ b/scenario/README.md @@ -6,9 +6,9 @@ without the hardware. Scenarios are Gherkin features in [`features/`](features), one per controller family and story, tagged with the family (`@megaraid`, -`@storcli2`). The `Background` names the capture of a real controller; -each `Scenario` plays events on its drives and gives, as a table, the disks -expected: +`@storcli2`, `@ssacli`). The `Background` names the capture of a real +controller; each `Scenario` plays events on its drives and gives, as a +table, the disks expected: ```gherkin @megaraid @@ -61,6 +61,8 @@ Feature: MegaRAID disk failure and replacement `scenario.Run`. - `storcli2sim/adapter_test.go` plays every `@storcli2` scenario through the raidmgmt storcli2 adapter. +- `ssaclisim/adapter_test.go` plays every `@ssacli` scenario (HPE Smart + Array) through the raidmgmt ssacli getters. - `steps_internal_test.go` checks the steps themselves on the features of [`testdata/`](testdata), in particular that they fail when they should. @@ -99,3 +101,9 @@ handling in every backend. 008.0005). Captured with `storcli2 show all J` on 24 drives, kept to the first 4 (306:0-3, volumes 1-4), with the same fake identifiers (drive serials, WWNs and SAS addresses, volume NAA Ids and serials). +- `ssaclisim/captures/ssacli-4hdd`: HPE Smart Array P816i-a, SAS HDDs each + in its own RAID0 array. Captured with `ssacli controller slot=0 ... show + detail` and `show config`, kept to 4 drives (1I:1:3, 1I:1:4, 2I:2:1, + 2I:2:2, logical drives 2-5), with the same fake identifiers (serials, + WWIDs, unique identifiers, controller and host serials). ssacli prints + text: the backend edits the lines of the captured sections. diff --git a/scenario/features/ssacli-disk-failure.feature b/scenario/features/ssacli-disk-failure.feature new file mode 100644 index 0000000..148ce0e --- /dev/null +++ b/scenario/features/ssacli-disk-failure.feature @@ -0,0 +1,51 @@ +@ssacli +Feature: ssacli disk failure and replacement + The ssacli counterpart of megaraid-disk-failure: an HPE Smart Array + P816i-a with HDDs, each in its own RAID0 array, as ARTESCA deploys them. + Drive 1I:1:4 (array C, logical drive 3) fails, is replaced, then gets a new + array (ARTESCA-17960). + + The capture is real ssacli output of a lab node while all its drives were + healthy, kept to 4 drives, with serials, WWIDs and identifiers replaced by + fake ones. ssacli getters report no permanent path. + + Background: + Given the controller captured as "ssacli-4hdd" + + Scenario: All drives are healthy + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 1I:1:3 | Used | SIMH0001 | /dev/sdd | | + | 1I:1:4 | Used | SIMH0002 | /dev/sde | | + | 2I:2:1 | Used | SIMH0003 | /dev/sdf | | + | 2I:2:2 | Used | SIMH0004 | /dev/sdg | | + + Scenario: A failed drive does not take the paths of the others + When drive 1I:1:4 fails + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 1I:1:3 | Used | SIMH0001 | /dev/sdd | | + | 1I:1:4 | Failed | SIMH0002 | | | + | 2I:2:1 | Used | SIMH0003 | /dev/sdf | | + | 2I:2:2 | Used | SIMH0004 | /dev/sdg | | + + Scenario: A replacement drive is reported as a new, available drive + Given drive 1I:1:4 has failed + When drive 1I:1:4 is replaced by a drive with serial "SIMH0013" and WWN "5000C50030000013" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 1I:1:3 | Used | SIMH0001 | /dev/sdd | | + | 1I:1:4 | UnassignedGood | SIMH0013 | | | + | 2I:2:1 | Used | SIMH0003 | /dev/sdf | | + | 2I:2:2 | Used | SIMH0004 | /dev/sdg | | + + Scenario: A replacement drive gets the paths of its new array + Given drive 1I:1:4 has failed + And drive 1I:1:4 has been replaced by a drive with serial "SIMH0013" and WWN "5000C50030000013" + When a volume is created on drive 1I:1:4, exposed as "/dev/sdh" with WWN "600508B1000000000000000000001003" + Then the drives are reported as: + | slot | status | serial | devicePath | permanentPath | + | 1I:1:3 | Used | SIMH0001 | /dev/sdd | | + | 1I:1:4 | Used | SIMH0013 | /dev/sdh | | + | 2I:2:1 | Used | SIMH0003 | /dev/sdf | | + | 2I:2:2 | Used | SIMH0004 | /dev/sdg | | diff --git a/scenario/ssaclisim/adapter_test.go b/scenario/ssaclisim/adapter_test.go new file mode 100644 index 0000000..7908332 --- /dev/null +++ b/scenario/ssaclisim/adapter_test.go @@ -0,0 +1,117 @@ +package ssaclisim_test + +import ( + "context" + "strings" + "testing" + + "github.com/cucumber/godog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/pkg/domain/entities/logicalvolume" + "github.com/scality/raidmgmt/pkg/domain/entities/physicaldrive" + "github.com/scality/raidmgmt/pkg/domain/entities/raidcontroller" + "github.com/scality/raidmgmt/pkg/implementation/controllergetter" + "github.com/scality/raidmgmt/pkg/implementation/logicalvolumegetter" + "github.com/scality/raidmgmt/pkg/implementation/physicaldrivegetter" + "github.com/scality/raidmgmt/scenario" + "github.com/scality/raidmgmt/scenario/ssaclisim" +) + +const capture = "ssacli-4hdd" + +// inventory composes the ssacli getters as disk-management-agent does. +type inventory struct { + controllers *controllergetter.SSACLI + physicalDrives *physicaldrivegetter.SSACLI + logicalVolumes *logicalvolumegetter.SSACLI +} + +func newInventory(ctrl *ssaclisim.Controller) inventory { + return inventory{ + controllers: &controllergetter.SSACLI{SSACLI: ctrl.SSACLI()}, + physicalDrives: &physicaldrivegetter.SSACLI{SSACLI: ctrl.SSACLI(), LSBLK: ctrl.LSBLK()}, + logicalVolumes: &logicalvolumegetter.SSACLI{SSACLI: ctrl.SSACLI(), LSBLK: ctrl.LSBLK()}, + } +} + +func (i inventory) Controllers() ([]*raidcontroller.RAIDController, error) { + return i.controllers.Controllers() +} + +func (i inventory) PhysicalDrives(m *raidcontroller.Metadata) ([]*physicaldrive.PhysicalDrive, error) { + return i.physicalDrives.PhysicalDrives(m) +} + +func (i inventory) LogicalVolumes(m *raidcontroller.Metadata) ([]*logicalvolume.LogicalVolume, error) { + return i.logicalVolumes.LogicalVolumes(m) +} + +// TestScenarios plays every @ssacli scenario through the raidmgmt ssacli +// getters of this repository and checks the drives they report, with the +// device and permanent paths of the volume holding each drive. +func TestScenarios(t *testing.T) { + scenario.Run(t, "ssacli", func(ctx context.Context, capture string, events []scenario.Event) ([]scenario.Disk, error) { + ctrl, err := ssaclisim.New(capture, events...) + if err != nil { + return nil, err + } + + disks, volumesErr, err := scenario.Report(newInventory(ctrl), &raidcontroller.Metadata{ID: 0}) + if volumesErr != nil { + godog.Logf(ctx, "LogicalVolumes: %v", volumesErr) + } + + return disks, err + }) +} + +// TestNewRejects checks that New fails on an unknown capture or drive, and on +// events in an order that cannot happen to a drive. +func TestNewRejects(t *testing.T) { + t.Parallel() + + fails := scenario.Event{Kind: scenario.DriveFails, Slot: "1I:1:4"} + replaced := scenario.Event{Kind: scenario.DriveReplaced, Slot: "1I:1:4", Serial: "S", WWN: "W"} + created := scenario.Event{Kind: scenario.VolumeCreated, Slot: "1I:1:4", Device: "/dev/sdz", VolumeWWN: "1"} + + tests := []struct { + name string + capture string + events []scenario.Event + wantErr string + }{ + {name: "unknown capture", capture: "no-such-capture", wantErr: "failed to load capture"}, + {name: "unknown drive", capture: capture, events: []scenario.Event{{Kind: scenario.DriveFails, Slot: "9I:9:9"}}, wantErr: "unknown drive"}, + {name: "fails twice", capture: capture, events: []scenario.Event{fails, fails}, wantErr: "cannot fail"}, + {name: "replaced while healthy", capture: capture, events: []scenario.Event{replaced}, wantErr: "cannot be replaced"}, + {name: "volume created on a failed drive", capture: capture, events: []scenario.Event{fails, created}, wantErr: "still exists"}, + {name: "replacement fails before its volume", capture: capture, events: []scenario.Event{fails, replaced, fails}, wantErr: "cannot fail"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, err := ssaclisim.New(tt.capture, tt.events...) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} + +// TestReplacedDriveListedOnce checks that a replacement drive is listed only +// as unassigned in show config, not also in its former array. +func TestReplacedDriveListedOnce(t *testing.T) { + t.Parallel() + + ctrl, err := ssaclisim.New(capture, + scenario.Event{Kind: scenario.DriveFails, Slot: "1I:1:4"}, + scenario.Event{Kind: scenario.DriveReplaced, Slot: "1I:1:4", Serial: "S", WWN: "W"}, + ) + require.NoError(t, err) + + out, err := ctrl.SSACLI().Run([]string{"controller", "slot=0", "show", "config"}) + require.NoError(t, err) + assert.Equal(t, 1, strings.Count(string(out), "physicaldrive 1I:1:4 "), "1I:1:4 listings in:\n%s", out) +} diff --git a/scenario/ssaclisim/captures/ssacli-4hdd/controller_all_show_detail.txt b/scenario/ssaclisim/captures/ssacli-4hdd/controller_all_show_detail.txt new file mode 100644 index 0000000..d441acf --- /dev/null +++ b/scenario/ssaclisim/captures/ssacli-4hdd/controller_all_show_detail.txt @@ -0,0 +1,89 @@ + +HPE Smart Array P816i-a SR Gen10 in Slot 0 (Embedded) + Bus Interface: PCI + Slot: 0 + Serial Number: SIMCTL00000003 + RAID 6 Status: Enabled + Controller Status: OK + Hardware Revision: A + Firmware Version: 5.00 + Firmware Supports Online Firmware Activation: False + Rebuild Priority: High + Expand Priority: Medium + Surface Scan Delay: 3 secs + Surface Scan Mode: Idle + Parallel Surface Scan Supported: Yes + Current Parallel Surface Scan Count: 1 + Max Parallel Surface Scan Count: 16 + Queue Depth: Automatic + Monitor and Performance Delay: 60 min + Elevator Sort: Enabled + Degraded Performance Optimization: Disabled + Inconsistency Repair Policy: Disabled + Write Cache Bypass Threshold Size: 1040 KiB + Wait for Cache Room: Disabled + Surface Analysis Inconsistency Notification: Disabled + Post Prompt Timeout: 15 secs + Cache Board Present: True + Cache Status: OK + Cache Ratio: 10% Read / 90% Write + Configured Drive Write Cache Policy: Disable + Unconfigured Drive Write Cache Policy: Default + Total Cache Size: 4.0 + Total Cache Memory Available: 3.8 + Battery Backed Cache Size: 3.8 + No-Battery Write Cache: Disabled + SSD Caching RAID5 WriteBack Enabled: True + SSD Caching Version: 2 + Cache Backup Power Source: Batteries + Battery/Capacitor Count: 1 + Battery/Capacitor Status: OK + SATA NCQ Supported: True + Spare Activation Mode: Activate on physical drive failure (default) + Spare Spindown Policy Supported: False + Controller Temperature (C): 64 + Capacitor Temperature (C): 50 + Number of Ports: 4 Internal only + Encryption: Not Set + Express Local Encryption: False + SED Based Encryption Supported: False + Driver Name: smartpqi + Driver Version: Linux 2.1.24-046 + WWN Port: 51402EC000000001 + PCI Address (Domain:Bus:Device.Function): 0000:5C:00.0 + Negotiated PCIe Data Rate: PCIe 3.0 x8 (7880 MB/s) + Controller Mode: Mixed + Port Max Phy Rate Limiting Supported: False + Latency Scheduler Setting: Disabled + Current Power Mode: MaxPerformance + Survival Mode: Enabled + Host Serial Number: SIMHOST001 + Sanitize Erase Supported: True + Sanitize Lock: None + Sensor ID: 0 + Location: Capacitor + Current Value (C): 50 + Max Value Since Power On: 52 + Sensor ID: 1 + Location: ASIC + Current Value (C): 64 + Max Value Since Power On: 67 + Sensor ID: 2 + Location: Unknown + Current Value (C): 50 + Max Value Since Power On: 52 + Primary Boot Volume: None + Secondary Boot Volume: None + SPDM Supports Get Slot Certificate Chain: no + SPDM Supports Get Controller Info : no + SPDM Supports Get Slot Info : no + SPDM Supports Set Import Certificate : no + SPDM Supports Set Invalidate Slot : no + Surface Scan Completion Supported: False + Persistent Event Log Policy Change Supported: False + UEFI Health Reporting Mode Supported: False + Firmware Supports NVMe Log Pages: False + Firmware Supports NAND Field: False + Firmware Supports NOR Field: False + + diff --git a/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_logicaldrive_all_show_detail.txt b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_logicaldrive_all_show_detail.txt new file mode 100644 index 0000000..c8d02de --- /dev/null +++ b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_logicaldrive_all_show_detail.txt @@ -0,0 +1,89 @@ + +HPE Smart Array P816i-a SR Gen10 in Slot 0 (Embedded) + + Array B + + Logical Drive: 2 + Size: 5.46 TB + Fault Tolerance: 0 + Heads: 255 + Sectors Per Track: 32 + Cylinders: 65535 + Strip Size: 256 KB + Full Stripe Size: 256 KB + Status: OK + MultiDomain Status: OK + Caching: Enabled + Last Surface Scan Completed: False + Unique Identifier: 600508B1000000000000000000000002 + Disk Name: /dev/sdd + Mount Points: None + Logical Drive Label: Data 01 + Drive Type: Data + LD Acceleration Method: Controller Cache + + + Array C + + Logical Drive: 3 + Size: 5.46 TB + Fault Tolerance: 0 + Heads: 255 + Sectors Per Track: 32 + Cylinders: 65535 + Strip Size: 256 KB + Full Stripe Size: 256 KB + Status: OK + MultiDomain Status: OK + Caching: Enabled + Last Surface Scan Completed: False + Unique Identifier: 600508B1000000000000000000000003 + Disk Name: /dev/sde + Mount Points: None + Logical Drive Label: Data 02 + Drive Type: Data + LD Acceleration Method: Controller Cache + + + Array D + + Logical Drive: 4 + Size: 5.46 TB + Fault Tolerance: 0 + Heads: 255 + Sectors Per Track: 32 + Cylinders: 65535 + Strip Size: 256 KB + Full Stripe Size: 256 KB + Status: OK + MultiDomain Status: OK + Caching: Enabled + Last Surface Scan Completed: False + Unique Identifier: 600508B1000000000000000000000004 + Disk Name: /dev/sdf + Mount Points: None + Logical Drive Label: Data 03 + Drive Type: Data + LD Acceleration Method: Controller Cache + + + Array E + + Logical Drive: 5 + Size: 5.46 TB + Fault Tolerance: 0 + Heads: 255 + Sectors Per Track: 32 + Cylinders: 65535 + Strip Size: 256 KB + Full Stripe Size: 256 KB + Status: OK + MultiDomain Status: OK + Caching: Enabled + Last Surface Scan Completed: False + Unique Identifier: 600508B1000000000000000000000005 + Disk Name: /dev/sdg + Mount Points: None + Logical Drive Label: Data 04 + Drive Type: Data + LD Acceleration Method: Controller Cache diff --git a/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_physicaldrive_all_show_detail.txt b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_physicaldrive_all_show_detail.txt new file mode 100644 index 0000000..a719a20 --- /dev/null +++ b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_physicaldrive_all_show_detail.txt @@ -0,0 +1,137 @@ + +HPE Smart Array P816i-a SR Gen10 in Slot 0 (Embedded) + + Array B + + physicaldrive 1I:1:3 + Port: 1I + Box: 1 + Bay: 3 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 6 TB + Drive exposed to OS: False + Logical/Physical Block Size: 512/4096 + Rotational Speed: 7200 + Firmware Revision: HPD4 + Serial Number: SIMH0001 + WWID: 5000C50030000001 + Model: HP MB6000JVYZD + Current Temperature (C): 33 + Maximum Temperature (C): 45 + PHY Count: 2 + PHY Transfer Rate: 12.0Gbps, Unknown + PHY Physical Link Rate: 12.0Gbps, Unknown + PHY Maximum Link Rate: 12.0Gbps, 12.0Gbps + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + Sanitize Erase Supported: True + Sanitize Estimated Max Erase Time: 11 hour(s), 40 minute(s) + Unrestricted Sanitize Supported: True + Shingled Magnetic Recording Support: None + Drive Unique ID: 5000C50040000001 + Multi-Actuator Drive: False + + + Array C + + physicaldrive 1I:1:4 + Port: 1I + Box: 1 + Bay: 4 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 6 TB + Drive exposed to OS: False + Logical/Physical Block Size: 512/4096 + Rotational Speed: 7200 + Firmware Revision: HPD4 + Serial Number: SIMH0002 + WWID: 5000C50030000002 + Model: HP MB6000JVYZD + Current Temperature (C): 34 + Maximum Temperature (C): 46 + PHY Count: 2 + PHY Transfer Rate: 12.0Gbps, Unknown + PHY Physical Link Rate: 12.0Gbps, Unknown + PHY Maximum Link Rate: 12.0Gbps, 12.0Gbps + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + Sanitize Erase Supported: True + Sanitize Estimated Max Erase Time: 11 hour(s), 40 minute(s) + Unrestricted Sanitize Supported: True + Shingled Magnetic Recording Support: None + Drive Unique ID: 5000C50040000002 + Multi-Actuator Drive: False + + + Array D + + physicaldrive 2I:2:1 + Port: 2I + Box: 2 + Bay: 1 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 6 TB + Drive exposed to OS: False + Logical/Physical Block Size: 512/4096 + Rotational Speed: 7200 + Firmware Revision: HPD4 + Serial Number: SIMH0003 + WWID: 5000C50030000003 + Model: HP MB6000JVYZD + Current Temperature (C): 33 + Maximum Temperature (C): 44 + PHY Count: 2 + PHY Transfer Rate: 12.0Gbps, Unknown + PHY Physical Link Rate: 12.0Gbps, Unknown + PHY Maximum Link Rate: 12.0Gbps, 12.0Gbps + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + Sanitize Erase Supported: True + Sanitize Estimated Max Erase Time: 11 hour(s), 40 minute(s) + Unrestricted Sanitize Supported: True + Shingled Magnetic Recording Support: None + Drive Unique ID: 5000C50040000003 + Multi-Actuator Drive: False + + + Array E + + physicaldrive 2I:2:2 + Port: 2I + Box: 2 + Bay: 2 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 6 TB + Drive exposed to OS: False + Logical/Physical Block Size: 512/4096 + Rotational Speed: 7200 + Firmware Revision: HPD4 + Serial Number: SIMH0004 + WWID: 5000C50030000004 + Model: HP MB6000JVYZD + Current Temperature (C): 34 + Maximum Temperature (C): 45 + PHY Count: 2 + PHY Transfer Rate: 12.0Gbps, Unknown + PHY Physical Link Rate: 12.0Gbps, Unknown + PHY Maximum Link Rate: 12.0Gbps, 12.0Gbps + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + Sanitize Erase Supported: True + Sanitize Estimated Max Erase Time: 11 hour(s), 40 minute(s) + Unrestricted Sanitize Supported: True + Shingled Magnetic Recording Support: None + Drive Unique ID: 5000C50040000004 + Multi-Actuator Drive: False diff --git a/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_config.txt b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_config.txt new file mode 100644 index 0000000..84f4897 --- /dev/null +++ b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_config.txt @@ -0,0 +1,54 @@ + +HPE Smart Array P816i-a SR Gen10 in Slot 0 (Embedded) (sn: SIMCTL00000003) + + + + Internal Drive Cage at Port 1I, Box 1 (Index 0), OK + + + + Internal Drive Cage at Port 2I, Box 2 (Index 1), OK + + + + Internal Drive Cage at Port 3I, Box 3 (Index 2), OK + + + + Internal Drive Cage at Port 4I, Box 6 (Index 3), OK + + + Port Name: 1I (Mixed) + + Port Name: 2I (Mixed) + + Port Name: 3I (Mixed) + + Port Name: 4I (Mixed) + + Array B (SAS, Unused Space: 0 MB) + + logicaldrive 2 (5.46 TB, RAID 0, OK) + + physicaldrive 1I:1:3 (port 1I:box 1:bay 3, SAS HDD, 6 TB, OK) + + + Array C (SAS, Unused Space: 0 MB) + + logicaldrive 3 (5.46 TB, RAID 0, OK) + + physicaldrive 1I:1:4 (port 1I:box 1:bay 4, SAS HDD, 6 TB, OK) + + + Array D (SAS, Unused Space: 0 MB) + + logicaldrive 4 (5.46 TB, RAID 0, OK) + + physicaldrive 2I:2:1 (port 2I:box 2:bay 1, SAS HDD, 6 TB, OK) + + + Array E (SAS, Unused Space: 0 MB) + + logicaldrive 5 (5.46 TB, RAID 0, OK) + + physicaldrive 2I:2:2 (port 2I:box 2:bay 2, SAS HDD, 6 TB, OK) diff --git a/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_detail.txt b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_detail.txt new file mode 100644 index 0000000..a03c047 --- /dev/null +++ b/scenario/ssaclisim/captures/ssacli-4hdd/controller_slot0_show_detail.txt @@ -0,0 +1,89 @@ + +HPE Smart Array P816i-a SR Gen10 in Slot 0 (Embedded) + Bus Interface: PCI + Slot: 0 + Serial Number: SIMCTL00000003 + RAID 6 Status: Enabled + Controller Status: OK + Hardware Revision: A + Firmware Version: 5.00 + Firmware Supports Online Firmware Activation: False + Rebuild Priority: High + Expand Priority: Medium + Surface Scan Delay: 3 secs + Surface Scan Mode: Idle + Parallel Surface Scan Supported: Yes + Current Parallel Surface Scan Count: 1 + Max Parallel Surface Scan Count: 16 + Queue Depth: Automatic + Monitor and Performance Delay: 60 min + Elevator Sort: Enabled + Degraded Performance Optimization: Disabled + Inconsistency Repair Policy: Disabled + Write Cache Bypass Threshold Size: 1040 KiB + Wait for Cache Room: Disabled + Surface Analysis Inconsistency Notification: Disabled + Post Prompt Timeout: 15 secs + Cache Board Present: True + Cache Status: OK + Cache Ratio: 10% Read / 90% Write + Configured Drive Write Cache Policy: Disable + Unconfigured Drive Write Cache Policy: Default + Total Cache Size: 4.0 + Total Cache Memory Available: 3.8 + Battery Backed Cache Size: 3.8 + No-Battery Write Cache: Disabled + SSD Caching RAID5 WriteBack Enabled: True + SSD Caching Version: 2 + Cache Backup Power Source: Batteries + Battery/Capacitor Count: 1 + Battery/Capacitor Status: OK + SATA NCQ Supported: True + Spare Activation Mode: Activate on physical drive failure (default) + Spare Spindown Policy Supported: False + Controller Temperature (C): 60 + Capacitor Temperature (C): 48 + Number of Ports: 4 Internal only + Encryption: Not Set + Express Local Encryption: False + SED Based Encryption Supported: False + Driver Name: smartpqi + Driver Version: Linux 2.1.24-046 + WWN Port: 51402EC000000001 + PCI Address (Domain:Bus:Device.Function): 0000:5C:00.0 + Negotiated PCIe Data Rate: PCIe 3.0 x8 (7880 MB/s) + Controller Mode: Mixed + Port Max Phy Rate Limiting Supported: False + Latency Scheduler Setting: Disabled + Current Power Mode: MaxPerformance + Survival Mode: Enabled + Host Serial Number: SIMHOST001 + Sanitize Erase Supported: True + Sanitize Lock: None + Sensor ID: 0 + Location: Capacitor + Current Value (C): 48 + Max Value Since Power On: 52 + Sensor ID: 1 + Location: ASIC + Current Value (C): 60 + Max Value Since Power On: 66 + Sensor ID: 2 + Location: Unknown + Current Value (C): 46 + Max Value Since Power On: 51 + Primary Boot Volume: None + Secondary Boot Volume: None + SPDM Supports Get Slot Certificate Chain: no + SPDM Supports Get Controller Info : no + SPDM Supports Get Slot Info : no + SPDM Supports Set Import Certificate : no + SPDM Supports Set Invalidate Slot : no + Surface Scan Completion Supported: False + Persistent Event Log Policy Change Supported: False + UEFI Health Reporting Mode Supported: False + Firmware Supports NVMe Log Pages: False + Firmware Supports NAND Field: False + Firmware Supports NOR Field: False + + diff --git a/scenario/ssaclisim/ssaclisim.go b/scenario/ssaclisim/ssaclisim.go new file mode 100644 index 0000000..c0ad02e --- /dev/null +++ b/scenario/ssaclisim/ssaclisim.go @@ -0,0 +1,550 @@ +// Package ssaclisim is the scenario backend for HPE Smart Array controllers +// driven by ssacli: it answers the ssacli commands of the raidmgmt ssacli +// getters from a real capture with the events of a scenario applied. +// +// A capture is a directory under captures/ holding the text output of each +// ssacli command, named after it with "_" for spaces and "slot=0" written +// "slot0": "controller slot=0 show config" -> controller_slot0_show_config.txt. +// +// A failed drive has status Failed, and so has its logical drive, which loses +// its "Disk Name" (it is no longer exposed to the OS); a replacement drive is +// OK and unassigned, its array and logical drive gone; a new logical drive +// is OK, in the array the drive had in the capture. No ssacli output was +// captured while a drive was failed: this follows what storcli showed on +// RD-2154. The ssacli getters read volume paths from the controller output +// only, so a scenario needs no host file. +package ssaclisim + +import ( + "embed" + "path" + "regexp" + "sort" + "strings" + + "github.com/pkg/errors" + + "github.com/scality/raidmgmt/pkg/implementation/commandrunner" + "github.com/scality/raidmgmt/scenario" +) + +//go:embed captures +var captures embed.FS //nolint:gochecknoglobals // embedded captures + +const ( + unassigned = "-" + + physicalDrivesFile = "controller_slot0_physicaldrive_all_show_detail" + logicalDrivesFile = "controller_slot0_logicaldrive_all_show_detail" + configFile = "controller_slot0_show_config" + // failed is the status of a failed drive or logical drive. + failed = "Failed" +) + +//nolint:gochecknoglobals // compiled once +var ( + // sectionRegexp starts an "Array X" or "Unassigned" section of an output. + sectionRegexp = regexp.MustCompile(`(?m)^ (Array ([A-Z]+)|Unassigned)\b.*$`) + pdBlockRegexp = regexp.MustCompile(`(?ms)^ physicaldrive (\S+)\n.*?(?:\n\n|\z)`) + ldIDRegexp = regexp.MustCompile(`(?m)^ Logical Drive: (\d+)$`) + // statusRegexp is the "Status" line of a drive or logical drive block, + // not "MultiDomain Status" or "Drive Authentication Status". + statusRegexp = regexp.MustCompile(`(?m)^ Status: .*$`) + serialRegexp = regexp.MustCompile(`(?m)^( Serial Number: ).*$`) + wwidRegexp = regexp.MustCompile(`(?m)^( WWID: ).*$`) + driveTypeRegexp = regexp.MustCompile(`(?m)^( Drive Type: ).*$`) + uniqueIDRegexp = regexp.MustCompile(`(?m)^( Unique Identifier: ).*$`) + diskNameRegexp = regexp.MustCompile(`(?m)^ Disk Name: .*\n`) + configLDRegexp = regexp.MustCompile(`(?m)^( logicaldrive (\d+) \(.*, )[^,()]+\)$`) + configPDRegexp = regexp.MustCompile(`(?m)^( physicaldrive (\S+) \(.*, )[^,()]+\)$`) + trailingNewlines = regexp.MustCompile(`\n+$`) +) + +type ( + // Controller replays a capture with events applied: SSACLI answers ssacli + // commands, LSBLK the lsblk commands of the getters. + Controller struct { + capture string + drives map[string]*drive // by "port:box:bay" + volumes map[string]*volume // by logical drive ID + // configLines are the "show config" lines of the drives, used to + // list an unassigned drive. + configLines map[string]string + } + + drive struct { + status, serial, wwid string + // array is the array of the drive, or "-" when it is unassigned. + array string + // captured is the array of the drive in the capture: a drive can only + // leave it and come back. + captured string + } + + volume struct { + status, device, uniqueID string + exposed, deleted bool + // array is the array of the logical drive. + array string + } + + // Runner answers the commands of one CLI. It implements + // commandrunner.CommandRunner. + Runner func(args []string) ([]byte, error) +) + +var _ commandrunner.CommandRunner = Runner(nil) + +// Run answers a command. +func (r Runner) Run(args []string) ([]byte, error) { return r(args) } + +// New returns the controller of a capture once events happened, in order. +func New(capture string, events ...scenario.Event) (*Controller, error) { + c := &Controller{ + capture: path.Join("captures", capture), + drives: map[string]*drive{}, + volumes: map[string]*volume{}, + configLines: map[string]string{}, + } + + if err := c.loadCapture(); err != nil { + return nil, errors.Wrapf(err, "failed to load capture %s", capture) + } + + for i, e := range events { + if err := c.apply(e); err != nil { + return nil, errors.Wrapf(err, "failed to apply event %d", i+1) + } + } + + return c, nil +} + +// SSACLI returns the runner answering ssacli commands. +func (c *Controller) SSACLI() Runner { + return c.runSSACLI +} + +// LSBLK returns the runner answering lsblk commands. The getters only call +// lsblk for a drive exposed to the OS on its own, which no capture has. +func (*Controller) LSBLK() Runner { + return Runner(func(args []string) ([]byte, error) { + return nil, errors.Errorf("lsblk %s: not replayed", strings.Join(args, " ")) + }) +} + +func (c *Controller) runSSACLI(args []string) ([]byte, error) { + cmd := strings.Join(args, " ") + name := strings.ReplaceAll(strings.ReplaceAll(cmd, "slot=", "slot"), " ", "_") + + text, err := c.read(name) + if err != nil { + return nil, errors.Wrapf(err, "ssacli %s: not replayed", cmd) + } + + switch name { + case physicalDrivesFile: + text, err = c.renderPhysicalDrives(text) + case logicalDrivesFile: + text, err = c.renderLogicalDrives(text) + case configFile: + text, err = c.renderConfig(text) + } + + if err != nil { + return nil, errors.Wrapf(err, "ssacli %s", cmd) + } + + return []byte(text), nil +} + +func (c *Controller) loadCapture() error { + text, err := c.read(physicalDrivesFile) + if err != nil { + return err + } + + _, sections := splitSections(text) + for _, sec := range sections { + for _, m := range pdBlockRegexp.FindAllStringSubmatch(sec.body, -1) { + c.drives[m[1]] = &drive{ + status: field(m[0], "Status"), serial: field(m[0], "Serial Number"), + wwid: field(m[0], "WWID"), array: sec.array, captured: sec.array, + } + } + } + + if text, err = c.read(logicalDrivesFile); err != nil { + return err + } + + _, sections = splitSections(text) + for _, sec := range sections { + if m := ldIDRegexp.FindStringSubmatch(sec.body); m != nil { + device := field(sec.body, "Disk Name") + c.volumes[m[1]] = &volume{ + status: field(sec.body, "Status"), uniqueID: field(sec.body, "Unique Identifier"), + device: device, exposed: device != "", array: sec.array, + } + } + } + + if text, err = c.read(configFile); err != nil { + return err + } + + for _, m := range configPDRegexp.FindAllStringSubmatch(text, -1) { + c.configLines[m[2]] = m[0] + } + + return nil +} + +func (c *Controller) apply(e scenario.Event) error { + d, ok := c.drives[e.Slot] + if !ok { + return errors.Errorf("unknown drive %s", e.Slot) + } + + v, err := c.volumeOf(d) + if err != nil { + return errors.Wrapf(err, "drive %s", e.Slot) + } + + if err := checkOrder(e, d.status == failed, v.deleted); err != nil { + return err + } + + switch e.Kind { + case scenario.DriveFails: + d.status = failed + v.status, v.exposed = failed, false + case scenario.DriveReplaced: + v.deleted = true + d.status, d.serial, d.wwid, d.array = "OK", e.Serial, e.WWN, unassigned + case scenario.VolumeCreated: + d.array = d.captured + v.deleted, v.status, v.exposed, v.device, v.uniqueID = false, "OK", true, e.Device, e.VolumeWWN + default: + return errors.Errorf("event %d not replayed", e.Kind) + } + + return nil +} + +// checkOrder rejects an event that cannot happen to a drive in its current +// state: only a drive in a logical drive can fail, only a failed drive can be +// replaced, and a logical drive can only be created once the old one is +// deleted. +func checkOrder(e scenario.Event, failed, volumeDeleted bool) error { + switch { + case e.Kind == scenario.DriveFails && (failed || volumeDeleted): + return errors.Errorf("drive %s cannot fail: it has failed or is in no logical drive", e.Slot) + case e.Kind == scenario.DriveReplaced && !failed: + return errors.Errorf("drive %s cannot be replaced: it has not failed", e.Slot) + case e.Kind == scenario.VolumeCreated && !volumeDeleted: + return errors.Errorf("no logical drive can be created on drive %s: its one still exists", e.Slot) + } + + return nil +} + +// volumeOf returns the logical drive of the array of a drive in the capture. +// An array holding several logical drives is not replayed: which of them an +// event applies to would be a guess. +func (c *Controller) volumeOf(d *drive) (*volume, error) { + var found []string + + for id, v := range c.volumes { + if v.array == d.captured { + found = append(found, id) + } + } + + switch len(found) { + case 0: + return nil, errors.New("in no logical drive of the capture") + case 1: + return c.volumes[found[0]], nil + default: + sort.Strings(found) + + return nil, errors.Errorf("array %s holds logical drives %s: not replayed", + d.captured, strings.Join(found, ", ")) + } +} + +// renderPhysicalDrives lists each drive under its array, or under +// "Unassigned" once it belongs to none. +func (c *Controller) renderPhysicalDrives(text string) (string, error) { + header, sections := splitSections(text) + + out := []string{header} + + var spares []string + + for _, sec := range sections { + blocks, sectionSpares, err := c.renderSectionDrives(sec) + if err != nil { + return "", err + } + + spares = append(spares, sectionSpares...) + + if len(blocks) > 0 { + out = append(out, sec.title+"\n\n"+strings.Join(blocks, "")+"\n") + } + } + + if len(spares) > 0 { + out = append(out, " Unassigned\n\n"+strings.Join(spares, "")+"\n") + } + + return trailingNewlines.ReplaceAllString(strings.Join(out, ""), "\n"), nil +} + +// renderSectionDrives renders the drive blocks of a section: the ones still +// in an array, and the unassigned ones. +func (c *Controller) renderSectionDrives(sec section) (blocks, spares []string, err error) { + for _, m := range pdBlockRegexp.FindAllStringSubmatch(sec.body, -1) { + d, err := c.drive(m[1]) + if err != nil { + return nil, nil, err + } + + block := renderDrive(m[0], d) + + if d.array == unassigned { + spares = append(spares, block) + } else { + blocks = append(blocks, block) + } + } + + return blocks, spares, nil +} + +func renderDrive(block string, d *drive) string { + driveType := "Data Drive" + if d.array == unassigned { + driveType = "Unassigned Drive" + } + + out := statusRegexp.ReplaceAllString(block, " Status: "+d.status) + out = serialRegexp.ReplaceAllString(out, "${1}"+d.serial) + out = wwidRegexp.ReplaceAllString(out, "${1}"+d.wwid) + + return driveTypeRegexp.ReplaceAllString(out, "${1}"+driveType) +} + +// renderLogicalDrives drops deleted logical drives and the disk name of the +// ones not exposed to the OS. +func (c *Controller) renderLogicalDrives(text string) (string, error) { + header, sections := splitSections(text) + + out := []string{header} + + for _, sec := range sections { + m := ldIDRegexp.FindStringSubmatch(sec.body) + if m == nil { + continue + } + + v, err := c.volume(m[1]) + if err != nil { + return "", err + } + + if v.deleted { + continue + } + + body := statusRegexp.ReplaceAllString(sec.body, " Status: "+v.status) + body = uniqueIDRegexp.ReplaceAllString(body, "${1}"+v.uniqueID) + + diskName := "" + if v.exposed { + diskName = " Disk Name: " + v.device + " \n" + } + + body = diskNameRegexp.ReplaceAllString(body, diskName) + out = append(out, sec.title+body) + } + + return trailingNewlines.ReplaceAllString(strings.Join(out, ""), "\n"), nil +} + +// renderConfig updates the status of logical and physical drives, drops the +// arrays of deleted logical drives and lists unassigned drives. +func (c *Controller) renderConfig(text string) (string, error) { + if err := c.checkConfig(text); err != nil { + return "", err + } + + header, sections := splitSections(text) + + out := []string{header} + + for _, sec := range sections { + if m := configLDRegexp.FindStringSubmatch(sec.body); m != nil && c.volumes[m[2]].deleted { + continue + } + + out = append(out, sec.title+c.renderConfigSection(sec.body)) + } + + spares, err := c.unassignedConfigLines() + if err != nil { + return "", err + } + + if len(spares) > 0 { + out = append(out, "\n Unassigned\n\n"+strings.Join(spares, "\n")+"\n") + } + + return trailingNewlines.ReplaceAllString(strings.Join(out, ""), "\n"), nil +} + +// checkConfig checks that every logical and physical drive of a show config +// output is one of the capture, so that rendering it cannot fail. +func (c *Controller) checkConfig(text string) error { + for _, m := range configLDRegexp.FindAllStringSubmatch(text, -1) { + if _, err := c.volume(m[2]); err != nil { + return err + } + } + + for _, m := range configPDRegexp.FindAllStringSubmatch(text, -1) { + if _, err := c.drive(m[2]); err != nil { + return err + } + } + + return nil +} + +// renderConfigSection updates the status of the logical and physical drives +// of a show config section, and drops its unassigned drives. +func (c *Controller) renderConfigSection(body string) string { + withLDs := configLDRegexp.ReplaceAllStringFunc(body, func(line string) string { + m := configLDRegexp.FindStringSubmatch(line) + + return m[1] + c.volumes[m[2]].status + ")" + }) + + return configPDRegexp.ReplaceAllStringFunc(withLDs, func(line string) string { + m := configPDRegexp.FindStringSubmatch(line) + + d := c.drives[m[2]] + if d.array == unassigned { + // Listed in the Unassigned section instead. + return "" + } + + return m[1] + d.status + ")" + }) +} + +// unassignedConfigLines are the show config lines of the unassigned drives. +func (c *Controller) unassignedConfigLines() ([]string, error) { + ids := make([]string, 0, len(c.drives)) + for id := range c.drives { + ids = append(ids, id) + } + + sort.Strings(ids) + + var lines []string + + for _, id := range ids { + if d := c.drives[id]; d.array == unassigned { + m := configPDRegexp.FindStringSubmatch(c.configLines[id]) + if m == nil { + return nil, errors.Errorf("drive %s is not in the captured show config", id) + } + + lines = append(lines, m[1]+d.status+")") + } + } + + return lines, nil +} + +// drive returns a drive of the capture. A drive missing from it means an +// inconsistent capture. +func (c *Controller) drive(id string) (*drive, error) { + d, ok := c.drives[id] + if !ok { + return nil, errors.Errorf("unknown drive %s", id) + } + + return d, nil +} + +// volume returns a logical drive of the capture. A logical drive missing +// from it means an inconsistent capture. +func (c *Controller) volume(id string) (*volume, error) { + v, ok := c.volumes[id] + if !ok { + return nil, errors.Errorf("unknown logical drive %s", id) + } + + return v, nil +} + +// section is an "Array X" or "Unassigned" section of an output. +type section struct { + title, body string + // array is the array letter, or "-" for the unassigned section. + array string +} + +// splitSections splits an output into the text before the first section and +// its sections. +func splitSections(text string) (string, []section) { + locs := sectionRegexp.FindAllStringSubmatchIndex(text, -1) + if len(locs) == 0 { + return text, nil + } + + sections := make([]section, 0, len(locs)) + + for i, loc := range locs { + end := len(text) + if i+1 < len(locs) { + end = locs[i+1][0] + } + + array := unassigned + if loc[4] >= 0 { + array = text[loc[4]:loc[5]] + } + + sections = append(sections, section{ + title: text[loc[0]:loc[1]], body: text[loc[1]:end], array: array, + }) + } + + return text[:locs[0][0]], sections +} + +// field returns the value of the first "Key: value" line of a block. +func field(block, key string) string { + re := regexp.MustCompile(`(?m)^\s+` + regexp.QuoteMeta(key) + `: (.*?)\s*$`) + + m := re.FindStringSubmatch(block) + if m == nil { + return "" + } + + return m[1] +} + +func (c *Controller) read(name string) (string, error) { + data, err := captures.ReadFile(path.Join(c.capture, name+".txt")) + if err != nil { + return "", errors.Wrapf(err, "no capture for %s", name) + } + + return string(data), nil +} diff --git a/scenario/ssaclisim/ssaclisim_internal_test.go b/scenario/ssaclisim/ssaclisim_internal_test.go new file mode 100644 index 0000000..0639b2a --- /dev/null +++ b/scenario/ssaclisim/ssaclisim_internal_test.go @@ -0,0 +1,81 @@ +package ssaclisim + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/scality/raidmgmt/scenario" +) + +// TestEventOnArrayWithSeveralVolumes checks that an event on a drive whose +// array holds several logical drives fails, rather than applying to whichever +// of them the map iteration reaches first. +func TestEventOnArrayWithSeveralVolumes(t *testing.T) { + t.Parallel() + + c := &Controller{ + drives: map[string]*drive{"1I:1:1": {status: "OK", array: "B", captured: "B"}}, + volumes: map[string]*volume{ + "6": {status: "OK", array: "B", exposed: true}, + "2": {status: "OK", array: "B", exposed: true}, + }, + } + + err := c.apply(scenario.Event{Kind: scenario.DriveFails, Slot: "1I:1:1"}) + require.ErrorContains(t, err, "array B holds logical drives 2, 6: not replayed") +} + +// TestRunInconsistentCapture checks that a capture listing a drive or a +// logical drive the controller does not know fails with an error, not a +// panic. +func TestRunInconsistentCapture(t *testing.T) { + t.Parallel() + + const ( + config = "controller slot=0 show config" + physicalDrives = "controller slot=0 physicaldrive all show detail" + logicalDrives = "controller slot=0 logicaldrive all show detail" + ) + + forgetDrive := func(c *Controller) { delete(c.drives, "1I:1:4") } + forgetVolume := func(c *Controller) { delete(c.volumes, "3") } + + tests := []struct { + name string + events []scenario.Event + forget func(c *Controller) + cmd string + wantErr string + }{ + {name: "drive, config", forget: forgetDrive, cmd: config, wantErr: "unknown drive 1I:1:4"}, + {name: "logical drive, config", forget: forgetVolume, cmd: config, wantErr: "unknown logical drive 3"}, + {name: "drive, physical drives", forget: forgetDrive, cmd: physicalDrives, wantErr: "unknown drive 1I:1:4"}, + {name: "logical drive, logical drives", forget: forgetVolume, cmd: logicalDrives, wantErr: "unknown logical drive 3"}, + { + name: "unassigned drive missing from config", + events: []scenario.Event{ + {Kind: scenario.DriveFails, Slot: "1I:1:4"}, + {Kind: scenario.DriveReplaced, Slot: "1I:1:4", Serial: "S", WWN: "W"}, + }, + forget: func(c *Controller) { delete(c.configLines, "1I:1:4") }, + cmd: config, + wantErr: "drive 1I:1:4 is not in the captured show config", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + c, err := New("ssacli-4hdd", tt.events...) + require.NoError(t, err) + + tt.forget(c) + + _, err = c.SSACLI().Run(strings.Fields(tt.cmd)) + require.ErrorContains(t, err, tt.wantErr) + }) + } +}