From 8d28858e45f6c92bd8bda276654ba05e41461ee6 Mon Sep 17 00:00:00 2001 From: Manuel Mendez <708570+mmlb@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:02:42 -0400 Subject: [PATCH] Better nvme capability detection (#129) * utils/nvme: Parse nvme json output not human readable JSON + bit twiddling is more likely to be future compatible (hopefully). This library is not a human and thus should not use the human readable output. I would say that json + bit fiddling is also easier to understand than a regex based state machine. * utils/nvme: Use nvme cli internal names for capabilities These dynamic/generated names don't really work as can probably be noticed by adding the `fna` and `sanicap` synthetic capabilities. They don't work because nvme gives different output for enabled/disable, supported/unsupported, all/single capabilities. So we end up with enabled/disabled is detected by presence vs absence and we have to look up two "different" capabilities to determine the status of an _actual_ capability. So instead of doing that dynamic name we just use the internal nvme identifiers seen in `nvme-print-stdout.c` at this point in time. We will not change them later if nvme changes them internally. This way we end up being more likely to be future proof. It would be much easier for nvme-cli to tweak the string returned in the human readable output than it would be for the linux kernel to change meaning of bits returned in the ioctl call. * utils/nvme: Get rid of fake nvme capabilities fna and sanicap These don't really tell much to callers, considering we have a better breakdown of what fna and sanicap mean so lets just drop it. * utils/nvme: Explicitly test parsing fna and sanicap values Better to test explicitly for all expected cases than implicitly/by proxy from the other tests. --- fixtures/dell/r6515.go | 20 +- fixtures/utils/nvme/nvmecli-id-ctrl | 374 +++++++++++++--------------- utils/nvme.go | 227 +++++++---------- utils/nvme_test.go | 180 +++++++------ 4 files changed, 377 insertions(+), 424 deletions(-) diff --git a/fixtures/dell/r6515.go b/fixtures/dell/r6515.go index aa1381f1..4a30a913 100644 --- a/fixtures/dell/r6515.go +++ b/fixtures/dell/r6515.go @@ -1782,20 +1782,16 @@ var ( } nvmeDriveCapabilities = []*common.Capability{ - {Name: "sanicap", Description: "Sanitize Support"}, - {Name: "ammasocsind", Description: "Additional media modification after sanitize operation completes successfully is not defined"}, - {Name: "nasbiscs", Description: "No-Deallocate After Sanitize bit in Sanitize command Supported"}, - {Name: "osons", Description: "Overwrite Sanitize Operation Not Supported"}, - {Name: "besons", Description: "Block Erase Sanitize Operation Not Supported"}, - {Name: "cesons", Description: "Crypto Erase Sanitize Operation Not Supported"}, - {Name: "fna", Description: "Crypto Erase Support", Enabled: true}, - {Name: "cesapose", Description: "Crypto Erase Supported as part of Secure Erase", Enabled: true}, - {Name: "ceatsn", Description: "Crypto Erase Applies to Single Namespace(s)"}, - {Name: "fatsn", Description: "Format Applies to Single Namespace(s)"}, + {Name: "fmns", Description: "Format Applies to All/Single Namespace(s) (t:All, f:Single)"}, + {Name: "cens", Description: "Crypto Erase Applies to All/Single Namespace(s) (t:All, f:Single)"}, + {Name: "cese", Description: "Crypto Erase Supported as part of Secure Erase", Enabled: true}, + {Name: "cer", Description: "Crypto Erase Sanitize Operation Supported"}, + {Name: "ber", Description: "Block Erase Sanitize Operation Supported"}, + {Name: "owr", Description: "Overwrite Sanitize Operation Supported"}, + {Name: "ndi", Description: "No-Deallocate After Sanitize bit in Sanitize command Supported"}, } - // - // // r6515 inventory taken with lshw, merged with data from smartctl + // r6515 inventory taken with lshw, merged with data from smartctl R6515_inventory_lshw_smartctl = &common.Device{ Common: common.Common{ Oem: false, diff --git a/fixtures/utils/nvme/nvmecli-id-ctrl b/fixtures/utils/nvme/nvmecli-id-ctrl index 6167e448..75f1b173 100644 --- a/fixtures/utils/nvme/nvmecli-id-ctrl +++ b/fixtures/utils/nvme/nvmecli-id-ctrl @@ -1,204 +1,170 @@ -NVME Identify Controller: -vid : 0x1344 -ssvid : 0x1344 -sn : 201327AA90B6 -mn : Micron_9300_MTFDHAL3T8TDP -fr : 11300DN0 -rab : 0 -ieee : 00a075 -cmic : 0 - [3:3] : 0 ANA not supported - [2:2] : 0 PCI - [1:1] : 0 Single Controller - [0:0] : 0 Single Port - -mdts : 5 -cntlid : 0x1 -ver : 0x10200 -rtd3r : 0xe4e1c0 -rtd3e : 0x989680 -oaes : 0x300 -[14:14] : 0 Endurance Group Event Aggregate Log Page Change Notice Not Supported -[13:13] : 0 LBA Status Information Notices Not Supported -[12:12] : 0 Predictable Latency Event Aggregate Log Change Notices Not Supported -[11:11] : 0 Asymmetric Namespace Access Change Notices Not Supported - [9:9] : 0x1 Firmware Activation Notices Supported - [8:8] : 0x1 Namespace Attribute Changed Event Supported - -ctratt : 0 - [9:9] : 0 UUID List Not Supported - [7:7] : 0 Namespace Granularity Not Supported - [5:5] : 0 Predictable Latency Mode Not Supported - [4:4] : 0 Endurance Groups Not Supported - [3:3] : 0 Read Recovery Levels Not Supported - [2:2] : 0 NVM Sets Not Supported - [1:1] : 0 Non-Operational Power State Permissive Not Supported - [0:0] : 0 128-bit Host Identifier Not Supported - -rrls : 0 -crdt1 : 0 -crdt2 : 0 -crdt3 : 0 -oacs : 0xe - [9:9] : 0 Get LBA Status Capability Not Supported - [8:8] : 0 Doorbell Buffer Config Not Supported - [7:7] : 0 Virtualization Management Not Supported - [6:6] : 0 NVMe-MI Send and Receive Not Supported - [5:5] : 0 Directives Not Supported - [4:4] : 0 Device Self-test Not Supported - [3:3] : 0x1 NS Management and Attachment Supported - [2:2] : 0x1 FW Commit and Download Supported - [1:1] : 0x1 Format NVM Supported - [0:0] : 0 Security Send and Receive Not Supported - -acl : 4 -aerl : 5 -frmw : 0x17 - [4:4] : 0x1 Firmware Activate Without Reset Supported - [3:1] : 0x3 Number of Firmware Slots - [0:0] : 0x1 Firmware Slot 1 Read-Only - -lpa : 0x2 - [3:3] : 0 Telemetry host/controller initiated log page Not Supported - [2:2] : 0 Extended data for Get Log Page Not Supported - [1:1] : 0x1 Command Effects Log Page Supported - [0:0] : 0 SMART/Health Log Page per NS Not Supported - -elpe : 62 -npss : 15 -avscc : 0x1 - [0:0] : 0x1 Admin Vendor Specific Commands uses NVMe Format - -apsta : 0 - [0:0] : 0 Autonomous Power State Transitions Not Supported - -wctemp : 348 -cctemp : 353 -mtfa : 50 -hmpre : 0 -hmmin : 0 -tnvmcap : 3840755982336 -unvmcap : 0 -rpmbs : 0 - [31:24]: 0 Access Size - [23:16]: 0 Total Size - [5:3] : 0 Authentication Method - [2:0] : 0 Number of RPMB Units - -edstt : 0 -dsto : 0 -fwug : 0 -kas : 0 -hctma : 0 - [0:0] : 0 Host Controlled Thermal Management Not Supported - -mntmt : 0 -mxtmt : 0 -sanicap : 0 - [31:30] : 0 Additional media modification after sanitize operation completes successfully is not defined - [29:29] : 0 No-Deallocate After Sanitize bit in Sanitize command Supported - [2:2] : 0 Overwrite Sanitize Operation Not Supported - [1:1] : 0 Block Erase Sanitize Operation Not Supported - [0:0] : 0 Crypto Erase Sanitize Operation Not Supported - -hmminds : 0 -hmmaxd : 0 -nsetidmax : 0 -anatt : 0 -anacap : 0 - [7:7] : 0 Non-zero group ID Not Supported - [6:6] : 0 Group ID does not change - [4:4] : 0 ANA Change state Not Supported - [3:3] : 0 ANA Persistent Loss state Not Supported - [2:2] : 0 ANA Inaccessible state Not Supported - [1:1] : 0 ANA Non-optimized state Not Supported - [0:0] : 0 ANA Optimized state Not Supported - -anagrpmax : 0 -nanagrpid : 0 -sqes : 0x66 - [7:4] : 0x6 Max SQ Entry Size (64) - [3:0] : 0x6 Min SQ Entry Size (64) - -cqes : 0x44 - [7:4] : 0x4 Max CQ Entry Size (16) - [3:0] : 0x4 Min CQ Entry Size (16) - -maxcmd : 0 -nn : 32 -oncs : 0x14 - [7:7] : 0 Verify Not Supported - [6:6] : 0 Timestamp Not Supported - [5:5] : 0 Reservations Not Supported - [4:4] : 0x1 Save and Select Supported - [3:3] : 0 Write Zeroes Not Supported - [2:2] : 0x1 Data Set Management Supported - [1:1] : 0 Write Uncorrectable Not Supported - [0:0] : 0 Compare Not Supported - -fuses : 0 - [0:0] : 0 Fused Compare and Write Not Supported - -fna : 0x4 - [2:2] : 0x1 Crypto Erase Supported as part of Secure Erase - [1:1] : 0 Crypto Erase Applies to Single Namespace(s) - [0:0] : 0 Format Applies to Single Namespace(s) - -vwc : 0 - [0:0] : 0 Volatile Write Cache Not Present - -awun : 0 -awupf : 0 -nvscc : 1 - [0:0] : 0x1 NVM Vendor Specific Commands uses NVMe Format - -nwpc : 0 - [2:2] : 0 Permanent Write Protect Not Supported - [1:1] : 0 Write Protect Until Power Supply Not Supported - [0:0] : 0 No Write Protect and Write Protect Namespace Not Supported - -acwu : 0 -sgls : 0 - [1:0] : 0 Scatter-Gather Lists Not Supported - -mnan : 0 -subnqn : nqn.2016-08.com.micron:nvme:nvm-subsystem-sn-201327AA90B6 -ioccsz : 0 -iorcsz : 0 -icdoff : 0 -ctrattr : 0 - [0:0] : 0 Dynamic Controller Model - -msdbd : 0 -ps 0 : mp:25.00W operational enlat:100 exlat:100 rrt:0 rrl:0 - rwt:0 rwl:0 idle_power:- active_power:- -ps 1 : mp:24.00W operational enlat:115 exlat:115 rrt:1 rrl:1 - rwt:1 rwl:1 idle_power:- active_power:- -ps 2 : mp:23.00W operational enlat:130 exlat:130 rrt:2 rrl:2 - rwt:2 rwl:2 idle_power:- active_power:- -ps 3 : mp:22.00W operational enlat:145 exlat:145 rrt:3 rrl:3 - rwt:3 rwl:3 idle_power:- active_power:- -ps 4 : mp:21.00W operational enlat:160 exlat:160 rrt:4 rrl:4 - rwt:4 rwl:4 idle_power:- active_power:- -ps 5 : mp:20.00W operational enlat:175 exlat:175 rrt:5 rrl:5 - rwt:5 rwl:5 idle_power:- active_power:- -ps 6 : mp:19.00W operational enlat:190 exlat:190 rrt:6 rrl:6 - rwt:6 rwl:6 idle_power:- active_power:- -ps 7 : mp:18.00W operational enlat:205 exlat:205 rrt:7 rrl:7 - rwt:7 rwl:7 idle_power:- active_power:- -ps 8 : mp:17.00W operational enlat:220 exlat:220 rrt:8 rrl:8 - rwt:8 rwl:8 idle_power:- active_power:- -ps 9 : mp:16.00W operational enlat:235 exlat:235 rrt:9 rrl:9 - rwt:9 rwl:9 idle_power:- active_power:- -ps 10 : mp:15.00W operational enlat:250 exlat:250 rrt:10 rrl:10 - rwt:10 rwl:10 idle_power:- active_power:- -ps 11 : mp:14.00W operational enlat:265 exlat:265 rrt:11 rrl:11 - rwt:11 rwl:11 idle_power:- active_power:- -ps 12 : mp:13.00W operational enlat:280 exlat:280 rrt:12 rrl:12 - rwt:12 rwl:12 idle_power:- active_power:- -ps 13 : mp:12.00W operational enlat:295 exlat:295 rrt:13 rrl:13 - rwt:13 rwl:13 idle_power:- active_power:- -ps 14 : mp:11.00W operational enlat:310 exlat:310 rrt:14 rrl:14 - rwt:14 rwl:14 idle_power:- active_power:- -ps 15 : mp:10.00W operational enlat:325 exlat:325 rrt:15 rrl:15 - rwt:15 rwl:15 idle_power:- active_power:- +{ + "acl": 7, + "acwu": 0, + "aerl": 3, + "anacap": 0, + "anagrpmax": 0, + "anatt": 0, + "apsta": 1, + "avscc": 1, + "awun": 1023, + "awupf": 0, + "cctemp": 358, + "cmic": 0, + "cntlid": 6, + "cntrltype": 0, + "cqes": 68, + "crdt1": 0, + "crdt2": 0, + "crdt3": 0, + "ctratt": 16, + "domainid": 0, + "dsto": 0, + "edstt": 35, + "elpe": 63, + "endgidmax": 1, + "fcatt": 0, + "fguid": "00000000-0000-0000-0000-000000000000", + "fna": 4, + "fr": "3B2QGXA7", + "frmw": 22, + "fuses": 0, + "fwug": 0, + "hctma": 1, + "hmmaxd": 0, + "hmmin": 0, + "hmminds": 0, + "hmpre": 0, + "icdoff": 0, + "icsvscc": 1, + "ieee": 9528, + "ioccsz": 0, + "iorcsz": 0, + "kas": 0, + "lpa": 15, + "maxcmd": 256, + "maxcna": 0, + "maxdna": 0, + "mdts": 7, + "mec": 0, + "megcap": 0, + "mn": "Samsung SSD 980 PRO 1TB ", + "mnan": 0, + "mntmt": 318, + "msdbd": 0, + "mtfa": 0, + "mxtmt": 356, + "nanagrpid": 0, + "nn": 1, + "npss": 4, + "nsetidmax": 0, + "nvmsr": 0, + "nwpc": 0, + "oacs": 23, + "oaes": 512, + "oaqd": 0, + "ocfs": 0, + "ofcs": 0, + "oncs": 87, + "pels": 0, + "psds": [ + { + "active_power": 0, + "active_power_work": 0, + "active_scale": 0, + "entry_lat": 0, + "exit_lat": 0, + "idle_power": 0, + "idle_scale": 0, + "max_power": 849, + "max_power_scale": 0, + "non-operational_state": 0, + "read_lat": 0, + "read_tput": 0, + "write_lat": 0, + "write_tput": 0 + }, + { + "active_power": 0, + "active_power_work": 0, + "active_scale": 0, + "entry_lat": 0, + "exit_lat": 200, + "idle_power": 0, + "idle_scale": 0, + "max_power": 448, + "max_power_scale": 0, + "non-operational_state": 0, + "read_lat": 1, + "read_tput": 1, + "write_lat": 1, + "write_tput": 1 + }, + { + "active_power": 0, + "active_power_work": 0, + "active_scale": 0, + "entry_lat": 0, + "exit_lat": 1000, + "idle_power": 0, + "idle_scale": 0, + "max_power": 318, + "max_power_scale": 0, + "non-operational_state": 0, + "read_lat": 2, + "read_tput": 2, + "write_lat": 2, + "write_tput": 2 + }, + { + "active_power": 0, + "active_power_work": 0, + "active_scale": 0, + "entry_lat": 500, + "exit_lat": 9500, + "idle_power": 0, + "idle_scale": 0, + "max_power": 50, + "max_power_scale": 1, + "non-operational_state": 1, + "read_lat": 4, + "read_tput": 4, + "write_lat": 4, + "write_tput": 4 + }, + { + "active_power": 0, + "active_power_work": 0, + "active_scale": 0, + "entry_lat": 2000, + "exit_lat": 1200, + "idle_power": 0, + "idle_scale": 0, + "max_power": 400, + "max_power_scale": 1, + "non-operational_state": 1, + "read_lat": 3, + "read_tput": 3, + "write_lat": 3, + "write_tput": 3 + } + ], + "rab": 2, + "rpmbs": 0, + "rrls": 0, + "rtd3e": 10000000, + "rtd3r": 200000, + "sanicap": 0, + "sgls": 0, + "sn": "S5P2NG0R824326R ", + "sqes": 102, + "ssvid": 5197, + "subnqn": "nqn.1994-11.com.samsung:nvme:980PRO:M.2:S5P2NG0R824326R ", + "tnvmcap": 1000204886016, + "unvmcap": 0, + "ver": 66304, + "vid": 5197, + "vwc": 7, + "vwci": 0, + "wctemp": 355 +} diff --git a/utils/nvme.go b/utils/nvme.go index 38227493..536c484e 100644 --- a/utils/nvme.go +++ b/utils/nvme.go @@ -1,11 +1,10 @@ package utils import ( - "bufio" "context" "encoding/json" + "errors" "os" - "regexp" "strconv" "strings" @@ -15,6 +14,8 @@ import ( const EnvNvmeUtility = "IRONLIB_UTIL_NVME" +var errSanicapNODMMASReserved = errors.New("sanicap nodmmas reserved bits set, not sure what to do with them") + type Nvme struct { Executor Executor } @@ -86,7 +87,6 @@ func (n *Nvme) Drives(ctx context.Context) ([]*common.Drive, error) { if len(modelTokens) > 1 { vendor = modelTokens[1] } - drive := &common.Drive{ Common: common.Common{ Serial: d.SerialNumber, @@ -130,9 +130,8 @@ func (n *Nvme) list(ctx context.Context) ([]byte, error) { } func (n *Nvme) cmdListCapabilities(ctx context.Context, logicalPath string) ([]byte, error) { - // nvme id-ctrl -H devicepath - n.Executor.SetArgs([]string{"id-ctrl", "-H", logicalPath}) - + // nvme id-ctrl --output-format=json devicepath + n.Executor.SetArgs([]string{"id-ctrl", "--output-format=json", logicalPath}) result, err := n.Executor.ExecWithContext(ctx) if err != nil { return nil, err @@ -141,161 +140,113 @@ func (n *Nvme) cmdListCapabilities(ctx context.Context, logicalPath string) ([]b return result.Stdout, nil } -// DriveCapabilities returns the drive capability attributes obtained through hdparm +// DriveCapabilities returns the drive capability attributes obtained through nvme // // The logicalName is the kernel/OS assigned drive name - /dev/nvmeX // // This method implements the actions.DriveCapabilityCollector interface. -// -// nolint:gocyclo // line parsing is cyclomatic func (n *Nvme) DriveCapabilities(ctx context.Context, logicalName string) ([]*common.Capability, error) { out, err := n.cmdListCapabilities(ctx, logicalName) if err != nil { return nil, err } - var capabilitiesFound []*common.Capability - - var lines []string - - s := string(out) - - scanner := bufio.NewScanner(strings.NewReader(s)) - for scanner.Scan() { - lines = append(lines, scanner.Text()) + var caps struct { + FNA uint `json:"fna"` + SANICAP uint `json:"sanicap"` } - err = scanner.Err() + err = json.Unmarshal(out, &caps) if err != nil { return nil, err } - // Delimiters - reFnaStart := regexp.MustCompile(`(?s)^fna\s`) - reFnaEnd := regexp.MustCompile(`(?s)^vwc\s`) - reSaniStart := regexp.MustCompile(`(?s)^sanicap\s`) - reSaniEnd := regexp.MustCompile(`(?s)^hmminds\s`) - reBlank := regexp.MustCompile(`(?m)^\s*$`) - - var fnaBool, saniBool bool - - for _, line := range lines { - line = strings.TrimSpace(line) - fnaStart := reFnaStart.MatchString(line) - fnaEnd := reFnaEnd.MatchString(line) - saniStart := reSaniStart.MatchString(line) - saniEnd := reSaniEnd.MatchString(line) - isBlank := reBlank.MatchString(line) - - // start/end match specific block delimiters - // bools are toggled to indicate lines within a given block - switch { - case fnaStart: - fnaBool = true - case fnaEnd: - fnaBool = false - case saniStart: - saniBool = true - case saniEnd: - saniBool = false - } - - switch { - case (fnaStart || saniStart): - capability := new(common.Capability) - - partsLen := 2 - - parts := strings.Split(line, ":") - if len(parts) != partsLen { - continue - } - - key, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) - capability.Name = key - - if value != "0" { - capability.Enabled = true - } - - if fnaStart { - capability.Description = "Crypto Erase Support" - } else { - capability.Description = "Sanitize Support" - } - - if value != "0" { - capability.Enabled = true - } - - capabilitiesFound = append(capabilitiesFound, capability) - - // crypto erase - case (fnaBool && !fnaEnd && !isBlank): - capability := new(common.Capability) - - partsLen := 3 - - parts := strings.Split(line, ":") - if len(parts) != partsLen { - continue - } - - data := strings.Split(parts[2], "\t") - enabled := strings.TrimSpace(data[0]) - - if enabled != "0" { - capability.Enabled = true - } - - // Generate short flag identifier - for _, word := range strings.Fields(data[1]) { - capability.Name += strings.ToLower(word[0:1]) - } - - capability.Description = data[1] - - if enabled != "0" { - capability.Enabled = true - } - - capability.Description = data[1] - capabilitiesFound = append(capabilitiesFound, capability) - // sanitize - case (saniBool && !saniEnd && !isBlank): - capability := new(common.Capability) - - partsLen := 3 - - parts := strings.Split(line, ":") - if len(parts) != partsLen { - continue - } - - data := strings.Split(parts[2], "\t") - enabled := strings.TrimSpace(data[0]) + var capabilitiesFound []*common.Capability + capabilitiesFound = append(capabilitiesFound, parseFna(caps.FNA)...) - if enabled != "0" { - capability.Enabled = true - } + var parsedCaps []*common.Capability + parsedCaps, err = parseSanicap(caps.SANICAP) + if err != nil { + return nil, err + } + capabilitiesFound = append(capabilitiesFound, parsedCaps...) - // Generate short flag identifier - for _, word := range strings.Fields(data[1]) { - capability.Name += strings.ToLower(word[0:1]) - } + return capabilitiesFound, nil +} - capability.Description = data[1] +func parseFna(fna uint) []*common.Capability { + // Bit masks values came from nvme-cli repo + // All names come from internal nvme-cli names + // We will *not* keep in sync as these names form our API + // https: // github.com/linux-nvme/nvme-cli/blob/v2.8/nvme-print-stdout.c#L2199-L2217 + + return []*common.Capability{ + { + Name: "fmns", + Description: "Format Applies to All/Single Namespace(s) (t:All, f:Single)", + Enabled: (fna&(0b1<<0))>>0 != 0, + }, + { + Name: "cens", + Description: "Crypto Erase Applies to All/Single Namespace(s) (t:All, f:Single)", + Enabled: (fna&(0b1<<1))>>1 != 0, + }, + { + Name: "cese", + Description: "Crypto Erase Supported as part of Secure Erase", + Enabled: (fna&(0b1<<2))>>2 != 0, + }, + } +} - if enabled != "0" { - capability.Enabled = true - } +func parseSanicap(sanicap uint) ([]*common.Capability, error) { + // Bit masks values came from nvme-cli repo + // All names come from internal nvme-cli names + // We will *not* keep in sync as these names form our API + // https://github.com/linux-nvme/nvme-cli/blob/v2.8/nvme-print-stdout.c#L2064-L2093 + + caps := []*common.Capability{ + { + Name: "cer", + Description: "Crypto Erase Sanitize Operation Supported", + Enabled: (sanicap&(0b1<<0))>>0 != 0, + }, + { + Name: "ber", + Description: "Block Erase Sanitize Operation Supported", + Enabled: (sanicap&(0b1<<1))>>1 != 0, + }, + { + Name: "owr", + Description: "Overwrite Sanitize Operation Supported", + Enabled: (sanicap&(0b1<<2))>>2 != 0, + }, + { + Name: "ndi", + Description: "No-Deallocate After Sanitize bit in Sanitize command Supported", + Enabled: (sanicap&(0b1<<29))>>29 != 0, + }, + } - capability.Description = data[1] - capabilitiesFound = append(capabilitiesFound, capability) - } + switch (sanicap & (0b11 << 30)) >> 30 { + case 0b00: + // nvme prints this out for 0b00: + // "Additional media modification after sanitize operation completes successfully is not defined" + // So I'm taking "not defined" literally since we can't really represent 2 bits in a bool + // If we ever want this as a bool we could maybe call it "dmmas" maybe? + case 0b01, 0b10: + caps = append(caps, &common.Capability{ + Name: "nodmmas", + Description: "Media is additionally modified after sanitize operation completes successfully", + Enabled: (sanicap&(0b11<<30))>>30 == 0b10, + }) + case 0b11: + return nil, errSanicapNODMMASReserved + default: + panic("unreachable") } - return capabilitiesFound, err + return caps, nil } // NewFakeNvme returns a mock nvme collector that returns mock data for use in tests. diff --git a/utils/nvme_test.go b/utils/nvme_test.go index a69e295c..0382de28 100644 --- a/utils/nvme_test.go +++ b/utils/nvme_test.go @@ -2,10 +2,12 @@ package utils import ( "context" + "strconv" "testing" "github.com/bmc-toolbox/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_NvmeComponents(t *testing.T) { @@ -13,31 +15,25 @@ func Test_NvmeComponents(t *testing.T) { {Common: common.Common{ Serial: "Z9DF70I8FY3L", Vendor: "TOSHIBA", Model: "KXG60ZNV256G TOSHIBA", Description: "KXG60ZNV256G TOSHIBA", Firmware: &common.Firmware{Installed: "AGGA4104"}, ProductName: "NULL", Metadata: map[string]string{ - "Additional media modification after sanitize operation completes successfully is not defined": "false", - "Block Erase Sanitize Operation Not Supported": "false", - "Crypto Erase Applies to Single Namespace(s)": "false", - "Crypto Erase Sanitize Operation Not Supported": "false", - "Crypto Erase Support": "true", - "Crypto Erase Supported as part of Secure Erase": "true", - "Format Applies to Single Namespace(s)": "false", - "No-Deallocate After Sanitize bit in Sanitize command Supported": "false", - "Overwrite Sanitize Operation Not Supported": "false", - "Sanitize Support": "false", + "Block Erase Sanitize Operation Supported": "false", + "Crypto Erase Applies to All/Single Namespace(s) (t:All, f:Single)": "false", + "Crypto Erase Sanitize Operation Supported": "false", + "Crypto Erase Supported as part of Secure Erase": "true", + "Format Applies to All/Single Namespace(s) (t:All, f:Single)": "false", + "No-Deallocate After Sanitize bit in Sanitize command Supported": "false", + "Overwrite Sanitize Operation Supported": "false", }, }}, {Common: common.Common{ Serial: "Z9DF70I9FY3L", Vendor: "TOSHIBA", Model: "KXG60ZNV256G TOSHIBA", Description: "KXG60ZNV256G TOSHIBA", Firmware: &common.Firmware{Installed: "AGGA4104"}, ProductName: "NULL", Metadata: map[string]string{ - "Additional media modification after sanitize operation completes successfully is not defined": "false", - "Block Erase Sanitize Operation Not Supported": "false", - "Crypto Erase Applies to Single Namespace(s)": "false", - "Crypto Erase Sanitize Operation Not Supported": "false", - "Crypto Erase Support": "true", - "Crypto Erase Supported as part of Secure Erase": "true", - "Format Applies to Single Namespace(s)": "false", - "No-Deallocate After Sanitize bit in Sanitize command Supported": "false", - "Overwrite Sanitize Operation Not Supported": "false", - "Sanitize Support": "false", + "Block Erase Sanitize Operation Supported": "false", + "Crypto Erase Applies to All/Single Namespace(s) (t:All, f:Single)": "false", + "Crypto Erase Sanitize Operation Supported": "false", + "Crypto Erase Supported as part of Secure Erase": "true", + "Format Applies to All/Single Namespace(s) (t:All, f:Single)": "false", + "No-Deallocate After Sanitize bit in Sanitize command Supported": "false", + "Overwrite Sanitize Operation Supported": "false", }, }}, } @@ -66,54 +62,98 @@ func Test_NvmeDriveCapabilities(t *testing.T) { } var fixtureNvmeDeviceCapabilities = []*common.Capability{ - { - Name: "sanicap", - Description: "Sanitize Support", - Enabled: false, - }, - { - Name: "ammasocsind", - Description: "Additional media modification after sanitize operation completes successfully is not defined", - Enabled: false, - }, - { - Name: "nasbiscs", - Description: "No-Deallocate After Sanitize bit in Sanitize command Supported", - Enabled: false, - }, - { - Name: "osons", - Description: "Overwrite Sanitize Operation Not Supported", - Enabled: false, - }, - { - Name: "besons", - Description: "Block Erase Sanitize Operation Not Supported", - Enabled: false, - }, - { - Name: "cesons", - Description: "Crypto Erase Sanitize Operation Not Supported", - Enabled: false, - }, - { - Name: "fna", - Description: "Crypto Erase Support", - Enabled: true, - }, - { - Name: "cesapose", - Description: "Crypto Erase Supported as part of Secure Erase", - Enabled: true, - }, - { - Name: "ceatsn", - Description: "Crypto Erase Applies to Single Namespace(s)", - Enabled: false, - }, - { - Name: "fatsn", - Description: "Format Applies to Single Namespace(s)", - Enabled: false, - }, + {Name: "fmns", Description: "Format Applies to All/Single Namespace(s) (t:All, f:Single)", Enabled: false}, + {Name: "cens", Description: "Crypto Erase Applies to All/Single Namespace(s) (t:All, f:Single)", Enabled: false}, + {Name: "cese", Description: "Crypto Erase Supported as part of Secure Erase", Enabled: true}, + {Name: "cer", Description: "Crypto Erase Sanitize Operation Supported", Enabled: false}, + {Name: "ber", Description: "Block Erase Sanitize Operation Supported", Enabled: false}, + {Name: "owr", Description: "Overwrite Sanitize Operation Supported", Enabled: false}, + {Name: "ndi", Description: "No-Deallocate After Sanitize bit in Sanitize command Supported", Enabled: false}, +} + +func Test_NvmeParseFna(t *testing.T) { + // These are laid out so if you squint and pretend false/true are 0/1 they match the bit pattern of the int + // Its a map so order doesn't matter but I think it makes it easier to match a broken test to the code + wants := []map[string]bool{ + {"cese": false, "cens": false, "fmns": false}, + {"cese": false, "cens": false, "fmns": true}, + {"cese": false, "cens": true, "fmns": false}, + {"cese": false, "cens": true, "fmns": true}, + {"cese": true, "cens": false, "fmns": false}, + {"cese": true, "cens": false, "fmns": true}, + {"cese": true, "cens": true, "fmns": false}, + {"cese": true, "cens": true, "fmns": true}, + } + for i, want := range wants { + t.Run(strconv.Itoa(i), func(t *testing.T) { + caps := parseFna(uint(i)) + require.Len(t, caps, len(want)) + for _, cap := range caps { + require.Equal(t, want[cap.Name], cap.Enabled) + } + }) + } +} + +func Test_NvmeParseSanicap(t *testing.T) { + // These are laid out so if you squint and pretend false/true are 0/1 they match the bit pattern of the int + // Its a map so order doesn't matter but I think it makes it easier to match a broken test to the code + + // lower bits only + wants := []map[string]bool{ + {"owr": false, "ber": false, "cer": false}, + {"owr": false, "ber": false, "cer": true}, + {"owr": false, "ber": true, "cer": false}, + {"owr": false, "ber": true, "cer": true}, + {"owr": true, "ber": false, "cer": false}, + {"owr": true, "ber": false, "cer": true}, + {"owr": true, "ber": true, "cer": false}, + {"owr": true, "ber": true, "cer": true}, + } + for i, want := range wants { + // not testing ndi yet but its being returned + // don't want to add it above to avoid noise + want["ndi"] = false + t.Run(strconv.Itoa(i), func(t *testing.T) { + caps, err := parseSanicap(uint(i)) + require.NoError(t, err) + require.Len(t, caps, len(want)) + for _, cap := range caps { + require.Equal(t, want[cap.Name], cap.Enabled) + } + }) + } + + // higher bits only + wants = []map[string]bool{ + {"ndi": false}, + {"ndi": true}, + {"nodmmas": false, "ndi": false}, + {"nodmmas": false, "ndi": true}, + {"nodmmas": true, "ndi": false}, + {"nodmmas": true, "ndi": true}, + } + for i, want := range wants { + // not testing these now but they are being returned + // don't want to add them above to avoid noise + want["owr"] = false + want["ber"] = false + want["cer"] = false + i = (i << 29) + t.Run(strconv.Itoa(i), func(t *testing.T) { + caps, err := parseSanicap(uint(i)) + require.NoError(t, err) + require.Len(t, caps, len(want)) + for _, cap := range caps { + require.Equal(t, want[cap.Name], cap.Enabled) + } + }) + } + + i := 0b11 << 30 + t.Run(strconv.Itoa(i), func(t *testing.T) { + caps, err := parseSanicap(uint(i)) + require.Error(t, err) + require.Nil(t, caps) + }) }