Skip to content

Commit

Permalink
Add DiskWipe support to nvme using sanitize (#136)
Browse files Browse the repository at this point in the history
### What does this PR do

Adds support to nvme for wiping the drive using sanitize ops. Both
crypto erase and block erase are supported. The appropriate sanitize
action is chosen based on detected capabilities.

### How can this change be tested by a PR reviewer?

Run on a machine with an nvme device.
  • Loading branch information
mmlb authored May 13, 2024
2 parents b9bec5a + c9f2811 commit 4fbf55d
Show file tree
Hide file tree
Showing 10 changed files with 333 additions and 21 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/push-pr-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ jobs:
with:
go-version-file: go.mod

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
args: --config .golangci.yml
version: v1.57
- name: Run golangci-lint
run: make golangci-lint

- name: Check go generated files
run: make check-go-generated

- name: Test
run: go test ./...
- name: Run go tests
run: make go-test

- name: Set up Docker Buildx
- name: Set up docker buildx
uses: docker/setup-buildx-action@v3

- name: Build image - no push
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### Go template
/bin/

# Test binary, build with `go test -c`
*.test
Expand Down
29 changes: 24 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
.DEFAULT_GOAL := help

## lint
lint:
go run github.com/golangci/golangci-lint/cmd/[email protected] run --config .golangci.yml
export GOBIN=$(CURDIR)/bin
export PATH:=$(PATH):$(GOBIN)

## Go test
test: lint
## Run all linters
lint: golangci-lint check-go-generated

## Run golangci-lint
golangci-lint:
go install github.com/golangci/golangci-lint/cmd/[email protected]
golangci-lint run --config .golangci.yml

## Run go generate
generate:
go install golang.org/x/tools/cmd/[email protected]
go generate ./...

## Check generated files are up to date
check-go-generated: generate
git diff | (! grep .)

## Run go test
go-test:
CGO_ENABLED=0 go test -v -covermode=atomic ./...

## Run all tests and linters
test: go-test lint

# https://gist.github.com/prwhite/8168133
# COLORS
GREEN := $(shell tput -Txterm setaf 2)
Expand Down
1 change: 1 addition & 0 deletions examples/nvme-wipe/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nvme-wipe
40 changes: 40 additions & 0 deletions examples/nvme-wipe/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"context"
"flag"
"time"

"github.com/metal-toolbox/ironlib/utils"
"github.com/sirupsen/logrus"
)

var (
device = flag.String("device", "/dev/nvmeX", "nvme disk to wipe")
verbose = flag.Bool("verbose", false, "show command runs and output")
dur = flag.String("timeout", (1 * time.Minute).String(), "time to wait for command to complete")
)

func main() {
flag.Parse()

logger := logrus.New()
logger.Formatter = new(logrus.TextFormatter)
logger.SetLevel(logrus.TraceLevel)

timeout, err := time.ParseDuration(*dur)
if err != nil {
logger.WithError(err).Fatal("failed to parse timeout duration")
}

nvme := utils.NewNvmeCmd(*verbose)

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

logger.Info("wiping")
err = nvme.WipeDisk(ctx, logger, *device)
if err != nil {
logger.WithError(err).Fatal("exiting")
}
}
36 changes: 36 additions & 0 deletions utils/fake_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package utils
import (
"bytes"
"context"
"fmt"
"io"
"os"
"path"
"strings"
)

Expand Down Expand Up @@ -65,6 +67,40 @@ func (e *FakeExecute) Exec(_ context.Context) (*Result, error) {
}

e.Stdout = b
case "sanitize":
dev := e.Args[len(e.Args)-1]
f, err := os.OpenFile(dev, os.O_WRONLY, 0)
if err != nil {
return nil, err
}
size, err := f.Seek(0, io.SeekEnd)
if err != nil {
return nil, err
}
err = f.Truncate(0)
if err != nil {
return nil, err
}
err = f.Sync()
if err != nil {
return nil, err
}
err = f.Truncate(size)
if err != nil {
return nil, err
}
err = f.Sync()
if err != nil {
return nil, err
}
err = f.Close()
if err != nil {
return nil, err
}
case "sanitize-log":
dev := e.Args[len(e.Args)-1]
dev = path.Base(dev)
e.Stdout = []byte(fmt.Sprintf(`{%q:{"sprog":65535}}`, dev))
}
case "hdparm":
if e.Args[0] == "-I" {
Expand Down
111 changes: 110 additions & 1 deletion utils/nvme.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"strconv"
"strings"
"time"

"github.com/bmc-toolbox/common"
"github.com/metal-toolbox/ironlib/model"
"github.com/sirupsen/logrus"
)

const EnvNvmeUtility = "IRONLIB_UTIL_NVME"

var errSanicapNODMMASReserved = errors.New("sanicap nodmmas reserved bits set, not sure what to do with them")
var (
errSanicapNODMMASReserved = errors.New("sanicap nodmmas reserved bits set, not sure what to do with them")
errSanitizeInvalidAction = errors.New("invalid sanitize action")
)

type Nvme struct {
Executor Executor
Expand Down Expand Up @@ -259,6 +267,107 @@ func parseSanicap(sanicap uint) ([]*common.Capability, error) {
return caps, nil
}

//go:generate stringer -type SanitizeAction
type SanitizeAction uint8

const (
Invalid SanitizeAction = iota
ExitFailureMode
BlockErase
Overwrite
CryptoErase
)

// WipeDisk implements DiskWiper by running nvme sanitize
func (n *Nvme) WipeDisk(ctx context.Context, logger *logrus.Logger, device string) error {
caps, err := n.DriveCapabilities(ctx, device)
if err != nil {
return fmt.Errorf("WipeDisk: %w", err)
}
return n.wipe(ctx, logger, device, caps)
}

func (n *Nvme) wipe(ctx context.Context, logger *logrus.Logger, device string, caps []*common.Capability) error {
var ber bool
var cer bool
for _, cap := range caps {
switch cap.Name {
case "ber":
ber = cap.Enabled
case "cer":
cer = cap.Enabled
}
}

if cer {
l := logger.WithField("method", "sanitize").WithField("action", CryptoErase)
l.Info("trying wipe")
err := n.Sanitize(ctx, device, CryptoErase)
if err == nil {
return nil
}
l.WithError(err).Info("failed")
}
if ber {
l := logger.WithField("method", "sanitize").WithField("action", BlockErase)
l.Info("trying wipe")
err := n.Sanitize(ctx, device, BlockErase)
if err == nil {
return nil
}
l.WithError(err).Info("failed")
}
return ErrIneffectiveWipe
}

func (n *Nvme) Sanitize(ctx context.Context, device string, sanact SanitizeAction) error {
switch sanact { // nolint:exhaustive
case BlockErase, CryptoErase:
default:
return fmt.Errorf("%w: %v", errSanitizeInvalidAction, sanact)
}

verify, err := ApplyWatermarks(device)
if err != nil {
return err
}

n.Executor.SetArgs("sanitize", "--sanact="+strconv.Itoa(int(sanact)), device)
_, err = n.Executor.Exec(ctx)
if err != nil {
return err
}

// now we loop until sanitize-log reports that sanitization is complete
dev := path.Base(device)
var log map[string]struct {
Progress uint16 `json:"sprog"`
}
for {
n.Executor.SetArgs("sanitize-log", "--output-format=json", device)
result, err := n.Executor.Exec(ctx)
if err != nil {
return err
}
err = json.Unmarshal(result.Stdout, &log)
if err != nil {
return err
}

l, ok := log[dev]
if !ok {
return fmt.Errorf("%s: device not present in sanitize-log: %w: %s", dev, io.ErrUnexpectedEOF, result.Stdout)
}

if l.Progress == 65535 {
break
}
time.Sleep(100 * time.Millisecond)
}

return verify()
}

// NewFakeNvme returns a mock nvme collector that returns mock data for use in tests.
func NewFakeNvme() *Nvme {
return &Nvme{
Expand Down
78 changes: 78 additions & 0 deletions utils/nvme_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package utils

import (
"context"
"fmt"
"os"
"strconv"
"testing"

"github.com/bmc-toolbox/common"
tlogrus "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -157,3 +160,78 @@ func Test_NvmeParseSanicap(t *testing.T) {
require.Nil(t, caps)
})
}

func fakeNVMEDevice(t *testing.T) string {
dir := t.TempDir()
f, err := os.Create(dir + "/nvme0n1")
require.NoError(t, err)
require.NoError(t, f.Truncate(20*1024))
require.NoError(t, f.Close())
return f.Name()
}

func Test_NvmeSanitize(t *testing.T) {
for action := range CryptoErase {
t.Run(action.String(), func(t *testing.T) {
n := NewFakeNvme()
dev := fakeNVMEDevice(t)
err := n.Sanitize(context.Background(), dev, action)

switch action { // nolint:exhaustive
case BlockErase, CryptoErase:
require.NoError(t, err)
// FakeExecute is a bad mocker since it doesn't record all calls and sanitize-log calls aren't that interesting
// TODO: Setup better mocks
//
// e, ok := n.Executor.(*FakeExecute)
// require.True(t, ok)
// require.Equal(t, []string{"sanitize", "--sanact=2", dev}, e.Args)
default:
require.Error(t, err)
require.ErrorIs(t, err, errSanitizeInvalidAction)
}
})
}
}

func Test_NvmeWipe(t *testing.T) {
tests := []struct {
caps map[string]bool
args []string
}{
{caps: map[string]bool{"ber": false, "cer": false}},
{caps: map[string]bool{"ber": false, "cer": true}, args: []string{"sanitize", "--sanact=4"}},
{caps: map[string]bool{"ber": true, "cer": false}, args: []string{"sanitize", "--sanact=2"}},
{caps: map[string]bool{"ber": true, "cer": true}, args: []string{"sanitize", "--sanact=4"}},
}
for _, test := range tests {
name := fmt.Sprintf("ber=%v,cer=%v", test.caps["ber"], test.caps["cer"])
t.Run(name, func(t *testing.T) {
caps := []*common.Capability{
{Name: "ber", Enabled: test.caps["ber"]},
{Name: "cer", Enabled: test.caps["cer"]},
}
n := NewFakeNvme()
dev := fakeNVMEDevice(t)
logger, hook := tlogrus.NewNullLogger()
defer hook.Reset()

err := n.wipe(context.Background(), logger, dev, caps)

if test.args == nil {
require.Error(t, err)
require.ErrorIs(t, err, ErrIneffectiveWipe)
return
}

require.NoError(t, err)
// FakeExecute is a bad mocker since it doesn't record all calls and sanitize-log calls aren't that interesting
// TODO: Setup better mocks
//
// e, ok := n.Executor.(*FakeExecute)
// require.True(t, ok)
// test.args = append(test.args, dev)
// require.Equal(t, test.args, e.Args)
})
}
}
Loading

0 comments on commit 4fbf55d

Please sign in to comment.