Skip to content

Commit 4e45969

Browse files
author
Swapnil Dinkar
committed
add ebsnvme script
this patch adds the ebsnvme script, which can be used to fetch ebs info from ebs nvme devices. Signed-off-by: Swapnil Dinkar <[email protected]>
1 parent 09b53f2 commit 4e45969

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed

ebsnvme

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved.
4+
#
5+
# Licensed under the MIT License. See the LICENSE accompanying this file
6+
# for the specific language governing permissions and limitations under
7+
# the License.
8+
9+
"""
10+
Usage:
11+
Reads EBS information from EBS NVMe device
12+
"""
13+
14+
from __future__ import print_function
15+
import argparse
16+
from ctypes import Structure, Array, c_uint8, c_uint16, c_uint32, c_uint64, \
17+
c_char, addressof, sizeof, byref
18+
from fcntl import ioctl
19+
import json
20+
import os
21+
import signal
22+
import sys
23+
import time
24+
25+
NVME_ADMIN_IDENTIFY = 0x06
26+
NVME_GET_LOG_PAGE = 0x02
27+
NVME_IOCTL_ADMIN_CMD = 0xC0484E41
28+
29+
AMZN_NVME_EBS_MN = "Amazon Elastic Block Store"
30+
AMZN_NVME_STATS_LOGPAGE_ID = 0xD0
31+
AMZN_NVME_STATS_MAGIC = 0x3C23B510
32+
AMZN_NVME_VID = 0x1D0F
33+
34+
35+
class structure_dict_mixin:
36+
def to_dict(self):
37+
return {
38+
field[0]: getattr(self, field[0])
39+
for field in self._fields_
40+
if not field[0].startswith("_") and
41+
not isinstance(getattr(self, field[0]), (Structure, Array))
42+
}
43+
44+
45+
class nvme_admin_command(Structure):
46+
_pack_ = 1
47+
_fields_ = [("opcode", c_uint8),
48+
("flags", c_uint8),
49+
("cid", c_uint16),
50+
("nsid", c_uint32),
51+
("_reserved0", c_uint64),
52+
("mptr", c_uint64),
53+
("addr", c_uint64),
54+
("mlen", c_uint32),
55+
("alen", c_uint32),
56+
("cdw10", c_uint32),
57+
("cdw11", c_uint32),
58+
("cdw12", c_uint32),
59+
("cdw13", c_uint32),
60+
("cdw14", c_uint32),
61+
("cdw15", c_uint32),
62+
("_reserved1", c_uint64)]
63+
64+
65+
class nvme_identify_controller_amzn_vs(Structure):
66+
_pack_ = 1
67+
_fields_ = [("bdev", c_char * 32), # block device name
68+
("_reserved0", c_char * (1024 - 32))]
69+
70+
71+
class nvme_identify_controller_psd(Structure):
72+
_pack_ = 1
73+
_fields_ = [("mp", c_uint16), # maximum power
74+
("_reserved0", c_uint16),
75+
("enlat", c_uint32), # entry latency
76+
("exlat", c_uint32), # exit latency
77+
("rrt", c_uint8), # relative read throughput
78+
("rrl", c_uint8), # relative read latency
79+
("rwt", c_uint8), # relative write throughput
80+
("rwl", c_uint8), # relative write latency
81+
("_reserved1", c_char * 16)]
82+
83+
84+
class nvme_identify_controller(Structure):
85+
_pack_ = 1
86+
_fields_ = [("vid", c_uint16), # PCI Vendor ID
87+
("ssvid", c_uint16), # PCI Subsystem Vendor ID
88+
("sn", c_char * 20), # Serial Number
89+
("mn", c_char * 40), # Module Number
90+
("fr", c_char * 8), # Firmware Revision
91+
("rab", c_uint8), # Recommend Arbitration Burst
92+
("ieee", c_uint8 * 3), # IEEE OUI Identifier
93+
("mic", c_uint8), # Multi-Interface Capabilities
94+
("mdts", c_uint8), # Maximum Data Transfer Size
95+
("_reserved0", c_uint8 * (256 - 78)),
96+
("oacs", c_uint16), # Optional Admin Command Support
97+
("acl", c_uint8), # Abort Command Limit
98+
("aerl", c_uint8), # Asynchronous Event Request Limit
99+
("frmw", c_uint8), # Firmware Updates
100+
("lpa", c_uint8), # Log Page Attributes
101+
("elpe", c_uint8), # Error Log Page Entries
102+
("npss", c_uint8), # Number of Power States Support
103+
("avscc", c_uint8), # Admin Vendor Specific Command Configuration # noqa
104+
("_reserved1", c_uint8 * (512 - 265)),
105+
("sqes", c_uint8), # Submission Queue Entry Size
106+
("cqes", c_uint8), # Completion Queue Entry Size
107+
("_reserved2", c_uint16),
108+
("nn", c_uint32), # Number of Namespaces
109+
("oncs", c_uint16), # Optional NVM Command Support
110+
("fuses", c_uint16), # Fused Operation Support
111+
("fna", c_uint8), # Format NVM Attributes
112+
("vwc", c_uint8), # Volatile Write Cache
113+
("awun", c_uint16), # Atomic Write Unit Normal
114+
("awupf", c_uint16), # Atomic Write Unit Power Fail
115+
("nvscc", c_uint8), # NVM Vendor Specific Command Configuration # noqa
116+
("_reserved3", c_uint8 * (704 - 531)),
117+
("_reserved4", c_uint8 * (2048 - 704)),
118+
("psd", nvme_identify_controller_psd * 32), # Power State Descriptor # noqa
119+
("vs", nvme_identify_controller_amzn_vs)] # Vendor Specific
120+
121+
122+
class nvme_histogram_bin(Structure, structure_dict_mixin):
123+
_pack_ = 1
124+
_fields_ = [("lower", c_uint64),
125+
("upper", c_uint64),
126+
("count", c_uint32),
127+
("_reserved0", c_uint32)]
128+
129+
def to_human_readable(self):
130+
print("[{0.lower:<8} - {0.upper:<8}] => {0.count}".format(self))
131+
132+
133+
class ebs_nvme_histogram(Structure, structure_dict_mixin):
134+
_pack_ = 1
135+
_fields_ = [("num_bins", c_uint64),
136+
("bins", nvme_histogram_bin * 64)]
137+
138+
def to_dict(self):
139+
dict = super(ebs_nvme_histogram, self).to_dict()
140+
dict["bins"] = [self.bins[i].to_dict() for i in range(self.num_bins)]
141+
142+
return dict
143+
144+
def to_human_readable(self):
145+
print("Number of bins: {0}".format(self.num_bins))
146+
147+
print("=================================")
148+
print("Lower Upper IO Count")
149+
print("=================================")
150+
151+
for i in range(self.num_bins):
152+
self.bins[i].to_human_readable()
153+
154+
155+
class nvme_get_amzn_stats_logpage(Structure, structure_dict_mixin):
156+
_pack_ = 1
157+
_fields_ = [("_magic", c_uint32),
158+
("_reserved0", c_char * 4),
159+
("total_read_ops", c_uint64), # total number of read operations
160+
("total_write_ops", c_uint64), # total number of write operations
161+
("total_read_bytes", c_uint64), # total bytes read
162+
("total_write_bytes", c_uint64), # total bytes written
163+
("total_read_time", c_uint64), # total time spent on read operations (in microseconds)
164+
("total_write_time", c_uint64), # total time spent on write operations (in microseconds)
165+
("ebs_volume_performance_exceeded_iops", c_uint64), # time EBS volume IOPS limit was exceeded (in microseconds)
166+
("ebs_volume_performance_exceeded_tp", c_uint64), # time EBS volume throughput limit was exceeded (in microseconds)
167+
("ec2_instance_ebs_performance_exceeded_iops", c_uint64), # time EC2 instance EBS IOPS limit was exceeded (in microseconds)
168+
("ec2_instance_ebs_performance_exceeded_tp", c_uint64), # time EC2 instance EBS throughput limit was exceeded (in microseconds)
169+
("volume_queue_length", c_uint64), # current volume queue length
170+
("_reserved1", c_char * 416),
171+
("read_io_latency_histogram", ebs_nvme_histogram), # histogram of read I/O latencies (in microseconds)
172+
("write_io_latency_histogram", ebs_nvme_histogram), # histogram of write I/O latencies (in microseconds)
173+
("_reserved2", c_char * 496)]
174+
175+
def to_dict(self):
176+
dict = super(nvme_get_amzn_stats_logpage, self).to_dict()
177+
dict["read_io_latency_histogram"] = self.read_io_latency_histogram.to_dict()
178+
dict["write_io_latency_histogram"] = self.write_io_latency_histogram.to_dict()
179+
180+
return dict
181+
182+
def to_json(self):
183+
print(json.dumps(self.to_dict()))
184+
185+
def to_human_readable(self):
186+
print("Total Ops")
187+
print(" Read: {0}".format(self.total_read_ops))
188+
print(" Write: {0}".format(self.total_write_ops))
189+
print("Total Bytes")
190+
print(" Read: {0}".format(self.total_read_bytes))
191+
print(" Write: {0}".format(self.total_write_bytes))
192+
print("Total Time (us)")
193+
print(" Read: {0}".format(self.total_read_time))
194+
print(" Write: {0}".format(self.total_write_time))
195+
196+
print("EBS Volume Performance Exceeded (us)")
197+
print(" IOPS: {0}".format(self.ebs_volume_performance_exceeded_iops))
198+
print(" Throughput: {0}".format(self.ebs_volume_performance_exceeded_tp))
199+
print("EC2 Instance EBS Performance Exceeded (us)")
200+
print(" IOPS: {0}".format(self.ec2_instance_ebs_performance_exceeded_iops))
201+
print(" Throughput: {0}".format(self.ec2_instance_ebs_performance_exceeded_tp))
202+
203+
print("Queue Length (point in time): {0} \n".format(self.volume_queue_length))
204+
205+
print("Read IO Latency Histogram (us)")
206+
self.read_io_latency_histogram.to_human_readable()
207+
208+
print("\nWrite IO Latency Histogram (us)")
209+
self.write_io_latency_histogram.to_human_readable()
210+
211+
212+
class ebs_nvme_device:
213+
def __init__(self, device):
214+
self.device = device
215+
216+
def _nvme_ioctl(self, admin_cmd):
217+
218+
with open(self.device, "r") as dev:
219+
try:
220+
ioctl(dev, NVME_IOCTL_ADMIN_CMD, admin_cmd)
221+
except (OSError) as err:
222+
print("Failed to issue nvme cmd, err: ", err)
223+
sys.exit(1)
224+
225+
226+
class ebs_nvme_device_stats(ebs_nvme_device):
227+
def _query_stats_from_device(self):
228+
stats = nvme_get_amzn_stats_logpage()
229+
admin_cmd = nvme_admin_command(
230+
opcode=NVME_GET_LOG_PAGE,
231+
addr=addressof(stats),
232+
alen=sizeof(stats),
233+
nsid=1,
234+
cdw10=(AMZN_NVME_STATS_LOGPAGE_ID | (1024 << 16))
235+
)
236+
self._nvme_ioctl(admin_cmd)
237+
238+
if stats._magic != AMZN_NVME_STATS_MAGIC:
239+
raise TypeError("[ERROR] Not an EBS device: {0}".format(self.device))
240+
241+
return stats
242+
243+
def _get_stats_diff(self):
244+
curr = self._query_stats_from_device()
245+
if self.prev is None:
246+
self.prev = curr
247+
return curr
248+
249+
diff = nvme_get_amzn_stats_logpage()
250+
diff.volume_queue_length = curr.volume_queue_length
251+
252+
for field, _ in nvme_get_amzn_stats_logpage._fields_:
253+
if field.startswith('_') or field == "volume_queue_length":
254+
continue
255+
if isinstance(getattr(self.prev, field), (int)):
256+
setattr(diff, field, getattr(curr, field) - getattr(self.prev, field))
257+
258+
for histogram_field in ['read_io_latency_histogram', 'write_io_latency_histogram']:
259+
self._calculate_histogram_diff(diff, curr, self.prev, histogram_field)
260+
261+
self.prev = curr
262+
return diff
263+
264+
def _calculate_histogram_diff(self, diff, curr, prev, histogram_field):
265+
prev_hist = getattr(prev, histogram_field)
266+
curr_hist = getattr(curr, histogram_field)
267+
diff_hist = getattr(diff, histogram_field)
268+
269+
diff_hist.num_bins = curr_hist.num_bins
270+
for i in range(diff_hist.num_bins):
271+
diff_hist.bins[i].lower = curr_hist.bins[i].lower
272+
diff_hist.bins[i].upper = curr_hist.bins[i].upper
273+
diff_hist.bins[i].count = curr_hist.bins[i].count - prev_hist.bins[i].count
274+
275+
def _print_stats(self, stats, json_format=False):
276+
if json_format:
277+
print(json.dumps(stats.to_dict()))
278+
else:
279+
stats.to_human_readable()
280+
281+
def _signal_handler(self, sig, frame):
282+
sys.exit(0)
283+
284+
def get_stats(self, interval=0, json_format=False):
285+
if interval > 0:
286+
print("Polling EBS stats every {0} sec(s); press Ctrl+C to stop".format(interval))
287+
signal.signal(signal.SIGINT, self._signal_handler)
288+
self.prev = None
289+
290+
while True:
291+
self._print_stats(self._get_stats_diff(), json_format)
292+
time.sleep(interval)
293+
print("\n")
294+
295+
else:
296+
self._print_stats(self._query_stats_from_device(), json_format)
297+
298+
class ebs_nvme_device_id(ebs_nvme_device):
299+
def get_id(self, volume=False, block_dev=False, udev=False):
300+
id_ctrl = self._query_id_ctrl_from_device()
301+
302+
if not (volume or block_dev or udev):
303+
print("Volume ID: {0}".format(self._get_volume_id(id_ctrl)))
304+
print(self._get_block_device(id_ctrl))
305+
else:
306+
if volume:
307+
print("Volume ID: {0}".format(self._get_volume_id(id_ctrl)))
308+
if block_dev or udev:
309+
print(self._get_block_device(id_ctrl, udev))
310+
311+
def _query_id_ctrl_from_device(self):
312+
id_ctrl = nvme_identify_controller()
313+
admin_cmd = nvme_admin_command(
314+
opcode=NVME_ADMIN_IDENTIFY,
315+
addr=addressof(id_ctrl),
316+
alen=sizeof(id_ctrl),
317+
cdw10=1
318+
)
319+
self._nvme_ioctl(admin_cmd)
320+
321+
if id_ctrl.vid != AMZN_NVME_VID or id_ctrl.mn.decode().strip() != AMZN_NVME_EBS_MN:
322+
raise TypeError("[ERROR] Not an EBS device: ", self.device)
323+
324+
return id_ctrl
325+
326+
def _get_volume_id(self, id_ctrl):
327+
vol = id_ctrl.sn.decode()
328+
if vol.startswith("vol") and vol[3] != "-":
329+
vol = "vol-" + vol[3:]
330+
return vol
331+
332+
def _get_block_device(self, id_ctrl, stripped=False):
333+
dev = id_ctrl.vs.bdev.decode().strip()
334+
if stripped and dev.startswith("/dev/"):
335+
dev = dev[5:]
336+
return dev
337+
338+
339+
if __name__ == "__main__":
340+
341+
# check if the script is being called as ebsnvme-id
342+
if os.path.basename(sys.argv[0]) == 'ebsnvme-id':
343+
sys.argv.insert(1, 'id')
344+
345+
parser = argparse.ArgumentParser(description="Reads EBS information from EBS NVMe devices.")
346+
cmd_parser = parser.add_subparsers(dest="cmd", help="Available commands")
347+
cmd_parser.required = True
348+
349+
stats_parser = cmd_parser.add_parser("stats", help="Get EBS NVMe stats")
350+
stats_parser.add_argument("device", help="Device to query")
351+
stats_parser.required = True
352+
stats_parser.add_argument("-j", "--json", action="store_true",
353+
help="Output in json format")
354+
stats_parser.add_argument("-i", "--interval", type=int, default=0,
355+
help='Interval in seconds to poll ebs stats')
356+
357+
id_parser = cmd_parser.add_parser("id", help="Display id options")
358+
id_parser.add_argument("device", help="Device to query")
359+
id_parser.required = True
360+
id_parser.add_argument("-v", "--volume", action="store_true",
361+
help="Return volume-id")
362+
id_parser.add_argument("-b", "--block-dev", action="store_true",
363+
help="Return block device mapping")
364+
id_parser.add_argument("-u", "--udev", action="store_true",
365+
help="Output data in format suitable for udev rules")
366+
367+
args = parser.parse_args()
368+
369+
try:
370+
if args.cmd == "stats":
371+
stats = ebs_nvme_device_stats(args.device)
372+
stats.get_stats(interval=args.interval, json_format=args.json)
373+
374+
elif args.cmd == "id":
375+
id_info = ebs_nvme_device_id(args.device)
376+
id_info.get_id(volume=args.volume, block_dev=args.block_dev, udev=args.udev)
377+
except (IOError, TypeError) as err:
378+
print(err, file=sys.stderr)
379+
sys.exit(1)

0 commit comments

Comments
 (0)