Skip to content

Commit

Permalink
Add support for hdparm based drive wipe (#167)
Browse files Browse the repository at this point in the history
### What does this PR do

Similar to what we're doing for NVME devices, this command supports
Sanitizing and Erasing and decides which function and parameters to use
depending on the capabilities reported by the drive.

Sanitize is preferred over erase. Cryptographic scramble is preferred of
block erase within Sanitize.

I reused the nvme sanitize/erase type since utils is all one big package
anyway to avoid confusion. If we ever split each utility into its own
package (as I want) then we'll just create a more specific type for
hdparm.
  • Loading branch information
mmlb authored Jul 9, 2024
2 parents 40a5499 + 42f0690 commit ececddd
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 8 deletions.
28 changes: 20 additions & 8 deletions examples/diskwipe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,30 @@ func main() {
case "nvme":
wiper = utils.NewNvmeCmd(*verbose)
case "sata":
// Lets see if drive supports TRIM, if so we'll use blkdiscard
// Lets figure out the drive capabilities in an easier format
var sanitize bool
var esee bool
var trim bool
for _, cap := range drive.Capabilities {
if strings.HasPrefix(cap.Description, "Data Set Management TRIM supported") {
if cap.Enabled {
wiper = utils.NewBlkdiscardCmd(*verbose)
}
break
switch {
case cap.Description == "encryption supports enhanced erase":
esee = cap.Enabled
case cap.Description == "SANITIZE feature":
sanitize = cap.Enabled
case strings.HasPrefix(cap.Description, "Data Set Management TRIM supported"):
trim = cap.Enabled
}
}

// drive does not support TRIM so we fall back to filling it up with zero
if wiper == nil {
switch {
case sanitize || esee:
// Drive supports Sanitize or Enhanced Erase, so we use hdparm
wiper = utils.NewHdparmCmd(*verbose)
case trim:
// Drive supports TRIM, so we use blkdiscard
wiper = utils.NewBlkdiscardCmd(*verbose)
default:
// Drive does not support any preferred wipe method so we fall back to filling it up with zero
wiper = utils.NewFillZeroCmd(*verbose)

// If the user supplied a non-default timeout then we'll honor it, otherwise we just go with a huge timeout.
Expand Down
134 changes: 134 additions & 0 deletions utils/hdparm.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package utils

import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"regexp"
"strings"
"time"

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

const (
Expand Down Expand Up @@ -207,6 +211,136 @@ func (h *Hdparm) DriveCapabilities(ctx context.Context, logicalName string) ([]*
return capabilities, err
}

// WipeDrive implements DriveWipe by calling Sanitize or Erase as appropriate.
// Sanitize(CryptoErase) is preferred over Sanitize(BlockErase) which is preferred over Erase(CryptographicErase).
func (h *Hdparm) WipeDrive(ctx context.Context, logger *logrus.Logger, drive *common.Drive) error { // nolint:gocyclo
var (
esee bool
eseu bool
sanitize bool
bee bool
cse bool
)
for _, cap := range drive.Capabilities {
switch {
case cap.Description == "encryption supports enhanced erase":
esee = cap.Enabled
case cap.Description == "BLOCK ERASE EXT":
bee = cap.Enabled
case cap.Description == "CRYPTO SCRAMBLE EXT":
cse = cap.Enabled
case cap.Description == "SANITIZE feature":
sanitize = cap.Enabled
case strings.HasPrefix(cap.Description, "erase time:"):
eseu = strings.Contains(cap.Description, "enhanced")
}
}

l := logger.WithField("drive", drive.LogicalName)
if sanitize && cse {
// nolint:govet
l := l.WithField("method", "sanitize").WithField("action", "sanitize-crypto-scramble")
l.Info("wiping")
err := h.Sanitize(ctx, drive, CryptoErase)
if err == nil {
return nil
}
l.WithError(err).Info("failed")
}
if sanitize && bee {
// nolint:govet
l := l.WithField("method", "sanitize").WithField("action", "sanitize-block-erase")
l.Info("wiping")
err := h.Sanitize(ctx, drive, BlockErase)
if err == nil {
return nil
}
l.WithError(err).Info("failed")
}
if esee && eseu {
// nolint:govet
l := l.WithField("method", "security-erase-enhanced")
l.Info("wiping")
err := h.Erase(ctx, drive, CryptographicErase)
if err == nil {
return nil
}
l.WithError(err).Info("failed")
}
return ErrIneffectiveWipe
}

// Sanitize wipes drive using `ATA Sanitize Device` via hdparm --sanitize
func (h *Hdparm) Sanitize(ctx context.Context, drive *common.Drive, sanact SanitizeAction) error {
var sanType string
switch sanact { // nolint:exhaustive
case BlockErase:
sanType = "block-erase"
case CryptoErase:
sanType = "crypto-scramble"
default:
return fmt.Errorf("%w: %v", errSanitizeInvalidAction, sanact)
}

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

h.Executor.SetArgs("--yes-i-know-what-i-am-doing", "--sanitize-"+sanType, drive.LogicalName)
_, err = h.Executor.Exec(ctx)
if err != nil {
return err
}

// now we loop until --sanitize-status reports that sanitization is complete
for {
h.Executor.SetArgs("--sanitize-status", drive.LogicalName)
result, err := h.Executor.Exec(ctx)
if err != nil {
return err
}
if h.sanitizeDone(result.Stdout) {
break
}
time.Sleep(100 * time.Millisecond)
}

return verify()
}

func (h *Hdparm) sanitizeDone(output []byte) bool {
return bytes.Contains(output, []byte("Sanitize Idle"))
}

// Erase wipes drive using ATA Secure Erase via hdparm --security-erase-enhanced
func (h *Hdparm) Erase(ctx context.Context, drive *common.Drive, ses SecureEraseSetting) error {
switch ses { // nolint:exhaustive
case CryptographicErase:
default:
return fmt.Errorf("%w: %v", errFormatInvalidSetting, ses)
}

h.Executor.SetArgs("--user-master", "u", "--security-set-pass", "p", drive.LogicalName)
_, err := h.Executor.Exec(ctx)
if err != nil {
return err
}

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

h.Executor.SetArgs("--user-master", "u", "--security-erase-enhanced", "p", drive.LogicalName)
_, err = h.Executor.Exec(ctx)
if err != nil {
return err
}

return verify()
}

// NewFakeHdparm returns a mock hdparm collector that returns mock data for use in tests.
func NewFakeHdparm() *Hdparm {
return &Hdparm{
Expand Down
58 changes: 58 additions & 0 deletions utils/hdparm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,61 @@ var fixtureHdparmDeviceCapabilities = []*common.Capability{
Enabled: false,
},
}

func Test_HdparmSanitizeDone(t *testing.T) {
// note the order is not messed up, this is indeed what hdparm showed me
tests := []struct {
name string
done bool
in string
}{
{
name: "5%",
in: `
/dev/sdb:
Issuing SANITIZE_STATUS command
Sanitize status:
State: SD2 Sanitize operation In Process
Progress: 0xf5a (5%)
`,
},
{
name: "1%",
in: `
/dev/sdb:
Issuing SANITIZE_STATUS command
Sanitize status:
State: SD2 Sanitize operation In Process
Progress: 0x28f (1%)
`,
},
{
name: "3%",
in: `
/dev/sdb:
Issuing SANITIZE_STATUS command
Sanitize status:
State: SD2 Sanitize operation In Process
Progress: 0xa3c (3%)
`,
},
{
name: "done",
done: true,
in: `
/dev/sdb:
Issuing SANITIZE_STATUS command
Sanitize status:
State: SD0 Sanitize Idle
Last Sanitize Operation Completed Without Error
`,
},
}

var h Hdparm
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.done, h.sanitizeDone([]byte(test.in)))
})
}
}

0 comments on commit ececddd

Please sign in to comment.