Merge ~ltrager/maas:lp1835954_2.6 into maas:2.6

Proposed by Lee Trager
Status: Merged
Approved by: Lee Trager
Approved revision: eefcc0910741ce62326b739049cd7744e44ee06e
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ltrager/maas:lp1835954_2.6
Merge into: maas:2.6
Diff against target: 1831 lines (+1179/-286)
4 files modified
src/maasserver/compose_preseed.py (+3/-0)
src/metadataserver/user_data/templates/snippets/maas_wipe.py (+292/-60)
src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe.py (+652/-226)
src/metadataserver/user_data/templates/snippets/tests/test_maas_wipe_defs.py (+232/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Needs Fixing
Lee Trager (community) Approve
Review via email: mp+387874@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 :
Revision history for this message
MAAS Lander (maas-lander) wrote :

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

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/8002/console
COMMIT: c48832ad281ae72d06db92d3197c3cd0d3e901fd

review: Needs Fixing
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :

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

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas/job/branch-tester/8004/console
COMMIT: ad390606298978cb1f13c2285f7b13d9713e2907

review: Needs Fixing

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

Subscribers

People subscribed via source and target branches