forked from pynvme/pynvme
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdriver_wrap.pyx
2205 lines (1792 loc) · 82.4 KB
/
driver_wrap.pyx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# BSD LICENSE
#
# Copyright (c) Crane Che <[email protected]>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Intel Corporation nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#cython: binding=True
#cython: language_level=3
##//cython: linetrace=True
##//distutils: define_macros=CYTHON_TRACE=1
"""test NVMe devices in Python. [https://github.com/cranechu/pynvme]
[](https://gitlab.com/cranechu/pynvme/pipelines)
[](https://github.com/cranechu/pynvme/blob/master/LICENSE)
[](https://github.com/cranechu/pynvme/releases)
<a href="http://www.youtube.com/watch?feature=player_embedded&v=_UE7gn68uwg" target="_blank"><img align="right" src="http://img.youtube.com/vi/_UE7gn68uwg/0.jpg" alt="pynvme introduction" width="480" border="0" /></a>
- [Tutorial](#tutorial)
- [Install](#install)
- [VSCode](#vscode)
- [Features](#features)
- [Files](#files)
- [Fixtures](#fixtures)
- [Debug](#debug)
- [Classes](#classes)
* [Pcie](#pcie)
* [Controller](#controller)
* [Namespace](#namespace)
* [Qpair](#qpair)
* [Buffer](#buffer)
* [Subsystem](#subsystem)
The pynvme is a python extension module. Users can operate NVMe SSD intuitively in Python scripts. It is designed for NVMe SSD testing with performance considered. Integrated with third-party tools, vscode and pytest, pynvme provides a convenient and professional NVMe device test solution.
Pynvme can test multiple NVMe DUT devices, operate most of the NVMe commands, support callback functions, and manage reset/power of NVMe devices. User needs root privilege to use pynvme. It provides classes to access and test NVMe devices:
1. Subsystem: controls the power and reset of NVMe subsystem
2. Pcie: accesses PCIe device's config space
3. Controller: accesses NVMe registers and operates admin commands
4. Namespace: abstracts NVMe namespace and operates NVM commands
5. Qpair: manages NVMe IO SQ/CQ. Admin SQ/CQ are managed by Controller
6. Buffer: allocates and manipulates the data buffer on host memory
7. IOWorker: reads and/or writes NVMe Namespace in separated processors
Please use python "help()" to find more details of these classes.
Pynvme works on Linux, and uses SPDK as the NVMe driver. DPDK and SPDK are statically linked in the module's .so object file, so users do not need to setup SPDK develop environment. The host Linux OS image is installed in a SATA drive, because the kernel's NVMe drive will be unloaded by Pynvme during the test. Pynvme does write data to your NVMe devices, so it could corrupt your data in the device. Users have to provide correct BDF (Bus:Device.Function) address to initialize the controller of the DUT device.
Tutorial
========
Pynvme is easy to use, from simple operations to deliberated designed test scripts. User can leverage well developed tools and knowledges in Python community. Here are some Pynvme script examples.
Fetch the controller's identify data. Example:
```python
>>> import nvme as d
>>> nvme0 = d.Controller(b"01:00.0") # initialize NVMe controller with its PCIe BDF address
>>> id_buf = d.Buffer(4096) # allocate the buffer
>>> nvme0.identify(id_buf, nsid=0xffffffff, cns=1) # read namespace identify data into buffer
>>> nvme0.waitdone() # nvme commands are executed asynchronously, so we have to
>>> id_buf.dump() # print the whole buffer
```
Yet another hello world example of SPDK nvme driver. Example:
```python
>>> import nvme as d
>>> data_buf = d.Buffer(512)
>>> data_buf[100:] = b'hello world'
>>> nvme0 = d.Controller(b"01:00.0")
>>> nvme0n1 = d.Namespace(nvme0, 1)
>>> qpair = d.Qpair(nvme0, 16) # create IO SQ/CQ pair, with 16 queue-depth
>>> def write_cb(cdw0, status): # command callback function
>>> nvme0n1.read(qpair, data_buf, 0, 1).waitdone()
>>> nvme0n1.write(qpair, data_buf, 0, 1, cb=write_cb).waitdone()
>>> qpair.cmdlog() # print recently issued commands
>>> assert data_buf[100:] = b'hello world'
```
Test invalid command. Example:
```python
>>> import nvme as d
>>> nvme0 = d.Controller(b"01:00.0")
>>> nvme0n1 = d.Namespace(nvme0, 1)
>>> qpair = d.Qpair(nvme0, 16) # create IO SQ/CQ pair, with 16 queue-depth
>>> logging.info("send an invalid command with opcode 0xff")
>>> with pytest.warns(UserWarning, match="ERROR status: 00/01"):
>>> nvme0n1.send_cmd(0xff, qpair, nsid=1).waitdone()
```
Performance test, while monitoring the device temperature. Example:
```python
>>> import nvme as d
>>> nvme0 = d.Controller(b"01:00.0")
>>> nvme0n1 = d.Namespace(nvme0, 1)
>>> with nvme0n1.ioworker(lba_start = 0, io_size = 256, lba_align = 8,
lba_random = False,
region_start = 0, region_end = 100000,
read_percentage = 0,
iops = 0, io_count = 1000000, time = 0,
qprio = 0, qdepth = 16), \\
nvme0n1.ioworker(lba_start = 0, io_size = 7, lba_align = 11,
lba_random = False,
region_start = 0, region_end = 1000,
read_percentage = 0,
iops = 0, io_count = 100, time = 1000,
qprio = 0, qdepth = 64), \\
nvme0n1.ioworker(lba_start = 0, io_size = 8, lba_align = 64,
lba_random = False,
region_start = 10000, region_end = 1000000,
read_percentage = 67,
iops = 10000, io_count = 1000000, time = 1000,
qprio = 0, qdepth = 16), \\
nvme0n1.ioworker(lba_start = 0, io_size = 8, lba_align = 8,
lba_random = True,
region_start = 0, region_end = 0xffffffffffffffff,
read_percentage = 0,
iops = 10, io_count = 100, time = 0,
qprio = 0, qdepth = 16):
>>> import time
>>> import logging
>>> import pytemperature
>>> # monitor device temperature on high loading operations
>>> logpage_buf = d.Buffer(512)
>>> nvme0.getlogpage(2, logpage_buf, 512).waitdone()
>>> logging.info("current temperature: %d" % pytemperature.k2c(logpage_buf[50]&0xffff))
>>> time.sleep(5)
```
Install
=======
Pynvme is installed from compiling source code. It is recommended to install and use pynvme in Fedora 29 or later. Ubuntu is also tested. Pynvme cannot be installed in NVMe device, because kernel's NVMe driver should be unloaded before starting pynvme and SPDK. It is recommended to install OS and pynvme into SATA drive.
System Requirement
------------------
1. Intel CPU with SSE4.2 instruction set
2. Linux, e.g. Fedora 29 or later
2. 8GB DRAM recommended, or larger if the DUT capacity is larger
3. deep mode (S3) is supported in /sys/power/mem_sleep
3. Python3. Python2 is not supported.
Source Code
-----------
```shell
git clone https://github.com/cranechu/pynvme
```
Prerequisites
-------------
First, to fetch all required dependencies source code, and the python pip.
```shell
cd pynvme
git submodule update --init --recursive
sudo dnf install python3-pip -y # Ubuntu: sudo apt-get install python3-pip
```
Build
-----
Compile the SPDK, and then pynvme.
```shell
cd spdk; ./configure --without-isal; cd .. # configurate SPDK
make spdk # compile SPDK
make clean; make # compile pynvme
```
Now, you can find the generated binary file like: nvme.cpython-37m-x86_64-linux-gnu.so
Test
----
Setup SPDK runtime environment to remove kernel NVMe driver and enable SPDK NVMe driver. Now, we can try the tests of pynvme.
```shell
make setup
make test
```
User can find pynvme documents in README.md, or use help() in python:
```shell
sudo python3 -c "import nvme; help(nvme)"
```
- After test, you may wish to bring kernel NVMe driver back like this.
```shell
make reset
```
VSCode
======
Pynvme works with VSCode! And pytest also!
Root user is not recommended in vscode, so just use your ordinary non-root user. It is required to configurate the user account to run sudo without a password.
```shell
sudo visudo
```
In order to monitor qpairs status and cmdlog along the progress of testing, user can install vscode extension pynvme-console:
```shell
code --install-extension pynvme-console-0.0.1.vsix
```
The extension pynvme-console gives realtime device status and cmdlog of every qpair in vscode's tree-views and (read-only) editors.
Before start vscode, modify .vscode/settings.json with correct pcie address (bus:device.function, which can be found by lspci shell command) of your DUT device.
```shell
lspci
# 01:00.0 Non-Volatile memory controller: Lite-On Technology Corporation Device 2300 (rev 01)
```
Then in pynvme folder, we can start vscode to edit, debug and run scripts:
```shell
make setup; code . # make sure to enable SPDK nvme driver before starting vscode
```
For each test file, import required python packages first:
```python
import nvme
import logging
import nvme as d # this is pynvme's python package name
```
VSCode is convenient and powerful, but it consumes a lot of resources. So, for formal performance test, it is recommended to run in command line, or Emacs.
Features
========
Pynvme writes and reads data in buffer to NVMe device LBA space. In order to verify the data integrity, it injects LBA address and version information into the write data buffer, and check with them after read completion. Furthermore, Pynvme computes and verifies CRC32 of each LBA on the fly. Both data buffer and LBA CRC32 are stored in host memory, so ECC memory are recommended if you are considering serious tests.
Buffer should be allocated for data commands, and held till that command is completed because the buffer is being used by NVMe device. Users need to pay more attention on the life scope of the buffer in Python test scripts.
NVMe commands are all asynchronous. Test scripts can sync through waitdone() method to make sure the command is completed. The method waitdone() polls command Completion Queues. When the optional callback function is provided in a command in python scripts, the callback function is called when that command is completed in waitdone().
Pynvme driver provides two arguments to python callback functions: cdw0 of the Completion Queue Entry, and the status. The argument status includes both Phase Tag and Status Field.
If DUT cannot complete the command in 5 seconds, that command would be timeout.
Pynvme traces recent thousands of commands in the cmdlog, as well as the completion entries. The cmdlog traces each qpair's commands and status. Pynvme supports up to 16 qpairs and their cmdlogs. User can list cmdlog to find the commands issued in different command queues, and their timestamps. In the progress of test, we can also use rpc to monitor DUT's registers, qpair, buffer and the cmdlog from 3rd-party tools. For example,
```shell
watch -n 1 sudo ./spdk/scripts/rpc.py get_nvme_controllers # we use existed get_nvme_controllers rpc method to get all DUT information
```
The cost is high and inconvenient to send each read and write command in Python scripts. Pynvme provides the low-cost IOWorker to send IOs in different processes. IOWorker takes full use of multi-core to not only send read/write IO in high speed, but also verify the correctness of data on the fly. User can get IOWorker's test statistics through its close() method. Here is an example of reading 4K data randomly with the IOWorker.
Example:
```python
>>> r = nvme0n1.ioworker(io_size = 8, lba_align = 8,
lba_random = True, qdepth = 16,
read_percentage = 100, time = 10).start().close()
>>> print(r.io_count_read)
>>> print(r.mseconds)
>>> print("IOPS: %dK/s\\n", r.io_count_read/r.mseconds)
```
The controller is not responsible for checking the LBA of a Read or Write command to ensure any type of ordering between commands (NVMe spec 1.3c, 6.3). It means conflicted read write operations on NVMe devices cannot predict the final data result, and thus hard to verify data correctness. Similarly, after writing of multiple IOWorkers in the same LBA region, the subsequent read does not know the latest data content. As a mitigation solution, we suggest to separate read and write operations to different IOWorkers and different LBA regions in test scripts, so it can be avoid to read and write same LBA at simultaneously. For those read and write operations on same LBA region, scripts have to complete one before submitting the other. Test scripts can disable or enable inline verification of read by function config(). By default, it is disabled.
Qpair instance is created based on Controller instance. So, user creates qpair after the controller. In the other side, user should free qpair before the controller. But without explicit code, Python may not do the job in right order. One of the mitigation solution is pytest fixture scope. User can define Controller fixture as session scope and Qpair as function. In the situation, qpair is always deleted before the controller. Admin qpair is managed by controller, so users do not need to create the admin qpair.
Pynvme also supports NVMe/TCP.
Example:
```python
>>> import nvme as d
>>> c = d.Controller(b'127.0.0.1') # connect to the NVMe/TCP target by IP address, and the port is fixed to 4420
>>> n = d.Namespace(c, 1) # test as NVMe/PCIe devices
>>> assert n.id_data(8, 0) != 0 # get identify namespace data
>>> assert n.id_data(16, 8) == n.id_data(8, 0)
```
User can create a local NVMe/TCP target for test purpose, by:
```shell
make nvmt
```
IOWorker
--------
Please refer to "nvme0n1.ioworker" examples in driver_test.py.
Files
=====
Here is a brief introduction on source code files.
| files | notes |
| ------------- |-------------|
| spdk | pynvme is built on SPDK |
| driver_wrap.pyx | pynvme uses cython to bind python and C. All python classes are defined here. |
| cdriver.pxd | interface between python and C |
| driver.h | interface of C |
| driver.c | the core part of pynvme, which extends SPDK for test purpose |
| setup.py | cython configuration for compile |
| Makefile | it is a part of SPDK makefiles |
| driver_test.py | pytest cases for pynvme test. Users can develop more test cases for their NVMe devices. |
| conftest.py | predefined pytest fixtures. Find more details below. |
| pytest.ini | pytest runtime configuration |
Fixtures
========
Pynvme uses pytest to test it self. Users can also use pytest as the test framework to test their NVMe devices. Pytest's fixture is a powerful way to create and free resources in the test.
| fixture | scope | notes |
| ------------- |-----|--------|
| pciaddr | session | PCIe BDF address of the DUT, pass in by argument --pciaddr |
| pcie | session | the object of the PCIe device. |
| nvme0 | session | the object of NVMe controller |
| nvme0n1 | session | the object of first Namespace of the controller |
| verify | function | declare this fixture in test cases where data crc is to be verified. |
Debug
=====
1. assert: it is recommended to compile SPDK with --enable-debug.
1. log: users can change log levels for driver and scripts. All logs are captured/hidden by pytest in default. Please use argument "-s" to print logs in test time.
1. driver: spdk_log_set_print_level in driver.c, for SPDK related logs
2. scripts: log_cli_level in pytest.ini, for python/pytest scripts
2. gdb: when driver crashes or misbehaviours, use can collect debug information through gdb.
1. core dump: sudo coredumpctl debug
2. generate core dump in dead loop: CTRL-\\
3. test within gdb: sudo gdb --args python3 -m pytest --color=yes --pciaddr=01:00.0 "driver_test.py::test_create_device"
If you meet any issue, or have any suggestions, please report them to [Issues](https://github.com/cranechu/pynvme/issues). They are warmly welcome.
Classes
=======
"""
# python package
import os
import sys
import time
import atexit
import signal
import struct
import logging
import warnings
import statistics
import subprocess
import multiprocessing
# c library
import cython
from libc.string cimport strncpy, memset, strlen
from libc.stdio cimport printf
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from cpython.exc cimport PyErr_CheckSignals
# c driver
cimport cdriver as d
# module informatoin
__author__ = "Crane Chu"
__version__ = "0.2.0"
# nvme command timeout, it's a warning
# driver times out earlier than driver wrap
_cTIMEOUT = 5
cdef void timeout_driver_cb(void* cb_arg, d.ctrlr* ctrlr,
d.qpair * qpair, unsigned short cid):
error_string = "drive timeout: %d sec, qpair: %d, cid: %d" % \
(_cTIMEOUT, d.qpair_get_id(qpair), cid)
warnings.warn(error_string)
# timeout signal in wrap layer, it's an assert fail
# driver wrap needs longer timeout, some commands need more time, like format
_cTIMEOUT_wrap = 30
def _timeout_signal_handler(signum, frame):
error_string = "pynvme timeout: %d sec" % _cTIMEOUT_wrap
_reentry_flag_init()
raise TimeoutError(error_string)
# prevent waitdone reentry
def _reentry_flag_init():
global _reentry_flag
_reentry_flag = False
# for abrupt exit
def _interrupt_handler(signal, frame):
logging.debug("terminated.")
sys.exit(0)
# handle cpl in callback from c
cdef struct _cpl:
# a revised completion structure returned to user,
# cdw2 is changed to latency of the command, in micro-seconds
unsigned int cdw0
unsigned int rsvd1
unsigned int latency
unsigned short cid
unsigned short status1 #this word actully inculdes some other bites
cdef void cmd_cb(void* f, const d.cpl* cpl):
arg = <_cpl*>cpl # no qa
status1 = arg.status1
func = <object>f # no qa
if func is not None:
# call script callback function to check cpl
try:
func(arg.cdw0, status1)
except AssertionError as e:
warnings.warn("ASSERT: "+str(e))
elif d.nvme_cpl_is_error(cpl):
# script not check, so driver check cpl
sc = (status1>>1) & 0xff
sct = (status1>>9) & 0x7
warnings.warn("ERROR status: %02x/%02x" % (sct, sc))
cdef void aer_cmd_cb(void* f, const d.cpl* cpl):
warnings.warn("AER notification is triggered")
cmd_cb(f, cpl)
cdef class Buffer(object):
"""Buffer class allocated in DPDK memzone,so can be used by DMA. Data in buffer is clear to 0 in initialization.
Args:
size (int): the size (in bytes) of the buffer
default: 4096
name (str): the name of the buffer
default: 'buffer'
Examples:
```python
>>> b = Buffer(1024, 'example')
>>> b[0] = 0x5a
>>> b[1:3] = [1, 2]
>>> b[4:] = [10, 11, 12, 13]
>>> b.dump(16)
example
00000000 5a 01 02 00 0a 0b 0c 0d 00 00 00 00 00 00 00 00 Z...............
>>> b[:8:2]
b'Z\\x02\\n\\x0c'
>>> b.data(2) == 2
True
>>> b[2] == 2
True
>>> b.data(2, 0) == 0x02015a
True
>>> len(b)
1024
>>> b
<buffer name: example>
>>> b[8:] = b'xyc'
example
00000000 5a 01 02 00 0a 0b 0c 0d 78 79 63 00 00 00 00 00 Z.......xyc.....
>>> b.set_dsm_range(1, 0x1234567887654321, 0xabcdef12)
>>> b.dump(64)
buffer
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 00 00 00 00 12 ef cd ab 21 43 65 87 78 56 34 12 ........!Ce.xV4.
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
```
"""
cdef void* ptr
cdef size_t size
cdef char* name
cdef unsigned long phys_addr
def __cinit__(self, size=4096, name="buffer"):
assert size > 0, "0 is not valid size"
# copy python string to c string
name_len = (len(name)+1)*sizeof(char)
self.name = <char*>PyMem_Malloc(name_len)
if not self.name:
raise MemoryError()
memset(self.name, 0, name_len)
strncpy(self.name, name.encode('ascii'), len(name))
# buffer init
self.size = size
self.ptr = d.buffer_init(size, &self.phys_addr)
if self.ptr is NULL:
raise MemoryError()
def __dealloc__(self):
if self.name is not NULL:
PyMem_Free(self.name)
if self.ptr is not NULL:
d.buffer_fini(self.ptr)
@property
def phys_addr(self):
return self.phys_addr
def dump(self, size=None):
"""print the buffer content
Args:
size: the size of the buffer to print,
default: None, means to print the whole buffer
"""
if self.ptr and self.size:
# 0 size means print the whole buffer
if size is None or size > self.size:
size = self.size
d.log_buf_dump(self.name, self.ptr, size)
def data(self, byte_end, byte_begin=None, type=int):
"""get field in the buffer. Little endian for integers.
Args:
byte_end (int): the end byte number of this field, which is specified in NVMe spec. Included.
byte_begin (int): the begin byte number of this field, which is specified in NVMe spec. It can be omitted if begin is the same as end when the field has only 1 byte. Included.
default: None, means only get 1 byte defined in byte_end
type (type): the type of the field. It should be int or str.
default: int, convert to integer python object
Rets:
(int or str): the data in the specified field
"""
if byte_begin is None:
byte_begin = byte_end
if type is int:
return int.from_bytes(self[byte_begin:byte_end+1], 'little')
else:
assert type is str, "identify data should be int or str"
return str(self[byte_begin:byte_end+1], "ascii").rstrip()
def __len__(self):
return self.size
def __repr__(self):
return '<buffer name: %s>' % str(self.name, "ascii")
def __getitem__(self, index):
if isinstance(index, slice):
return bytes([self[i] for i in range(*index.indices(len(self)))])
elif isinstance(index, int):
return (<unsigned char*>self.ptr)[index]
else:
raise TypeError()
def __setitem__(self, index, value):
if isinstance(index, slice):
start = 0 if index.start is None else index.start
for i, d in enumerate(value):
self[i+start] = d
elif isinstance(index, int):
(<unsigned char*>self.ptr)[index] = value
else:
raise TypeError()
def set_dsm_range(self, index, lba, lba_count):
"""set dsm ranges in the buffer, for dsm/deallocation (a.ka trim) commands
Args:
index (int): the index of the dsm range to set
lba (int): the start lba of the range
lba_count (int): the lba count of the range
"""
self[index*16:(index+1)*16] = struct.pack("<LLQ", 0, lba_count, lba)
cdef class Subsystem(object):
"""Subsystem class. Prefer to use fixture "subsystem" in test scripts.
Args:
nvme (Controller): the nvme controller object of that subsystem
"""
cdef Controller _nvme
def __cinit__(self, Controller nvme):
self._nvme = nvme
def power_cycle(self, sec=10):
"""power off and on in seconds
Args:
sec (int): the seconds between power off and power on
"""
# use S3/suspend to power off nvme device, and use rtc to power on again
logging.info("power off nvme device for %d seconds" % sec)
subprocess.call("sudo echo deep > /sys/power/mem_sleep", shell=True)
subprocess.call("sudo rtcwake -m mem -s %d 1>/dev/null 2>/dev/null" % sec, shell=True)
#subprocess.call("sudo echo mem >/sys/power/state", shell=True)
logging.info("power is back")
#reset driver
self._nvme._reinit()
def shutdown_notify(self, abrupt=False):
"""notify nvme subsystem a shutdown event through register cc.chn
Args:
abrupt (bool): it will be an abrupt shutdown (return immediately) or clean shutdown (wait shutdown completely)
"""
# cc.chn
cc = self._nvme[0x14]
if abrupt:
cc = cc | 0x8000
else:
cc = cc | 0x4000
self._nvme[0x14] = cc
# refer to spec 7.6.2, host delay is recommended
rtd3e = self._nvme.id_data(91, 88)
if rtd3e == 0:
rtd3e = 1000_000
time.sleep(rtd3e/1000_000)
# wait shutdown processing is complete
while (self._nvme[0x1c] & 0xc) != 0x8: pass
logging.debug("shutdown completed")
def reset(self):
"""reset the nvme subsystem through register nssr.nssrc"""
# nssr.nssrc: nvme subsystem reset
logging.debug("nvme subsystem reset by NSSR.NSSRC")
self._nvme[0x20] = 0x4E564D65 # "NVMe"
self._nvme._reinit()
cdef class Pcie(object):
"""Pcie class. Prefer to use fixture "pcie" in test scripts
Args:
nvme (Controller): the nvme controller object of that subsystem
"""
cdef d.pcie * _pcie
cdef Controller _nvme
def __cinit__(self, Controller nvme):
self._nvme = nvme
self._pcie = d.pcie_init(nvme._ctrlr)
if self._pcie is NULL:
raise SystemError()
def __getitem__(self, index):
"""access pcie config space by bytes."""
cdef unsigned char value
if isinstance(index, slice):
return [self[ii] for ii in range(index.stop)[index]]
elif isinstance(index, int):
d.pcie_cfg_read8(self._pcie, & value, index)
return value
else:
raise TypeError()
def __setitem__(self, index, value):
"""set pcie config space by bytes."""
if isinstance(index, int):
d.pcie_cfg_write8(self._pcie, value, index)
else:
raise TypeError()
def register(self, offset, byte_count):
"""access registers in pcie config space, and get its integer value.
Args:
offset (int): the offset (in bytes) of the register in the config space
byte_count (int): the size (in bytes) of the register
Rets:
(int): the value of the register
"""
assert byte_count <= 8, "support uptp 8-byte PCIe register access"
value = bytes(self[offset:offset+byte_count])
return int.from_bytes(value, 'little')
def cap_offset(self, cap_id):
"""get the offset of a capability
Args:
cap_id (int): capability id
Rets:
(int): the offset of the register
or None if the capability is not existed
"""
next_offset = self.register(0x34, 1)
while next_offset != 0:
value = self.register(next_offset, 2)
cid = value % 256
cap_offset = next_offset
next_offset = value>>8
if cid == cap_id:
return cap_offset
def reset(self):
"""reset this pcie device"""
vid = self.register(0, 2)
did = self.register(2, 2)
vdid = '%04x %04x' % (vid, did)
nvme = 'nvme'
spdk = 'uio_pci_generic'
bdf = '0000:' + self._nvme._bdf.decode('utf-8')
logging.debug("pci reset %s on %s" % (vdid, bdf))
# reset
subprocess.call('echo "%s" > "/sys/bus/pci/devices/%s/driver/remove_id" 2> /dev/null || true' % (vid, bdf), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/devices/%s/driver/unbind" 2> /dev/null || true' % (bdf, bdf), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/drivers/%s/new_id" 2> /dev/null || true' % (vid, nvme), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/drivers/%s/bind" 2> /dev/null || true' % (bdf, nvme), shell=True)
# config
subprocess.call('echo "%s" > "/sys/bus/pci/devices/%s/driver/remove_id" 2> /dev/null || true' % (vid, bdf), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/devices/%s/driver/unbind" 2> /dev/null || true' % (bdf, bdf), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/drivers/%s/new_id" 2> /dev/null || true' % (vid, spdk), shell=True)
subprocess.call('echo "%s" > "/sys/bus/pci/drivers/%s/bind" 2> /dev/null || true' % (bdf, spdk), shell=True)
# reset driver: namespace is init by every test, so no need reinit
self._nvme._reinit()
class NvmeEnumerateError(Exception):
pass
class NvmeDeletionError(Exception):
pass
cdef class Controller(object):
"""Controller class. Prefer to use fixture "nvme0" in test scripts.
Args:
addr (bytes): the bus/device/function address of the DUT, for example:
b'01:00.0' (PCIe BDF address);
b'127.0.0.1' (TCP IP address).
Example:
```python
>>> n = Controller(b'01:00.0')
>>> hex(n[0]) # CAP register
'0x28030fff'
>>> hex(n[0x1c]) # CSTS register
'0x1'
>>> n.id_data(23, 4, str)
'TW0546VPLOH007A6003Y'
>>> n.supports(0x18)
False
>>> n.supports(0x80)
True
>>> id_buf = Buffer()
>>> n.identify().waitdone()
>>> id_buf.dump(64)
buffer
00000000 a4 14 4b 1b 54 57 30 35 34 36 56 50 4c 4f 48 30 ..K.TW0546VPLOH0
00000010 30 37 41 36 30 30 33 59 43 41 33 2d 38 44 32 35 07A6003YCA3-8D25
00000020 36 2d 51 31 31 20 4e 56 4d 65 20 4c 49 54 45 4f 6-Q11 NVMe LITEO
00000030 4e 20 32 35 36 47 42 20 20 20 20 20 20 20 20 20 N 256GB
>>> n.cmdlog(2)
driver.c:1451:log_cmd_dump: *NOTICE*: dump qpair 0, latest tail in cmdlog: 1
driver.c:1462:log_cmd_dump: *NOTICE*: index 0, 2018-10-14 14:52:25.533708
nvme_qpair.c: 118:nvme_admin_qpair_print_command: *NOTICE*: IDENTIFY (06) sqid:0 cid:0 nsid:1 cdw10:00000001 cdw11:00000000
driver.c:1469:log_cmd_dump: *NOTICE*: index 0, 2018-10-14 14:52:25.534030
nvme_qpair.c: 306:nvme_qpair_print_completion: *NOTICE*: SUCCESS (00/00) sqid:0 cid:95 cdw0:0 sqhd:0142 p:1 m:0 dnr:0
driver.c:1462:log_cmd_dump: *NOTICE*: index 1, 1970-01-01 07:30:00.000000
nvme_qpair.c: 118:nvme_admin_qpair_print_command: *NOTICE*: DELETE IO SQ (00) sqid:0 cid:0 nsid:0 cdw10:00000000 cdw11:00000000
driver.c:1469:log_cmd_dump: *NOTICE*: index 1, 1970-01-01 07:30:00.000000
nvme_qpair.c: 306:nvme_qpair_print_completion: *NOTICE*: SUCCESS (00/00) sqid:0 cid:0 cdw0:0 sqhd:0000 p:0 m:0 dnr:0
```
"""
cdef d.ctrlr * _ctrlr
cdef char _bdf[20]
cdef Buffer hmb_buf
def __cinit__(self, addr):
strncpy(self._bdf, addr, strlen(addr)+1)
self._create()
def __dealloc__(self):
self._close()
def _reinit(self):
logging.debug("to re-initialize nvme: %s", self._bdf)
self._close()
self._create()
def _create(self):
self._ctrlr = d.nvme_init(self._bdf)
if self._ctrlr is NULL:
raise NvmeEnumerateError(f"fail to create the controller")
d.nvme_register_timeout_cb(self._ctrlr, timeout_driver_cb, _cTIMEOUT)
self.register_aer_cb(None)
logging.debug("nvme initialized: %s", self._bdf)
def enable_hmb(self):
# init hmb function
hmb_size = self.id_data(275, 272)
if hmb_size:
self.hmb_buf = Buffer(4096*hmb_size)
hmb_list_buf = Buffer(4096)
hmb_list_buf[0:8] = self.hmb_buf.phys_addr.to_bytes(8, 'little')
hmb_list_buf[8:12] = hmb_size.to_bytes(4, 'little')
hmb_list_phys = hmb_list_buf.phys_addr
self.setfeatures(0x0d, 1, hmb_size,
hmb_list_phys&0xffffffff,
hmb_list_phys>>32, 1).waitdone()
def disable_hmb(self):
self.setfeatures(0x0d, 0).waitdone()
@property
def mdts(self):
"""max data transfer size"""
page_size = (1UL<<(12+((self[4]>>16)&0xf)))
mdts_shift = self.id_data(77)
if mdts_shift == 0:
return 512*(1UL<<16)
else:
return page_size*(1UL<<mdts_shift)
def _close(self):
if self._ctrlr is not NULL:
ret = d.nvme_fini(self._ctrlr)
if ret != 0:
raise NvmeDeletionError(f"fail to close the controller, check if any qpair is not deleted: {ret}")
self._ctrlr = NULL
def __getitem__(self, index):
"""read nvme registers in BAR memory space by dwords."""
cdef unsigned int value
assert index % 4 == 0, "only support 4-byte aligned NVMe register read"
if isinstance(index, int):
d.nvme_get_reg32(self._ctrlr, index, & value)
if ~value == 0:
raise SystemError()
return value
else:
raise TypeError()
def __setitem__(self, index, value):
"""write nvme registers in BAR memory space by dwords."""
assert index % 4 == 0, "only support 4-byte aligned NVMe register write"
if isinstance(index, int):
d.nvme_set_reg32(self._ctrlr, index, value)
else:
raise TypeError()
def cmdlog(self, count=0):
"""print recent commands and their completions.
Args:
count (int): the number of commands to print
default: 0, to print the whole cmdlog
"""
d.log_cmd_dump_admin(self._ctrlr, count)
def reset(self):
"""controller reset: cc.en 1 => 0 => 1
Notices:
Test scripts should delete all io qpairs before reset!
"""
cc = self[0x14]
assert (cc & 1) == 1, "cc.en is not 1 before reset"
logging.debug("cc.en 1=>0")
self[0x14] = cc & 0xfffffffe
while (self[0x1c] & 1) == 1:
logging.debug("wait csts.rdy, 0x%x" % self[0x1c])
logging.debug("cc.en 0=>1")
self[0x14] = cc | 1
while (self[0x1c] & 1) == 0:
logging.debug("wait csts.rdy, 0x%x" % self[0x1c])
# reset driver
self._reinit()
def cmdname(self, opcode):
"""get the name of the admin command
Args:
opcode (int): the opcode of the admin command
Rets:
(str): the command name
"""
assert opcode < 256
name = d.cmd_name(opcode, 0)
return name.decode('ascii')
def supports(self, opcode):
"""check if the admin command is supported
Args:
opcode (int): the opcode of the admin command
Rets:
(bool): if the command is supported
"""
assert opcode < 256*2 # *2 for nvm command set
logpage_buf = Buffer(4096)
self.getlogpage(5, logpage_buf).waitdone()
return logpage_buf.data((opcode+1)*4-1, opcode*4) != 0
def waitdone(self, expected=1):
"""sync until expected commands completion
Args:
expected (int): expected commands to complete
default: 1
Notices:
Do not call this function in commands callback functions.
"""
reaped = 0
global _reentry_flag
assert _reentry_flag is False, f"cannot re-entry waitdone() functions which may be caused by waitdone in callback functions, {_reentry_flag}"
_reentry_flag = True
logging.debug("to reap %d admin commands" % expected)
# some admin commands need long timeout limit, like: format,
signal.alarm(_cTIMEOUT_wrap)
while reaped < expected:
# wait admin Q pair done
reaped += d.nvme_wait_completion_admin(self._ctrlr)
# Since signals are delivered asynchronously at unpredictable
# times, it is problematic to run any meaningful code directly
# from the signal handler. Therefore, Python queues incoming
# signals. The queue is processed later as part of the interpreter
# loop. If your code is fully compiled, interpreter loop is never
# executed and Python has no chance to check and run queued signal
# handlers.
# - from: https://stackoverflow.com/questions/16769870/cython-python-and-keyboardinterrupt-ignored
PyErr_CheckSignals()
signal.alarm(0)
# in admin queue, may reap more than expected, because driver
# will get admin CQ as many as possible
assert reaped >= expected, \
"not reap the exact completions! reaped %d, expected %d" % (reaped, expected)
_reentry_flag = False
def abort(self, cid, sqid=0, cb=None):
"""abort admin commands
Args:
cid (int): command id of the command to be aborted
sqid (int): sq id of the command to be aborted
default: 0, to abort the admin command
cb (function): callback function called at completion