Merge ~ltrager/maas:lp1835954_2.8 into maas:2.8

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: ddf53cddd6079dd0b1fb4a128cb134f8c4f88d40
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1835954_2.8
Merge into: maas:2.8
Diff against target: 1741 lines (+1118/-242)
4 files modified
src/maasserver/compose_preseed.py (+3/-0)
src/metadataserver/user_data/templates/snippets/maas_wipe.py (+289/-63)
src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py (+594/-179)
src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe_defs.py (+232/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Lee Trager (community) Approve
Review via email: mp+387870@code.launchpad.net

Commit message

This is the NVMe secure erase implementation (+ one more patch, to fix/improve quick erase).
I tried to explain/detail everything in the commit messages; also, code/tests were validated against Flake8 to prevent style issues.

Backport of 9633810

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :
review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b lp1835954_2.8 lp:~ltrager/maas/+git/maas into -b 2.8 lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: ddf53cddd6079dd0b1fb4a128cb134f8c4f88d40

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/compose_preseed.py b/src/maasserver/compose_preseed.py
2index 8acf2f5..4baa65a 100644
3--- a/src/maasserver/compose_preseed.py
4+++ b/src/maasserver/compose_preseed.py
5@@ -391,6 +391,9 @@ def get_base_preseed(node=None):
6 # jq is used during enlistment to read the JSON string containing
7 # the system_id of the newly created machine.
8 cloud_config["packages"] += ["jq"]
9+ # On disk erasing, we need nvme-cli
10+ if node is not None and node.status == NODE_STATUS.DISK_ERASING:
11+ cloud_config["packages"] += ["nvme-cli"]
12
13 return cloud_config
14
15diff --git a/src/metadataserver/user_data/templates/snippets/maas_wipe.py b/src/metadataserver/user_data/templates/snippets/maas_wipe.py
16old mode 100644
17new mode 100755
18index 317c62b..f7e611a
19--- a/src/metadataserver/user_data/templates/snippets/maas_wipe.py
20+++ b/src/metadataserver/user_data/templates/snippets/maas_wipe.py
21@@ -29,12 +29,95 @@ def list_disks():
22 return disks
23
24
25-def get_disk_security_info(disk):
26- """Get the disk security information.
27+def get_nvme_security_info(disk):
28+ """Gather NVMe information from the NVMe disks using the
29+ nvme-cli tool. Info from id-ctrl and id-ns is needed for
30+ secure erase (nvme format) and write zeroes."""
31+
32+ # Grab the relevant info from nvme id-ctrl. We need to check the
33+ # following bits:
34+ #
35+ # OACS (Optional Admin Command Support) bit 1: Format supported
36+ # ONCS (Optional NVM Command Support) bit 3: Write Zeroes supported
37+ # FNA (Format NVM Attributes) bit 2: Cryptographic format supported
38+
39+ security_info = {
40+ "format_supported": False,
41+ "writez_supported": False,
42+ "crypto_format": False,
43+ "nsze": 0,
44+ "lbaf": 0,
45+ "ms": 0,
46+ }
47+
48+ try:
49+ output = subprocess.check_output(["nvme", "id-ctrl", DEV_PATH % disk])
50+ except subprocess.CalledProcessError as exc:
51+ print_flush("Error on nvme id-ctrl (%s)" % exc.returncode)
52+ return security_info
53+ except OSError as exc:
54+ print_flush("OS error when running nvme-cli (%s)" % exc.strerror)
55+ return security_info
56+
57+ output = output.decode()
58+
59+ for line in output.split("\n"):
60+ if "oacs" in line:
61+ oacs = line.split(":")[1]
62+ if int(oacs, 16) & 0x2:
63+ security_info["format_supported"] = True
64+
65+ if "oncs" in line:
66+ oncs = line.split(":")[1]
67+ if int(oncs, 16) & 0x8:
68+ security_info["writez_supported"] = True
69+
70+ if "fna" in line:
71+ fna = line.split(":")[1]
72+ if int(fna, 16) & 0x4:
73+ security_info["crypto_format"] = True
74+
75+ # Next step: collect LBAF (LBA Format), MS (Metadata Setting) and
76+ # NSZE (Namespace Size) from id-ns. According to NVMe spec, bits 0:3
77+ # from FLBAS corresponds to the LBAF value, whereas bit 4 is MS.
78+
79+ try:
80+ output = subprocess.check_output(["nvme", "id-ns", DEV_PATH % disk])
81+ except subprocess.CalledProcessError as exc:
82+ print_flush("Error on nvme id-ns (%s)" % exc.returncode)
83+ security_info["format_supported"] = False
84+ security_info["writez_supported"] = False
85+ return security_info
86+ except OSError as exc:
87+ print_flush("OS error when running nvme-cli (%s)" % exc.strerror)
88+ security_info["format_supported"] = False
89+ security_info["writez_supported"] = False
90+ return security_info
91+
92+ output = output.decode()
93+
94+ for line in output.split("\n"):
95+ if "nsze" in line:
96+ # According to spec., this should be used as 0-based value.
97+ nsze = line.split(":")[1]
98+ security_info["nsze"] = int(nsze, 16) - 1
99+
100+ if "flbas" in line:
101+ flbas = line.split(":")[1]
102+ flbas = int(flbas, 16)
103+ security_info["lbaf"] = flbas & 0xF
104+
105+ if flbas & 0x10:
106+ security_info["ms"] = 1
107+
108+ return security_info
109
110- Uses `hdparam` to get security information about the disk. Sadly hdparam
111- doesn't provide an output that makes it easy to parse.
112+
113+def get_hdparm_security_info(disk):
114+ """Get SCSI/ATA disk security info from hdparm.
115+ Sadly hdparam doesn't provide an output that makes it easy to parse.
116 """
117+
118 # Grab the security section for hdparam.
119 security_section = []
120 output = subprocess.check_output([b"hdparm", b"-I", DEV_PATH % disk])
121@@ -62,57 +145,25 @@ def get_disk_security_info(disk):
122 return security_info
123
124
125+def get_disk_security_info(disk):
126+ """Get the disk security information.
127+
128+ Uses `hdparam` to get security information about the SCSI/ATA disks.
129+ If NVMe, nvme-cli is used instead.
130+ """
131+
132+ if b"nvme" in disk:
133+ return get_nvme_security_info(disk)
134+
135+ return get_hdparm_security_info(disk)
136+
137+
138 def get_disk_info():
139 """Return dictionary of wipeable disks and thier security information."""
140 return {kname: get_disk_security_info(kname) for kname in list_disks()}
141
142
143-def try_secure_erase(kname, info):
144- """Try to wipe the disk with secure erase."""
145- if info[b"supported"]:
146- if info[b"frozen"]:
147- print_flush(
148- "%s: not using secure erase; "
149- "drive is currently frozen." % kname.decode("ascii")
150- )
151- return False
152- elif info[b"locked"]:
153- print_flush(
154- "%s: not using secure erase; "
155- "drive is currently locked." % kname.decode("ascii")
156- )
157- return False
158- elif info[b"enabled"]:
159- print_flush(
160- "%s: not using secure erase; "
161- "drive security is already enabled." % kname.decode("ascii")
162- )
163- return False
164- else:
165- # Wiping using secure erase.
166- try:
167- secure_erase(kname)
168- except Exception as e:
169- print_flush(
170- "%s: failed to be securely erased: %s"
171- % (kname.decode("ascii"), e)
172- )
173- return False
174- else:
175- print_flush(
176- "%s: successfully securely erased."
177- % (kname.decode("ascii"))
178- )
179- return True
180- else:
181- print_flush(
182- "%s: drive does not support secure erase."
183- % (kname.decode("ascii"))
184- )
185- return False
186-
187-
188-def secure_erase(kname):
189+def secure_erase_hdparm(kname):
190 """Securely wipe the device."""
191 # First write 1 MiB of known data to the beginning of the block device.
192 # This is used to check at the end of the secure erase that it worked
193@@ -142,7 +193,7 @@ def secure_erase(kname):
194
195 # Now that the user password is set the device should have its
196 # security mode enabled.
197- info = get_disk_security_info(kname)
198+ info = get_hdparm_security_info(kname)
199 if not info[b"enabled"]:
200 # If not enabled that means the password did not take, so it does not
201 # need to be cleared.
202@@ -167,7 +218,7 @@ def secure_erase(kname):
203 failed_exc = exc
204
205 # Make sure that the device is now not enabled.
206- info = get_disk_security_info(kname)
207+ info = get_hdparm_security_info(kname)
208 if info[b"enabled"]:
209 # Wipe failed since security is still enabled.
210 subprocess.check_output(
211@@ -184,23 +235,197 @@ def secure_erase(kname):
212 )
213
214
215-def wipe_quickly(kname):
216- """Quickly wipe the disk by zeroing the beginning and end of the disk.
217+def try_secure_erase_hdparm(kname, info):
218+ """Try to wipe the disk with secure erase."""
219+ if info[b"supported"]:
220+ if info[b"frozen"]:
221+ print_flush(
222+ "%s: not using secure erase; "
223+ "drive is currently frozen." % kname.decode("ascii")
224+ )
225+ return False
226+ elif info[b"locked"]:
227+ print_flush(
228+ "%s: not using secure erase; "
229+ "drive is currently locked." % kname.decode("ascii")
230+ )
231+ return False
232+ elif info[b"enabled"]:
233+ print_flush(
234+ "%s: not using secure erase; "
235+ "drive security is already enabled." % kname.decode("ascii")
236+ )
237+ return False
238+ else:
239+ # Wiping using secure erase.
240+ try:
241+ secure_erase_hdparm(kname)
242+ except Exception as e:
243+ print_flush(
244+ "%s: failed to be securely erased: %s"
245+ % (kname.decode("ascii"), e)
246+ )
247+ return False
248+ else:
249+ print_flush(
250+ "%s: successfully securely erased."
251+ % (kname.decode("ascii"))
252+ )
253+ return True
254+ else:
255+ print_flush(
256+ "%s: drive does not support secure erase."
257+ % (kname.decode("ascii"))
258+ )
259+ return False
260+
261+
262+def try_secure_erase_nvme(kname, info):
263+ """Perform a secure-erase on NVMe disk if that feature is
264+ available. Prefer cryptographic erase, when available."""
265+
266+ if not info["format_supported"]:
267+ print_flush(
268+ "Device %s does not support formatting" % kname.decode("ascii")
269+ )
270+ return False
271+
272+ if info["crypto_format"]:
273+ ses = 2
274+ else:
275+ ses = 1
276+
277+ try:
278+ subprocess.check_output(
279+ [
280+ "nvme",
281+ "format",
282+ "-s",
283+ str(ses),
284+ "-l",
285+ str(info["lbaf"]),
286+ "-m",
287+ str(info["ms"]),
288+ DEV_PATH % kname,
289+ ]
290+ )
291+ except subprocess.CalledProcessError as exc:
292+ print_flush("Error with format command (%s)" % exc.returncode)
293+ return False
294+ except OSError as exc:
295+ print_flush("OS error when running nvme-cli (%s)" % exc.strerror)
296+ return False
297+
298+ print_flush(
299+ "Secure erase was successful on NVMe drive %s" % kname.decode("ascii")
300+ )
301+ return True
302+
303+
304+def try_secure_erase(kname, info):
305+ """Entry-point for secure-erase for SCSI/ATA or NVMe disks."""
306+
307+ if b"nvme" in kname:
308+ return try_secure_erase_nvme(kname, info)
309+
310+ return try_secure_erase_hdparm(kname, info)
311
312- This is not a secure erase but does make it harder to get the data from
313- the device.
314+
315+def wipe_quickly(kname):
316+ """Quickly wipe the disk by using wipefs and zeroing the beginning
317+ and end of the disk. This is not a secure erase but does make it
318+ harder to get the data from the device and also clears previous layouts.
319 """
320+
321+ wipe_error = 0
322 print_flush("%s: starting quick wipe." % kname.decode("ascii"))
323+ try:
324+ subprocess.check_output(["wipefs", "-f", "-a", DEV_PATH % kname])
325+ wipe_error -= 1
326+ except subprocess.CalledProcessError as exc:
327+ print_flush(
328+ "%s: wipefs failed (%s)" % (kname.decode("ascii"), exc.returncode)
329+ )
330+ wipe_error += 1
331+
332 buf = b"\0" * 1024 * 1024 * 2 # 2 MiB
333- with open(DEV_PATH % kname, "wb") as fp:
334+ try:
335+ fp = open(DEV_PATH % kname, "wb")
336 fp.write(buf)
337 fp.seek(-len(buf), 2)
338 fp.write(buf)
339- print_flush("%s: successfully quickly wiped." % kname.decode("ascii"))
340+ wipe_error -= 1
341+ except OSError as exc:
342+ print_flush(
343+ "%s: OS error while wiping beginning/end of disk (%s)"
344+ % (kname.decode("ascii"), exc.strerror)
345+ )
346+ wipe_error += 1
347+
348+ if wipe_error > 0:
349+ print_flush("%s: failed to be quickly wiped." % kname.decode("ascii"))
350+ else:
351+ print_flush("%s: successfully quickly wiped." % kname.decode("ascii"))
352+
353+
354+def nvme_write_zeroes(kname, info):
355+ """Perform a write-zeroes operation on NVMe device instead of
356+ dd'ing 0 to the entire disk if secure erase is not available.
357+ Write-zeroes is a faster way to clean a NVMe disk."""
358+
359+ fallback = False
360+
361+ if not info["writez_supported"]:
362+ print(
363+ "NVMe drive %s does not support write-zeroes"
364+ % kname.decode("ascii")
365+ )
366+ fallback = True
367+
368+ if info["nsze"] <= 0:
369+ print(
370+ "Bad namespace information collected on NVMe drive %s"
371+ % kname.decode("ascii")
372+ )
373+ fallback = True
374+
375+ if fallback:
376+ print_flush("Will fallback to regular drive zeroing.")
377+ return False
378+
379+ try:
380+ subprocess.check_output(
381+ [
382+ "nvme",
383+ "write-zeroes",
384+ "-f",
385+ "-s",
386+ "0",
387+ "-c",
388+ str(hex(info["nsze"])[2:]),
389+ DEV_PATH % kname,
390+ ]
391+ )
392+ except subprocess.CalledProcessError as exc:
393+ print_flush("Error with write-zeroes command (%s)" % exc.returncode)
394+ return False
395+ except OSError as exc:
396+ print_flush("OS error when running nvme-cli (%s)" % exc.strerror)
397+ return False
398+
399+ print_flush(
400+ "%s: successfully zeroed (using write-zeroes)." % kname.decode("ascii")
401+ )
402+ return True
403+
404+
405+def zero_disk(kname, info):
406+ """Zero the entire disk, trying write-zeroes first if NVMe disk."""
407
408+ if b"nvme" in kname:
409+ if nvme_write_zeroes(kname, info):
410+ return
411
412-def zero_disk(kname):
413- """Zero the entire disk."""
414 # Get the total size of the device.
415 size = 0
416 with open(DEV_PATH % kname, "rb") as fp:
417@@ -266,8 +491,9 @@ def main():
418 action="store_true",
419 default=False,
420 help=(
421- "Wipe 1MiB at the start and at the end of the drive to make data "
422- "recovery inconvenient and unlikely to happen by accident. This "
423+ "Wipe 2MiB at the start and at the end of the drive to make data "
424+ "recovery inconvenient and unlikely to happen by accident. Also, "
425+ "it runs wipefs to clear known partition/layout signatures. This "
426 "is not secure."
427 ),
428 )
429@@ -288,7 +514,7 @@ def main():
430 if args.quick_erase:
431 wipe_quickly(kname)
432 else:
433- zero_disk(kname)
434+ zero_disk(kname, info)
435
436 print_flush("All disks have been successfully wiped.")
437
438diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py
439index 995c0b5..191cd02 100644
440--- a/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py
441+++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py
442@@ -6,6 +6,7 @@
443 __all__ = []
444
445 import argparse
446+import builtins
447 import subprocess
448 from textwrap import dedent
449 from unittest.mock import call, MagicMock
450@@ -22,148 +23,29 @@ from snippets.maas_wipe import (
451 get_disk_info,
452 get_disk_security_info,
453 list_disks,
454- secure_erase,
455+ nvme_write_zeroes,
456+ secure_erase_hdparm,
457 try_secure_erase,
458 wipe_quickly,
459 WipeError,
460 zero_disk,
461 )
462-
463-HDPARM_BEFORE_SECURITY = b"""\
464-/dev/sda:
465-
466-ATA device, with non-removable media
467- Model Number: INTEL SSDSC2CT240A4
468- Serial Number: CVKI3206029X240DGN
469- Firmware Revision: 335u
470- Transport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions
471-Standards:
472- Used: unknown (minor revision code 0xffff)
473- Supported: 9 8 7 6 5
474- Likely used: 9
475-Configuration:
476- Logical max current
477- cylinders 16383 16383
478- heads 16 16
479- sectors/track 63 63
480- --
481- CHS current addressable sectors: 16514064
482- LBA user addressable sectors: 268435455
483- LBA48 user addressable sectors: 468862128
484- Logical Sector size: 512 bytes
485- Physical Sector size: 512 bytes
486- Logical Sector-0 offset: 0 bytes
487- device size with M = 1024*1024: 228936 MBytes
488- device size with M = 1000*1000: 240057 MBytes (240 GB)
489- cache/buffer size = unknown
490- Nominal Media Rotation Rate: Solid State Device
491-Capabilities:
492- LBA, IORDY(can be disabled)
493- Queue depth: 32
494- Standby timer values: spec'd by Standard, no device specific minimum
495- R/W multiple sector transfer: Max = 16 Current = 16
496- Advanced power management level: 254
497- DMA: mdma0 mdma1 mdma2 udma0 udma1 udma2 udma3 udma4 udma5 *udma6
498- Cycle time: min=120ns recommended=120ns
499- PIO: pio0 pio1 pio2 pio3 pio4
500- Cycle time: no flow control=120ns IORDY flow control=120ns
501-Commands/features:
502- Enabled Supported:
503- * SMART feature set
504- Security Mode feature set
505- * Power Management feature set
506- * Write cache
507- * Look-ahead
508- * Host Protected Area feature set
509- * WRITE_BUFFER command
510- * READ_BUFFER command
511- * NOP cmd
512- * DOWNLOAD_MICROCODE
513- * Advanced Power Management feature set
514- Power-Up In Standby feature set
515- * 48-bit Address feature set
516- * Mandatory FLUSH_CACHE
517- * FLUSH_CACHE_EXT
518- * SMART error logging
519- * SMART self-test
520- * General Purpose Logging feature set
521- * WRITE_{DMA|MULTIPLE}_FUA_EXT
522- * 64-bit World wide name
523- * IDLE_IMMEDIATE with UNLOAD
524- * WRITE_UNCORRECTABLE_EXT command
525- * {READ,WRITE}_DMA_EXT_GPL commands
526- * Segmented DOWNLOAD_MICROCODE
527- * Gen1 signaling speed (1.5Gb/s)
528- * Gen2 signaling speed (3.0Gb/s)
529- * Gen3 signaling speed (6.0Gb/s)
530- * Native Command Queueing (NCQ)
531- * Host-initiated interface power management
532- * Phy event counters
533- * DMA Setup Auto-Activate optimization
534- Device-initiated interface power management
535- * Software settings preservation
536- * SMART Command Transport (SCT) feature set
537- * SCT Data Tables (AC5)
538- * reserved 69[4]
539- * Data Set Management TRIM supported (limit 1 block)
540- * Deterministic read data after TRIM
541-"""
542-
543-HDPARM_AFTER_SECURITY = b"""\
544-Logical Unit WWN Device Identifier: 55cd2e40002643cf
545- NAA : 5
546- IEEE OUI : 5cd2e4
547- Unique ID : 0002643cf
548-Checksum: correct
549-"""
550-
551-HDPARM_SECURITY_NOT_SUPPORTED = b"""\
552-Security:
553- Master password revision code = 65534
554- not supported
555- not enabled
556- not locked
557- not frozen
558- not expired: security count
559- supported: enhanced erase
560- 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
561-"""
562-
563-HDPARM_SECURITY_SUPPORTED_NOT_ENABLED = b"""\
564-Security:
565- Master password revision code = 65534
566- supported
567- not enabled
568- not locked
569- not frozen
570- not expired: security count
571- supported: enhanced erase
572- 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
573-"""
574-
575-HDPARM_SECURITY_SUPPORTED_ENABLED = b"""\
576-Security:
577- Master password revision code = 65534
578- supported
579- enabled
580- not locked
581- not frozen
582- not expired: security count
583- supported: enhanced erase
584- 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
585-"""
586-
587-HDPARM_SECURITY_ALL_TRUE = b"""\
588-Security:
589- Master password revision code = 65534
590- supported
591- enabled
592- locked
593- frozen
594- not expired: security count
595- supported: enhanced erase
596- 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
597-"""
598+from snippets.tests.test_maas_wipe_defs import (
599+ HDPARM_AFTER_SECURITY,
600+ HDPARM_BEFORE_SECURITY,
601+ HDPARM_SECURITY_ALL_TRUE,
602+ HDPARM_SECURITY_NOT_SUPPORTED,
603+ HDPARM_SECURITY_SUPPORTED_ENABLED,
604+ HDPARM_SECURITY_SUPPORTED_NOT_ENABLED,
605+ NVME_IDCTRL_EPILOGUE,
606+ NVME_IDCTRL_FNA_CRYPTFORMAT_SUPPORTED,
607+ NVME_IDCTRL_FNA_CRYPTFORMAT_UNSUPPORTED,
608+ NVME_IDCTRL_OACS_FORMAT_SUPPORTED,
609+ NVME_IDCTRL_OACS_FORMAT_UNSUPPORTED,
610+ NVME_IDCTRL_ONCS_WRITEZ_SUPPORTED,
611+ NVME_IDCTRL_ONCS_WRITEZ_UNSUPPORTED,
612+ NVME_IDCTRL_PROLOGUE,
613+)
614
615
616 class TestMAASWipe(MAASTestCase):
617@@ -196,11 +78,13 @@ class TestMAASWipe(MAASTestCase):
618 sdb disk 1
619 sr0 rom 0
620 sr1 rom 0
621+ nvme0n1 disk 0
622+ nvme1n1 disk 1
623 """
624 ).encode("ascii")
625- self.assertEqual([b"sda"], list_disks())
626+ self.assertEqual([b"sda", b"nvme0n1"], list_disks())
627
628- def test_get_disk_security_info_missing(self):
629+ def test_get_disk_security_info_missing_hdparm(self):
630 hdparm_output = HDPARM_BEFORE_SECURITY + HDPARM_AFTER_SECURITY
631 mock_check_output = self.patch(subprocess, "check_output")
632 mock_check_output.return_value = hdparm_output
633@@ -220,7 +104,7 @@ class TestMAASWipe(MAASTestCase):
634 observered,
635 )
636
637- def test_get_disk_security_info_not_supported(self):
638+ def test_get_disk_security_info_not_supported_hdparm(self):
639 hdparm_output = (
640 HDPARM_BEFORE_SECURITY
641 + HDPARM_SECURITY_NOT_SUPPORTED
642@@ -244,7 +128,7 @@ class TestMAASWipe(MAASTestCase):
643 observered,
644 )
645
646- def test_get_disk_security_info_supported_not_enabled(self):
647+ def test_get_disk_security_info_supported_not_enabled_hdparm(self):
648 hdparm_output = (
649 HDPARM_BEFORE_SECURITY
650 + HDPARM_SECURITY_SUPPORTED_NOT_ENABLED
651@@ -268,7 +152,7 @@ class TestMAASWipe(MAASTestCase):
652 observered,
653 )
654
655- def test_get_disk_security_info_supported_enabled(self):
656+ def test_get_disk_security_info_supported_enabled_hdparm(self):
657 hdparm_output = (
658 HDPARM_BEFORE_SECURITY
659 + HDPARM_SECURITY_SUPPORTED_ENABLED
660@@ -292,7 +176,7 @@ class TestMAASWipe(MAASTestCase):
661 observered,
662 )
663
664- def test_get_disk_security_info_all_true(self):
665+ def test_get_disk_security_info_all_true_hdparm(self):
666 hdparm_output = (
667 HDPARM_BEFORE_SECURITY
668 + HDPARM_SECURITY_ALL_TRUE
669@@ -316,7 +200,7 @@ class TestMAASWipe(MAASTestCase):
670 observered,
671 )
672
673- def test_get_disk_info(self):
674+ def test_get_disk_info_hdparm(self):
675 disk_names = [
676 factory.make_name("disk").encode("ascii") for _ in range(3)
677 ]
678@@ -331,14 +215,243 @@ class TestMAASWipe(MAASTestCase):
679 for _ in range(3)
680 ]
681 self.patch(
682- maas_wipe, "get_disk_security_info"
683+ maas_wipe, "get_hdparm_security_info"
684+ ).side_effect = security_info
685+ observed = get_disk_info()
686+ self.assertEqual(
687+ {disk_names[i]: security_info[i] for i in range(3)}, observed
688+ )
689+
690+ def test_get_disk_security_info_crypt_format_writez_nvme(self):
691+ nvme_cli_output = (
692+ NVME_IDCTRL_PROLOGUE
693+ + NVME_IDCTRL_OACS_FORMAT_SUPPORTED
694+ + NVME_IDCTRL_ONCS_WRITEZ_SUPPORTED
695+ + NVME_IDCTRL_FNA_CRYPTFORMAT_SUPPORTED
696+ + NVME_IDCTRL_EPILOGUE
697+ )
698+ mock_check_output = self.patch(subprocess, "check_output")
699+ mock_check_output.return_value = nvme_cli_output
700+ disk_name = factory.make_name("nvme").encode("ascii")
701+ observered = get_disk_security_info(disk_name)
702+ self.assertThat(
703+ mock_check_output,
704+ MockCallsMatch(
705+ call(["nvme", "id-ctrl", maas_wipe.DEV_PATH % disk_name]),
706+ call(["nvme", "id-ns", maas_wipe.DEV_PATH % disk_name]),
707+ ),
708+ )
709+ self.assertEqual(
710+ {
711+ "format_supported": True,
712+ "writez_supported": True,
713+ "crypto_format": True,
714+ "nsze": 0,
715+ "lbaf": 0,
716+ "ms": 0,
717+ },
718+ observered,
719+ )
720+
721+ def test_get_disk_security_info_nocrypt_format_writez_nvme(self):
722+ nvme_cli_output = (
723+ NVME_IDCTRL_PROLOGUE
724+ + NVME_IDCTRL_OACS_FORMAT_SUPPORTED
725+ + NVME_IDCTRL_ONCS_WRITEZ_SUPPORTED
726+ + NVME_IDCTRL_FNA_CRYPTFORMAT_UNSUPPORTED
727+ + NVME_IDCTRL_EPILOGUE
728+ )
729+ mock_check_output = self.patch(subprocess, "check_output")
730+ mock_check_output.return_value = nvme_cli_output
731+ disk_name = factory.make_name("nvme").encode("ascii")
732+ observered = get_disk_security_info(disk_name)
733+ self.assertThat(
734+ mock_check_output,
735+ MockCallsMatch(
736+ call(["nvme", "id-ctrl", maas_wipe.DEV_PATH % disk_name]),
737+ call(["nvme", "id-ns", maas_wipe.DEV_PATH % disk_name]),
738+ ),
739+ )
740+ self.assertEqual(
741+ {
742+ "format_supported": True,
743+ "writez_supported": True,
744+ "crypto_format": False,
745+ "nsze": 0,
746+ "lbaf": 0,
747+ "ms": 0,
748+ },
749+ observered,
750+ )
751+
752+ def test_get_disk_security_info_crypt_format_nowritez_nvme(self):
753+ nvme_cli_output = (
754+ NVME_IDCTRL_PROLOGUE
755+ + NVME_IDCTRL_OACS_FORMAT_SUPPORTED
756+ + NVME_IDCTRL_ONCS_WRITEZ_UNSUPPORTED
757+ + NVME_IDCTRL_FNA_CRYPTFORMAT_SUPPORTED
758+ + NVME_IDCTRL_EPILOGUE
759+ )
760+ mock_check_output = self.patch(subprocess, "check_output")
761+ mock_check_output.return_value = nvme_cli_output
762+ disk_name = factory.make_name("nvme").encode("ascii")
763+ observered = get_disk_security_info(disk_name)
764+ self.assertThat(
765+ mock_check_output,
766+ MockCallsMatch(
767+ call(["nvme", "id-ctrl", maas_wipe.DEV_PATH % disk_name]),
768+ call(["nvme", "id-ns", maas_wipe.DEV_PATH % disk_name]),
769+ ),
770+ )
771+ self.assertEqual(
772+ {
773+ "format_supported": True,
774+ "writez_supported": False,
775+ "crypto_format": True,
776+ "nsze": 0,
777+ "lbaf": 0,
778+ "ms": 0,
779+ },
780+ observered,
781+ )
782+
783+ def test_get_disk_security_info_noformat_writez_nvme(self):
784+ nvme_cli_output = (
785+ NVME_IDCTRL_PROLOGUE
786+ + NVME_IDCTRL_OACS_FORMAT_UNSUPPORTED
787+ + NVME_IDCTRL_ONCS_WRITEZ_SUPPORTED
788+ + NVME_IDCTRL_FNA_CRYPTFORMAT_SUPPORTED
789+ + NVME_IDCTRL_EPILOGUE
790+ )
791+ mock_check_output = self.patch(subprocess, "check_output")
792+ mock_check_output.return_value = nvme_cli_output
793+ disk_name = factory.make_name("nvme").encode("ascii")
794+ observered = get_disk_security_info(disk_name)
795+ self.assertThat(
796+ mock_check_output,
797+ MockCallsMatch(
798+ call(["nvme", "id-ctrl", maas_wipe.DEV_PATH % disk_name]),
799+ call(["nvme", "id-ns", maas_wipe.DEV_PATH % disk_name]),
800+ ),
801+ )
802+ self.assertEqual(
803+ {
804+ "format_supported": False,
805+ "writez_supported": True,
806+ "crypto_format": True,
807+ "nsze": 0,
808+ "lbaf": 0,
809+ "ms": 0,
810+ },
811+ observered,
812+ )
813+
814+ def test_get_disk_security_info_noformat_nowritez_nvme(self):
815+ nvme_cli_output = (
816+ NVME_IDCTRL_PROLOGUE
817+ + NVME_IDCTRL_OACS_FORMAT_UNSUPPORTED
818+ + NVME_IDCTRL_ONCS_WRITEZ_UNSUPPORTED
819+ + NVME_IDCTRL_FNA_CRYPTFORMAT_UNSUPPORTED
820+ + NVME_IDCTRL_EPILOGUE
821+ )
822+ mock_check_output = self.patch(subprocess, "check_output")
823+ mock_check_output.return_value = nvme_cli_output
824+ disk_name = factory.make_name("nvme").encode("ascii")
825+ observered = get_disk_security_info(disk_name)
826+ self.assertThat(
827+ mock_check_output,
828+ MockCallsMatch(
829+ call(["nvme", "id-ctrl", maas_wipe.DEV_PATH % disk_name]),
830+ call(["nvme", "id-ns", maas_wipe.DEV_PATH % disk_name]),
831+ ),
832+ )
833+ self.assertEqual(
834+ {
835+ "format_supported": False,
836+ "writez_supported": False,
837+ "crypto_format": False,
838+ "nsze": 0,
839+ "lbaf": 0,
840+ "ms": 0,
841+ },
842+ observered,
843+ )
844+
845+ def test_get_disk_security_info_failed_cmd_nvme(self):
846+ mock_check_output = self.patch(subprocess, "check_output")
847+ mock_check_output.side_effect = subprocess.CalledProcessError(
848+ 1, "nvme id-ctrl"
849+ )
850+ disk_name = factory.make_name("nvme").encode("ascii")
851+ observered = get_disk_security_info(disk_name)
852+
853+ self.assertThat(
854+ self.print_flush,
855+ MockCalledOnceWith("Error on nvme id-ctrl (%s)" % "1"),
856+ )
857+ self.assertEqual(
858+ {
859+ "format_supported": False,
860+ "writez_supported": False,
861+ "crypto_format": False,
862+ "nsze": 0,
863+ "lbaf": 0,
864+ "ms": 0,
865+ },
866+ observered,
867+ )
868+
869+ def test_get_disk_security_info_failed_os_nvme(self):
870+ mock_check_output = self.patch(subprocess, "check_output")
871+ mock_check_output.side_effect = OSError(
872+ -2, "No such file or directory"
873+ )
874+ disk_name = factory.make_name("nvme").encode("ascii")
875+ observered = get_disk_security_info(disk_name)
876+
877+ self.assertThat(
878+ self.print_flush,
879+ MockCalledOnceWith(
880+ "OS error when running nvme-cli (No such file or directory)"
881+ ),
882+ )
883+ self.assertEqual(
884+ {
885+ "format_supported": False,
886+ "writez_supported": False,
887+ "crypto_format": False,
888+ "nsze": 0,
889+ "lbaf": 0,
890+ "ms": 0,
891+ },
892+ observered,
893+ )
894+
895+ def test_get_disk_info_nvme(self):
896+ disk_names = [
897+ factory.make_name("nvme").encode("ascii") for _ in range(3)
898+ ]
899+ self.patch(maas_wipe, "list_disks").return_value = disk_names
900+ security_info = [
901+ {
902+ "format_supported": True,
903+ "writez_supported": True,
904+ "crypto_format": True,
905+ "nsze": 0,
906+ "lbaf": 0,
907+ "ms": 0,
908+ }
909+ for _ in range(3)
910+ ]
911+ self.patch(
912+ maas_wipe, "get_nvme_security_info"
913 ).side_effect = security_info
914 observed = get_disk_info()
915 self.assertEqual(
916 {disk_names[i]: security_info[i] for i in range(3)}, observed
917 )
918
919- def test_try_secure_erase_not_supported(self):
920+ def test_try_secure_erase_not_supported_hdparm(self):
921 disk_name = factory.make_name("disk").encode("ascii")
922 disk_info = {
923 b"supported": False,
924@@ -355,7 +468,7 @@ class TestMAASWipe(MAASTestCase):
925 ),
926 )
927
928- def test_try_secure_erase_frozen(self):
929+ def test_try_secure_erase_frozen_hdparm(self):
930 disk_name = factory.make_name("disk").encode("ascii")
931 disk_info = {
932 b"supported": True,
933@@ -372,7 +485,7 @@ class TestMAASWipe(MAASTestCase):
934 ),
935 )
936
937- def test_try_secure_erase_locked(self):
938+ def test_try_secure_erase_locked_hdparm(self):
939 disk_name = factory.make_name("disk").encode("ascii")
940 disk_info = {
941 b"supported": True,
942@@ -389,7 +502,7 @@ class TestMAASWipe(MAASTestCase):
943 ),
944 )
945
946- def test_try_secure_erase_enabled(self):
947+ def test_try_secure_erase_enabled_hdparm(self):
948 disk_name = factory.make_name("disk").encode("ascii")
949 disk_info = {
950 b"supported": True,
951@@ -406,7 +519,7 @@ class TestMAASWipe(MAASTestCase):
952 ),
953 )
954
955- def test_try_secure_erase_failed_erase(self):
956+ def test_try_secure_erase_failed_erase_hdparm(self):
957 disk_name = factory.make_name("disk").encode("ascii")
958 disk_info = {
959 b"supported": True,
960@@ -415,7 +528,7 @@ class TestMAASWipe(MAASTestCase):
961 b"frozen": False,
962 }
963 exception = factory.make_exception()
964- self.patch(maas_wipe, "secure_erase").side_effect = exception
965+ self.patch(maas_wipe, "secure_erase_hdparm").side_effect = exception
966 self.assertFalse(try_secure_erase(disk_name, disk_info))
967 self.assertThat(
968 self.print_flush,
969@@ -425,7 +538,7 @@ class TestMAASWipe(MAASTestCase):
970 ),
971 )
972
973- def test_try_secure_erase_successful_erase(self):
974+ def test_try_secure_erase_successful_erase_hdparm(self):
975 disk_name = factory.make_name("disk").encode("ascii")
976 disk_info = {
977 b"supported": True,
978@@ -433,7 +546,7 @@ class TestMAASWipe(MAASTestCase):
979 b"locked": False,
980 b"frozen": False,
981 }
982- self.patch(maas_wipe, "secure_erase")
983+ self.patch(maas_wipe, "secure_erase_hdparm")
984 self.assertTrue(try_secure_erase(disk_name, disk_info))
985 self.assertThat(
986 self.print_flush,
987@@ -443,7 +556,223 @@ class TestMAASWipe(MAASTestCase):
988 ),
989 )
990
991- def test_secure_erase_writes_known_data(self):
992+ def test_try_secure_erase_not_supported_nvme(self):
993+ disk_name = factory.make_name("nvme").encode("ascii")
994+ sec_info = {
995+ "format_supported": False,
996+ "writez_supported": True,
997+ "crypto_format": True,
998+ "nsze": 0,
999+ "lbaf": 0,
1000+ "ms": 0,
1001+ }
1002+ self.assertFalse(try_secure_erase(disk_name, sec_info))
1003+ self.assertThat(
1004+ self.print_flush,
1005+ MockCalledOnceWith(
1006+ "Device %s does not support formatting"
1007+ % disk_name.decode("ascii")
1008+ ),
1009+ )
1010+
1011+ def test_try_secure_erase_successful_cryto_nvme(self):
1012+ disk_name = factory.make_name("nvme").encode("ascii")
1013+ sec_info = {
1014+ "format_supported": True,
1015+ "writez_supported": True,
1016+ "crypto_format": True,
1017+ "nsze": 0,
1018+ "lbaf": 0,
1019+ "ms": 0,
1020+ }
1021+ mock_check_output = self.patch(subprocess, "check_output")
1022+ self.assertTrue(try_secure_erase(disk_name, sec_info))
1023+ self.assertThat(
1024+ mock_check_output,
1025+ MockCalledOnceWith(
1026+ [
1027+ "nvme",
1028+ "format",
1029+ "-s",
1030+ "2",
1031+ "-l",
1032+ "0",
1033+ "-m",
1034+ "0",
1035+ maas_wipe.DEV_PATH % disk_name,
1036+ ]
1037+ ),
1038+ )
1039+ self.assertThat(
1040+ self.print_flush,
1041+ MockCalledOnceWith(
1042+ "Secure erase was successful on NVMe drive %s"
1043+ % disk_name.decode("ascii")
1044+ ),
1045+ )
1046+
1047+ def test_try_secure_erase_successful_nocryto_nvme(self):
1048+ disk_name = factory.make_name("nvme").encode("ascii")
1049+ sec_info = {
1050+ "format_supported": True,
1051+ "writez_supported": True,
1052+ "crypto_format": False,
1053+ "nsze": 0,
1054+ "lbaf": 0,
1055+ "ms": 0,
1056+ }
1057+ mock_check_output = self.patch(subprocess, "check_output")
1058+ self.assertTrue(try_secure_erase(disk_name, sec_info))
1059+ self.assertThat(
1060+ mock_check_output,
1061+ MockCalledOnceWith(
1062+ [
1063+ "nvme",
1064+ "format",
1065+ "-s",
1066+ "1",
1067+ "-l",
1068+ "0",
1069+ "-m",
1070+ "0",
1071+ maas_wipe.DEV_PATH % disk_name,
1072+ ]
1073+ ),
1074+ )
1075+ self.assertThat(
1076+ self.print_flush,
1077+ MockCalledOnceWith(
1078+ "Secure erase was successful on NVMe drive %s"
1079+ % disk_name.decode("ascii")
1080+ ),
1081+ )
1082+
1083+ def test_try_secure_erase_failed_nvme(self):
1084+ disk_name = factory.make_name("nvme").encode("ascii")
1085+ sec_info = {
1086+ "format_supported": True,
1087+ "writez_supported": True,
1088+ "crypto_format": True,
1089+ "nsze": 0,
1090+ "lbaf": 0,
1091+ "ms": 0,
1092+ }
1093+ mock_check_output = self.patch(subprocess, "check_output")
1094+ mock_check_output.side_effect = subprocess.CalledProcessError(
1095+ 1, "nvme format"
1096+ )
1097+
1098+ self.assertFalse(try_secure_erase(disk_name, sec_info))
1099+ self.assertThat(
1100+ self.print_flush,
1101+ MockCalledOnceWith("Error with format command (%s)" % "1"),
1102+ )
1103+
1104+ def test_try_write_zeroes_not_supported_nvme(self):
1105+ disk_name = factory.make_name("nvme").encode("ascii")
1106+ sec_info = {
1107+ "format_supported": False,
1108+ "writez_supported": False,
1109+ "crypto_format": False,
1110+ "nsze": 1,
1111+ "lbaf": 0,
1112+ "ms": 0,
1113+ }
1114+ mock_print = self.patch(builtins, "print")
1115+ self.assertFalse(nvme_write_zeroes(disk_name, sec_info))
1116+ self.assertThat(
1117+ mock_print,
1118+ MockCalledOnceWith(
1119+ "NVMe drive %s does not support write-zeroes"
1120+ % disk_name.decode("ascii")
1121+ ),
1122+ )
1123+ self.assertThat(
1124+ self.print_flush,
1125+ MockCalledOnceWith("Will fallback to regular drive zeroing."),
1126+ )
1127+
1128+ def test_try_write_zeroes_supported_invalid_nsze_nvme(self):
1129+ disk_name = factory.make_name("nvme").encode("ascii")
1130+ sec_info = {
1131+ "format_supported": False,
1132+ "writez_supported": True,
1133+ "crypto_format": False,
1134+ "nsze": 0,
1135+ "lbaf": 0,
1136+ "ms": 0,
1137+ }
1138+ mock_print = self.patch(builtins, "print")
1139+ self.assertFalse(nvme_write_zeroes(disk_name, sec_info))
1140+ self.assertThat(
1141+ mock_print,
1142+ MockCalledOnceWith(
1143+ "Bad namespace information collected on NVMe drive %s"
1144+ % disk_name.decode("ascii")
1145+ ),
1146+ )
1147+ self.assertThat(
1148+ self.print_flush,
1149+ MockCalledOnceWith("Will fallback to regular drive zeroing."),
1150+ )
1151+
1152+ def test_try_write_zeroes_successful_nvme(self):
1153+ disk_name = factory.make_name("nvme").encode("ascii")
1154+ sec_info = {
1155+ "format_supported": False,
1156+ "writez_supported": True,
1157+ "crypto_format": False,
1158+ "nsze": 0x100A,
1159+ "lbaf": 0,
1160+ "ms": 0,
1161+ }
1162+ mock_check_output = self.patch(subprocess, "check_output")
1163+ self.assertTrue(nvme_write_zeroes(disk_name, sec_info))
1164+ self.assertThat(
1165+ mock_check_output,
1166+ MockCalledOnceWith(
1167+ [
1168+ "nvme",
1169+ "write-zeroes",
1170+ "-f",
1171+ "-s",
1172+ "0",
1173+ "-c",
1174+ "100a",
1175+ maas_wipe.DEV_PATH % disk_name,
1176+ ]
1177+ ),
1178+ )
1179+ self.assertThat(
1180+ self.print_flush,
1181+ MockCalledOnceWith(
1182+ "%s: successfully zeroed (using write-zeroes)."
1183+ % disk_name.decode("ascii")
1184+ ),
1185+ )
1186+
1187+ def test_try_write_zeroes_failed_nvme(self):
1188+ disk_name = factory.make_name("nvme").encode("ascii")
1189+ sec_info = {
1190+ "format_supported": False,
1191+ "writez_supported": True,
1192+ "crypto_format": False,
1193+ "nsze": 100,
1194+ "lbaf": 0,
1195+ "ms": 0,
1196+ }
1197+ mock_check_output = self.patch(subprocess, "check_output")
1198+ mock_check_output.side_effect = subprocess.CalledProcessError(
1199+ 1, "nvme write-zeroes"
1200+ )
1201+
1202+ self.assertFalse(nvme_write_zeroes(disk_name, sec_info))
1203+ self.assertThat(
1204+ self.print_flush,
1205+ MockCalledOnceWith("Error with write-zeroes command (%s)" % "1"),
1206+ )
1207+
1208+ def test_secure_erase_writes_known_data_hdparm(self):
1209 tmp_dir = self.make_dir()
1210 dev_path = (tmp_dir + "/%s").encode("ascii")
1211 self.patch(maas_wipe, "DEV_PATH", dev_path)
1212@@ -455,7 +784,7 @@ class TestMAASWipe(MAASTestCase):
1213 mock_check_output = self.patch(subprocess, "check_output")
1214 mock_check_output.side_effect = factory.make_exception()
1215
1216- self.assertRaises(WipeError, secure_erase, dev_name)
1217+ self.assertRaises(WipeError, secure_erase_hdparm, dev_name)
1218 expected_buf = b"M" * 1024 * 1024
1219 with open(file_path, "rb") as fp:
1220 read_buf = fp.read(len(expected_buf))
1221@@ -463,7 +792,7 @@ class TestMAASWipe(MAASTestCase):
1222 expected_buf, read_buf, "First 1 MiB of file was not written."
1223 )
1224
1225- def test_secure_erase_sets_security_password(self):
1226+ def test_secure_erase_sets_security_password_hdparm(self):
1227 tmp_dir = self.make_dir()
1228 dev_path = (tmp_dir + "/%s").encode("ascii")
1229 self.patch(maas_wipe, "DEV_PATH", dev_path)
1230@@ -476,10 +805,10 @@ class TestMAASWipe(MAASTestCase):
1231 # Fail to get disk info just to exit early.
1232 exception_type = factory.make_exception_type()
1233 self.patch(
1234- maas_wipe, "get_disk_security_info"
1235+ maas_wipe, "get_hdparm_security_info"
1236 ).side_effect = exception_type()
1237
1238- self.assertRaises(exception_type, secure_erase, dev_name)
1239+ self.assertRaises(exception_type, secure_erase_hdparm, dev_name)
1240 self.assertThat(
1241 mock_check_output,
1242 MockCalledOnceWith(
1243@@ -494,7 +823,7 @@ class TestMAASWipe(MAASTestCase):
1244 ),
1245 )
1246
1247- def test_secure_erase_fails_if_not_enabled(self):
1248+ def test_secure_erase_fails_if_not_enabled_hdparm(self):
1249 tmp_dir = self.make_dir()
1250 dev_path = (tmp_dir + "/%s").encode("ascii")
1251 self.patch(maas_wipe, "DEV_PATH", dev_path)
1252@@ -503,16 +832,16 @@ class TestMAASWipe(MAASTestCase):
1253 self.make_empty_file(file_path)
1254
1255 self.patch(subprocess, "check_output")
1256- self.patch(maas_wipe, "get_disk_security_info").return_value = {
1257+ self.patch(maas_wipe, "get_hdparm_security_info").return_value = {
1258 b"enabled": False
1259 }
1260
1261- error = self.assertRaises(WipeError, secure_erase, dev_name)
1262+ error = self.assertRaises(WipeError, secure_erase_hdparm, dev_name)
1263 self.assertEqual(
1264 "Failed to enable security to perform secure erase.", str(error)
1265 )
1266
1267- def test_secure_erase_fails_when_still_enabled(self):
1268+ def test_secure_erase_fails_when_still_enabled_hdparm(self):
1269 tmp_dir = self.make_dir()
1270 dev_path = (tmp_dir + "/%s").encode("ascii")
1271 self.patch(maas_wipe, "DEV_PATH", dev_path)
1272@@ -521,14 +850,14 @@ class TestMAASWipe(MAASTestCase):
1273 self.make_empty_file(file_path)
1274
1275 mock_check_output = self.patch(subprocess, "check_output")
1276- self.patch(maas_wipe, "get_disk_security_info").return_value = {
1277+ self.patch(maas_wipe, "get_hdparm_security_info").return_value = {
1278 b"enabled": True
1279 }
1280 exception = factory.make_exception()
1281 mock_check_call = self.patch(subprocess, "check_call")
1282 mock_check_call.side_effect = exception
1283
1284- error = self.assertRaises(WipeError, secure_erase, dev_name)
1285+ error = self.assertRaises(WipeError, secure_erase_hdparm, dev_name)
1286 self.assertThat(
1287 mock_check_call,
1288 MockCalledOnceWith(
1289@@ -561,7 +890,7 @@ class TestMAASWipe(MAASTestCase):
1290 self.assertEqual("Failed to securely erase.", str(error))
1291 self.assertEqual(exception, error.__cause__)
1292
1293- def test_secure_erase_fails_when_buffer_not_different(self):
1294+ def test_secure_erase_fails_when_buffer_not_different_hdparm(self):
1295 tmp_dir = self.make_dir()
1296 dev_path = (tmp_dir + "/%s").encode("ascii")
1297 self.patch(maas_wipe, "DEV_PATH", dev_path)
1298@@ -570,13 +899,13 @@ class TestMAASWipe(MAASTestCase):
1299 self.make_empty_file(file_path)
1300
1301 self.patch(subprocess, "check_output")
1302- self.patch(maas_wipe, "get_disk_security_info").side_effect = [
1303+ self.patch(maas_wipe, "get_hdparm_security_info").side_effect = [
1304 {b"enabled": True},
1305 {b"enabled": False},
1306 ]
1307 mock_check_call = self.patch(subprocess, "check_call")
1308
1309- error = self.assertRaises(WipeError, secure_erase, dev_name)
1310+ error = self.assertRaises(WipeError, secure_erase_hdparm, dev_name)
1311 self.assertThat(
1312 mock_check_call,
1313 MockCalledOnceWith(
1314@@ -595,7 +924,7 @@ class TestMAASWipe(MAASTestCase):
1315 str(error),
1316 )
1317
1318- def test_secure_erase_fails_success(self):
1319+ def test_secure_erase_fails_success_hdparm(self):
1320 tmp_dir = self.make_dir()
1321 dev_path = (tmp_dir + "/%s").encode("ascii")
1322 self.patch(maas_wipe, "DEV_PATH", dev_path)
1323@@ -604,7 +933,7 @@ class TestMAASWipe(MAASTestCase):
1324 self.make_empty_file(file_path)
1325
1326 self.patch(subprocess, "check_output")
1327- self.patch(maas_wipe, "get_disk_security_info").side_effect = [
1328+ self.patch(maas_wipe, "get_hdparm_security_info").side_effect = [
1329 {b"enabled": True},
1330 {b"enabled": False},
1331 ]
1332@@ -620,9 +949,9 @@ class TestMAASWipe(MAASTestCase):
1333 mock_check_call.side_effect = wipe_buffer
1334
1335 # No error should be raised.
1336- secure_erase(dev_name)
1337+ secure_erase_hdparm(dev_name)
1338
1339- def test_wipe_quickly(self):
1340+ def test_wipe_quickly_successful(self):
1341 tmp_dir = self.make_dir()
1342 dev_path = (tmp_dir + "/%s").encode("ascii")
1343 self.patch(maas_wipe, "DEV_PATH", dev_path)
1344@@ -630,7 +959,14 @@ class TestMAASWipe(MAASTestCase):
1345 file_path = dev_path % dev_name
1346 self.make_empty_file(file_path, content=b"T")
1347
1348+ mock_check_output = self.patch(subprocess, "check_output")
1349 wipe_quickly(dev_name)
1350+ self.assertThat(
1351+ mock_check_output,
1352+ MockCalledOnceWith(
1353+ ["wipefs", "-f", "-a", maas_wipe.DEV_PATH % dev_name]
1354+ ),
1355+ )
1356
1357 buf_size = 1024 * 1024
1358 with open(file_path, "rb") as fp:
1359@@ -641,8 +977,18 @@ class TestMAASWipe(MAASTestCase):
1360 zero_buf = b"\0" * 1024 * 1024
1361 self.assertEqual(zero_buf, first_buf, "First 1 MiB was not wiped.")
1362 self.assertEqual(zero_buf, last_buf, "Last 1 MiB was not wiped.")
1363+ self.assertThat(
1364+ self.print_flush,
1365+ MockCallsMatch(
1366+ call("%s: starting quick wipe." % dev_name.decode("ascii")),
1367+ call(
1368+ "%s: successfully quickly wiped."
1369+ % dev_name.decode("ascii")
1370+ ),
1371+ ),
1372+ )
1373
1374- def test_zero_disk(self):
1375+ def test_wipe_quickly_successful_but_wipefs_failed(self):
1376 tmp_dir = self.make_dir()
1377 dev_path = (tmp_dir + "/%s").encode("ascii")
1378 self.patch(maas_wipe, "DEV_PATH", dev_path)
1379@@ -650,13 +996,83 @@ class TestMAASWipe(MAASTestCase):
1380 file_path = dev_path % dev_name
1381 self.make_empty_file(file_path, content=b"T")
1382
1383+ mock_check_output = self.patch(subprocess, "check_output")
1384+ mock_check_output.side_effect = subprocess.CalledProcessError(
1385+ 1, "wipefs"
1386+ )
1387+ wipe_quickly(dev_name)
1388+
1389+ buf_size = 1024 * 1024
1390+ with open(file_path, "rb") as fp:
1391+ first_buf = fp.read(buf_size)
1392+ fp.seek(-buf_size, 2)
1393+ last_buf = fp.read(buf_size)
1394+
1395+ zero_buf = b"\0" * 1024 * 1024
1396+ self.assertEqual(zero_buf, first_buf, "First 1 MiB was not wiped.")
1397+ self.assertEqual(zero_buf, last_buf, "Last 1 MiB was not wiped.")
1398+ self.assertThat(
1399+ self.print_flush,
1400+ MockCallsMatch(
1401+ call("%s: starting quick wipe." % dev_name.decode("ascii")),
1402+ call("%s: wipefs failed (1)" % dev_name.decode("ascii")),
1403+ call(
1404+ "%s: successfully quickly wiped."
1405+ % dev_name.decode("ascii")
1406+ ),
1407+ ),
1408+ )
1409+
1410+ def test_wipe_quickly_failed(self):
1411+ dev_name = factory.make_name("disk").encode("ascii")
1412+
1413+ mock_check_output = self.patch(subprocess, "check_output")
1414+ mock_check_output.side_effect = subprocess.CalledProcessError(
1415+ 1, "wipefs"
1416+ )
1417+
1418+ mock_os_open = self.patch(builtins, "open")
1419+ mock_os_open.side_effect = OSError(-2, "No such file or directory")
1420+
1421+ wipe_quickly(dev_name)
1422+
1423+ self.assertThat(
1424+ self.print_flush,
1425+ MockCallsMatch(
1426+ call("%s: starting quick wipe." % dev_name.decode("ascii")),
1427+ call("%s: wipefs failed (1)" % dev_name.decode("ascii")),
1428+ call(
1429+ "%s: OS error while wiping beginning/end of disk (No such file or directory)"
1430+ % dev_name.decode("ascii")
1431+ ),
1432+ call(
1433+ "%s: failed to be quickly wiped."
1434+ % dev_name.decode("ascii")
1435+ ),
1436+ ),
1437+ )
1438+
1439+ def test_zero_disk_hdd(self):
1440+ tmp_dir = self.make_dir()
1441+ dev_path = (tmp_dir + "/%s").encode("ascii")
1442+ self.patch(maas_wipe, "DEV_PATH", dev_path)
1443+ dev_name = factory.make_name("disk").encode("ascii")
1444+ file_path = dev_path % dev_name
1445+ self.make_empty_file(file_path, content=b"T")
1446+ disk_info = {
1447+ b"supported": True,
1448+ b"enabled": False,
1449+ b"locked": False,
1450+ b"frozen": False,
1451+ }
1452+
1453 # Add a little size to the file making it not evenly
1454 # divisable by 1 MiB.
1455 extra_end = 512
1456 with open(file_path, "a+b") as fp:
1457 fp.write(b"T" * extra_end)
1458
1459- zero_disk(dev_name)
1460+ zero_disk(dev_name, disk_info)
1461
1462 zero_buf = b"\0" * 1024 * 1024
1463 with open(file_path, "rb") as fp:
1464@@ -682,7 +1098,7 @@ class TestMAASWipe(MAASTestCase):
1465 parser.parse_args.return_value = args
1466 self.patch(argparse, "ArgumentParser").return_value = parser
1467
1468- def test_main_calls_try_secure_erase_for_all_disks(self):
1469+ def test_main_calls_try_secure_erase_for_all_hdd(self):
1470 self.patch_args(True, False)
1471 disks = {
1472 factory.make_name("disk").encode("ascii"): {} for _ in range(3)
1473@@ -698,7 +1114,7 @@ class TestMAASWipe(MAASTestCase):
1474 self.assertThat(mock_try, MockCallsMatch(*calls))
1475 self.assertThat(mock_zero, MockNotCalled())
1476
1477- def test_main_calls_zero_disk_if_no_secure_erase(self):
1478+ def test_main_calls_zero_disk_if_no_secure_erase_hdd(self):
1479 self.patch_args(True, False)
1480 disks = {
1481 factory.make_name("disk").encode("ascii"): {} for _ in range(3)
1482@@ -711,11 +1127,10 @@ class TestMAASWipe(MAASTestCase):
1483 maas_wipe.main()
1484
1485 try_calls = [call(disk, info) for disk, info in disks.items()]
1486- wipe_calls = [call(disk) for disk in disks.keys()]
1487 self.assertThat(mock_try, MockCallsMatch(*try_calls))
1488- self.assertThat(mock_zero, MockCallsMatch(*wipe_calls))
1489+ self.assertThat(mock_zero, MockCallsMatch(*try_calls))
1490
1491- def test_main_calls_wipe_quickly_if_no_secure_erase(self):
1492+ def test_main_calls_wipe_quickly_if_no_secure_erase_hdd(self):
1493 self.patch_args(True, True)
1494 disks = {
1495 factory.make_name("disk").encode("ascii"): {} for _ in range(3)
1496@@ -760,6 +1175,6 @@ class TestMAASWipe(MAASTestCase):
1497 mock_try.return_value = False
1498 maas_wipe.main()
1499
1500- wipe_calls = [call(disk) for disk in disks.keys()]
1501+ wipe_calls = [call(disk, info) for disk, info in disks.items()]
1502 self.assertThat(mock_try, MockNotCalled())
1503 self.assertThat(zero_disk, MockCallsMatch(*wipe_calls))
1504diff --git a/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe_defs.py b/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe_defs.py
1505new file mode 100644
1506index 0000000..f9bb440
1507--- /dev/null
1508+++ b/src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe_defs.py
1509@@ -0,0 +1,232 @@
1510+#!/usr/bin/python3
1511+# Copyright 2020 Canonical Ltd. This software is licensed under the
1512+# GNU Affero General Public License version 3 (see the file LICENSE).
1513+#
1514+# hdparm / nvme-cli outputs used on maas_wipe testing
1515+
1516+HDPARM_BEFORE_SECURITY = b"""\
1517+/dev/sda:
1518+
1519+ATA device, with non-removable media
1520+ Model Number: INTEL SSDSC2CT240A4
1521+ Serial Number: CVKI3206029X240DGN
1522+ Firmware Revision: 335u
1523+ Transport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions
1524+Standards:
1525+ Used: unknown (minor revision code 0xffff)
1526+ Supported: 9 8 7 6 5
1527+ Likely used: 9
1528+Configuration:
1529+ Logical max current
1530+ cylinders 16383 16383
1531+ heads 16 16
1532+ sectors/track 63 63
1533+ --
1534+ CHS current addressable sectors: 16514064
1535+ LBA user addressable sectors: 268435455
1536+ LBA48 user addressable sectors: 468862128
1537+ Logical Sector size: 512 bytes
1538+ Physical Sector size: 512 bytes
1539+ Logical Sector-0 offset: 0 bytes
1540+ device size with M = 1024*1024: 228936 MBytes
1541+ device size with M = 1000*1000: 240057 MBytes (240 GB)
1542+ cache/buffer size = unknown
1543+ Nominal Media Rotation Rate: Solid State Device
1544+Capabilities:
1545+ LBA, IORDY(can be disabled)
1546+ Queue depth: 32
1547+ Standby timer values: spec'd by Standard, no device specific minimum
1548+ R/W multiple sector transfer: Max = 16 Current = 16
1549+ Advanced power management level: 254
1550+ DMA: mdma0 mdma1 mdma2 udma0 udma1 udma2 udma3 udma4 udma5 *udma6
1551+ Cycle time: min=120ns recommended=120ns
1552+ PIO: pio0 pio1 pio2 pio3 pio4
1553+ Cycle time: no flow control=120ns IORDY flow control=120ns
1554+Commands/features:
1555+ Enabled Supported:
1556+ * SMART feature set
1557+ Security Mode feature set
1558+ * Power Management feature set
1559+ * Write cache
1560+ * Look-ahead
1561+ * Host Protected Area feature set
1562+ * WRITE_BUFFER command
1563+ * READ_BUFFER command
1564+ * NOP cmd
1565+ * DOWNLOAD_MICROCODE
1566+ * Advanced Power Management feature set
1567+ Power-Up In Standby feature set
1568+ * 48-bit Address feature set
1569+ * Mandatory FLUSH_CACHE
1570+ * FLUSH_CACHE_EXT
1571+ * SMART error logging
1572+ * SMART self-test
1573+ * General Purpose Logging feature set
1574+ * WRITE_{DMA|MULTIPLE}_FUA_EXT
1575+ * 64-bit World wide name
1576+ * IDLE_IMMEDIATE with UNLOAD
1577+ * WRITE_UNCORRECTABLE_EXT command
1578+ * {READ,WRITE}_DMA_EXT_GPL commands
1579+ * Segmented DOWNLOAD_MICROCODE
1580+ * Gen1 signaling speed (1.5Gb/s)
1581+ * Gen2 signaling speed (3.0Gb/s)
1582+ * Gen3 signaling speed (6.0Gb/s)
1583+ * Native Command Queueing (NCQ)
1584+ * Host-initiated interface power management
1585+ * Phy event counters
1586+ * DMA Setup Auto-Activate optimization
1587+ Device-initiated interface power management
1588+ * Software settings preservation
1589+ * SMART Command Transport (SCT) feature set
1590+ * SCT Data Tables (AC5)
1591+ * reserved 69[4]
1592+ * Data Set Management TRIM supported (limit 1 block)
1593+ * Deterministic read data after TRIM
1594+"""
1595+
1596+HDPARM_AFTER_SECURITY = b"""\
1597+Logical Unit WWN Device Identifier: 55cd2e40002643cf
1598+ NAA : 5
1599+ IEEE OUI : 5cd2e4
1600+ Unique ID : 0002643cf
1601+Checksum: correct
1602+"""
1603+
1604+HDPARM_SECURITY_NOT_SUPPORTED = b"""\
1605+Security:
1606+ Master password revision code = 65534
1607+ not supported
1608+ not enabled
1609+ not locked
1610+ not frozen
1611+ not expired: security count
1612+ supported: enhanced erase
1613+ 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
1614+"""
1615+
1616+HDPARM_SECURITY_SUPPORTED_NOT_ENABLED = b"""\
1617+Security:
1618+ Master password revision code = 65534
1619+ supported
1620+ not enabled
1621+ not locked
1622+ not frozen
1623+ not expired: security count
1624+ supported: enhanced erase
1625+ 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
1626+"""
1627+
1628+HDPARM_SECURITY_SUPPORTED_ENABLED = b"""\
1629+Security:
1630+ Master password revision code = 65534
1631+ supported
1632+ enabled
1633+ not locked
1634+ not frozen
1635+ not expired: security count
1636+ supported: enhanced erase
1637+ 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
1638+"""
1639+
1640+HDPARM_SECURITY_ALL_TRUE = b"""\
1641+Security:
1642+ Master password revision code = 65534
1643+ supported
1644+ enabled
1645+ locked
1646+ frozen
1647+ not expired: security count
1648+ supported: enhanced erase
1649+ 4min for SECURITY ERASE UNIT. 2min for ENHANCED SECURITY ERASE UNIT.
1650+"""
1651+
1652+NVME_IDCTRL_PROLOGUE = b"""\
1653+NVME Identify Controller:
1654+vid : 0x8086
1655+ssvid : 0x8086
1656+sn : CVMD5066002T400AGN
1657+mn : INTEL SSDPEDME400G4
1658+fr : 8DV10131
1659+rab : 0
1660+ieee : 5cd2e4
1661+cmic : 0
1662+mdts : 5
1663+cntlid : 0
1664+ver : 0
1665+rtd3r : 0
1666+rtd3e : 0
1667+oaes : 0
1668+ctratt : 0
1669+acl : 3
1670+aerl : 3
1671+frmw : 0x2
1672+lpa : 0
1673+elpe : 63
1674+npss : 0
1675+avscc : 0
1676+apsta : 0
1677+wctemp : 0
1678+cctemp : 0
1679+mtfa : 0
1680+hmpre : 0
1681+hmmin : 0
1682+tnvmcap : 0
1683+unvmcap : 0
1684+rpmbs : 0
1685+edstt : 0
1686+dsto : 0
1687+fwug : 0
1688+kas : 0
1689+hctma : 0
1690+mntmt : 0
1691+mxtmt : 0
1692+sanicap : 0
1693+hmminds : 0
1694+hmmaxd : 0
1695+sqes : 0x66
1696+cqes : 0x44
1697+maxcmd : 0
1698+nn : 1
1699+fuses : 0
1700+"""
1701+
1702+NVME_IDCTRL_OACS_FORMAT_SUPPORTED = b"""\
1703+oacs : 0x6
1704+"""
1705+
1706+NVME_IDCTRL_OACS_FORMAT_UNSUPPORTED = b"""\
1707+oacs : 0x4
1708+"""
1709+
1710+NVME_IDCTRL_ONCS_WRITEZ_SUPPORTED = b"""\
1711+oncs : 0xe
1712+"""
1713+
1714+NVME_IDCTRL_ONCS_WRITEZ_UNSUPPORTED = b"""\
1715+oncs : 0x6
1716+"""
1717+
1718+NVME_IDCTRL_FNA_CRYPTFORMAT_SUPPORTED = b"""\
1719+fna : 0x7
1720+"""
1721+
1722+NVME_IDCTRL_FNA_CRYPTFORMAT_UNSUPPORTED = b"""\
1723+fna : 0x3
1724+"""
1725+
1726+NVME_IDCTRL_EPILOGUE = b"""\
1727+vwc : 0
1728+awun : 0
1729+awupf : 0
1730+nvscc : 0
1731+acwu : 0
1732+sgls : 0
1733+subnqn :
1734+ioccsz : 0
1735+iorcsz : 0
1736+icdoff : 0
1737+ctrattr : 0
1738+msdbd : 0
1739+ps 0 : mp:25.00W operational enlat:0 exlat:0 rrt:0 rrl:0
1740+ rwt:0 rwl:0 idle_power:- active_power:-
1741+"""

Subscribers

People subscribed via source and target branches