Skip to content

Commit 066b044

Browse files
Merge pull request #40 from swapnildinkar/ebsnvme-main
add ebsnvme script
2 parents 09b53f2 + 4e45969 commit 066b044

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)