master
py 698 lines 21.3 KB
Raw
1 #!/usr/bin/env python3
2 #
3 # pylint: disable=C0103,E0213,E1135,E1136,E1137,R0902,R0903,R0912,R0913,R0917
4 # SPDX-License-Identifier: GPL-2.0-or-later
5 #
6 # Copyright (C) 2024-2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
7
8 """
9 Helper classes to be used by ghes_inject command classes.
10 """
11
12 import json
13 import sys
14
15 from datetime import datetime
16
17 try:
18 from qemu.qmp.legacy import QEMUMonitorProtocol
19 except ModuleNotFoundError as exc:
20 print(f"Module '{exc.name}' not found.", file=sys.stderr)
21 print(f"Try $builddir/run {' '.join(sys.argv)}", file=sys.stderr)
22 sys.exit(1)
23
24 from base64 import b64encode
25
26 class util:
27 """
28 Ancillary functions to deal with bitmaps, parse arguments,
29 generate GUID and encode data on a bytearray buffer.
30 """
31
32 #
33 # Helper routines to handle multiple choice arguments
34 #
35 def get_choice(name, value, choices, suffixes=None, bitmask=True):
36 """Produce a list from multiple choice argument"""
37
38 new_values = 0
39
40 if not value:
41 return new_values
42
43 for val in value.split(","):
44 val = val.lower()
45
46 if suffixes:
47 for suffix in suffixes:
48 val = val.removesuffix(suffix)
49
50 if val not in choices.keys():
51 if suffixes:
52 for suffix in suffixes:
53 if val + suffix in choices.keys():
54 val += suffix
55 break
56
57 if val not in choices.keys():
58 sys.exit(f"Error on '{name}': choice '{val}' is invalid.")
59
60 val = choices[val]
61
62 if bitmask:
63 new_values |= val
64 else:
65 if new_values:
66 sys.exit(f"Error on '{name}': only one value is accepted.")
67
68 new_values = val
69
70 return new_values
71
72 def get_array(name, values, max_val=None):
73 """Add numbered hashes from integer lists into an array"""
74
75 array = []
76
77 for value in values:
78 for val in value.split(","):
79 try:
80 val = int(val, 0)
81 except ValueError:
82 sys.exit(f"Error on '{name}': {val} is not an integer")
83
84 if val < 0:
85 sys.exit(f"Error on '{name}': {val} is not unsigned")
86
87 if max_val and val > max_val:
88 sys.exit(f"Error on '{name}': {val} is too little")
89
90 array.append(val)
91
92 return array
93
94 def get_mult_array(mult, name, values, allow_zero=False, max_val=None):
95 """Add numbered hashes from integer lists"""
96
97 if not allow_zero:
98 if not values:
99 return
100 else:
101 if values is None:
102 return
103
104 if not values:
105 i = 0
106 if i not in mult:
107 mult[i] = {}
108
109 mult[i][name] = []
110 return
111
112 i = 0
113 for value in values:
114 for val in value.split(","):
115 try:
116 val = int(val, 0)
117 except ValueError:
118 sys.exit(f"Error on '{name}': {val} is not an integer")
119
120 if val < 0:
121 sys.exit(f"Error on '{name}': {val} is not unsigned")
122
123 if max_val and val > max_val:
124 sys.exit(f"Error on '{name}': {val} is too little")
125
126 if i not in mult:
127 mult[i] = {}
128
129 if name not in mult[i]:
130 mult[i][name] = []
131
132 mult[i][name].append(val)
133
134 i += 1
135
136
137 def get_mult_choices(mult, name, values, choices,
138 suffixes=None, allow_zero=False):
139 """Add numbered hashes from multiple choice arguments"""
140
141 if not allow_zero:
142 if not values:
143 return
144 else:
145 if values is None:
146 return
147
148 i = 0
149 for val in values:
150 new_values = util.get_choice(name, val, choices, suffixes)
151
152 if i not in mult:
153 mult[i] = {}
154
155 mult[i][name] = new_values
156 i += 1
157
158
159 def get_mult_int(mult, name, values, allow_zero=False):
160 """Add numbered hashes from integer arguments"""
161 if not allow_zero:
162 if not values:
163 return
164 else:
165 if values is None:
166 return
167
168 i = 0
169 for val in values:
170 try:
171 val = int(val, 0)
172 except ValueError:
173 sys.exit(f"Error on '{name}': {val} is not an integer")
174
175 if val < 0:
176 sys.exit(f"Error on '{name}': {val} is not unsigned")
177
178 if i not in mult:
179 mult[i] = {}
180
181 mult[i][name] = val
182 i += 1
183
184
185 #
186 # Data encode helper functions
187 #
188 def bit(b):
189 """Simple macro to define a bit on a bitmask"""
190 return 1 << b
191
192
193 def data_add(data, value, num_bytes):
194 """Adds bytes from value inside a bitarray"""
195
196 data.extend(value.to_bytes(num_bytes, byteorder="little")) # pylint: disable=E1101
197
198 def dump_bytearray(name, data):
199 """Does an hexdump of a byte array, grouping in bytes"""
200
201 print(f"{name} ({len(data)} bytes):")
202
203 for ln_start in range(0, len(data), 16):
204 ln_end = min(ln_start + 16, len(data))
205 print(f" {ln_start:08x} ", end="")
206 for i in range(ln_start, ln_end):
207 print(f"{data[i]:02x} ", end="")
208 for i in range(ln_end, ln_start + 16):
209 print(" ", end="")
210 print(" ", end="")
211 for i in range(ln_start, ln_end):
212 if data[i] >= 32 and data[i] < 127:
213 print(chr(data[i]), end="")
214 else:
215 print(".", end="")
216
217 print()
218 print()
219
220 def time(string):
221 """Handle BCD timestamps used on Generic Error Data Block"""
222
223 time = None
224
225 # Formats to be used when parsing time stamps
226 formats = [
227 "%Y-%m-%d %H:%M:%S",
228 ]
229
230 if string == "now":
231 time = datetime.now()
232
233 if time is None:
234 for fmt in formats:
235 try:
236 time = datetime.strptime(string, fmt)
237 break
238 except ValueError:
239 pass
240
241 if time is None:
242 raise ValueError("Invalid time format")
243
244 return time
245
246 class guid:
247 """
248 Simple class to handle GUID fields.
249 """
250
251 def __init__(self, time_low, time_mid, time_high, nodes):
252 """Initialize a GUID value"""
253
254 assert len(nodes) == 8
255
256 self.time_low = time_low
257 self.time_mid = time_mid
258 self.time_high = time_high
259 self.nodes = nodes
260
261 @classmethod
262 def UUID(cls, guid_str):
263 """Initialize a GUID using a string on its standard format"""
264
265 if len(guid_str) != 36:
266 print("Size not 36")
267 raise ValueError('Invalid GUID size')
268
269 # It is easier to parse without separators. So, drop them
270 guid_str = guid_str.replace('-', '')
271
272 if len(guid_str) != 32:
273 print("Size not 32", guid_str, len(guid_str))
274 raise ValueError('Invalid GUID hex size')
275
276 time_low = 0
277 time_mid = 0
278 time_high = 0
279 nodes = []
280
281 for i in reversed(range(16, 32, 2)):
282 h = guid_str[i:i + 2]
283 value = int(h, 16)
284 nodes.insert(0, value)
285
286 time_high = int(guid_str[12:16], 16)
287 time_mid = int(guid_str[8:12], 16)
288 time_low = int(guid_str[0:8], 16)
289
290 return cls(time_low, time_mid, time_high, nodes)
291
292 def __str__(self):
293 """Output a GUID value on its default string representation"""
294
295 clock = self.nodes[0] << 8 | self.nodes[1]
296
297 node = 0
298 for i in range(2, len(self.nodes)):
299 node = node << 8 | self.nodes[i]
300
301 s = f"{self.time_low:08x}-{self.time_mid:04x}-"
302 s += f"{self.time_high:04x}-{clock:04x}-{node:012x}"
303 return s
304
305 def to_bytes(self):
306 """Output a GUID value in bytes"""
307
308 data = bytearray()
309
310 util.data_add(data, self.time_low, 4)
311 util.data_add(data, self.time_mid, 2)
312 util.data_add(data, self.time_high, 2)
313 data.extend(bytearray(self.nodes))
314
315 return data
316
317 class qmp:
318 """
319 Opens a connection and send/receive QMP commands.
320 """
321
322 def send_cmd(self, command, args=None, may_open=False, return_error=True):
323 """Send a command to QMP, optinally opening a connection"""
324
325 if may_open:
326 self._connect()
327 elif not self.connected:
328 return False
329
330 msg = { 'execute': command }
331 if args:
332 msg['arguments'] = args
333
334 try:
335 obj = self.qmp_monitor.cmd_obj(msg)
336 # Can we use some other exception class here?
337 except Exception as e: # pylint: disable=W0718
338 print(f"Command: {command}")
339 print(f"Failed to inject error: {e}.")
340 return None
341
342 if "return" in obj:
343 if isinstance(obj.get("return"), dict):
344 if obj["return"]:
345 return obj["return"]
346 return "OK"
347
348 return obj["return"]
349
350 if isinstance(obj.get("error"), dict):
351 error = obj["error"]
352 if return_error:
353 print(f"Command: {msg}")
354 print(f'{error["class"]}: {error["desc"]}')
355 else:
356 print(json.dumps(obj))
357
358 return None
359
360 def _close(self):
361 """Shutdown and close the socket, if opened"""
362 if not self.connected:
363 return
364
365 self.qmp_monitor.close()
366 self.connected = False
367
368 def _connect(self):
369 """Connect to a QMP TCP/IP port, if not connected yet"""
370
371 if self.connected:
372 return True
373
374 try:
375 self.qmp_monitor.connect(negotiate=True)
376 except ConnectionError:
377 sys.exit(f"Can't connect to QMP host {self.host}:{self.port}")
378
379 self.connected = True
380
381 return True
382
383 BLOCK_STATUS_BITS = {
384 "uncorrectable": util.bit(0),
385 "correctable": util.bit(1),
386 "multi-uncorrectable": util.bit(2),
387 "multi-correctable": util.bit(3),
388 }
389
390 ERROR_SEVERITY = {
391 "recoverable": 0,
392 "fatal": 1,
393 "corrected": 2,
394 "none": 3,
395 }
396
397 VALIDATION_BITS = {
398 "fru-id": util.bit(0),
399 "fru-text": util.bit(1),
400 "timestamp": util.bit(2),
401 }
402
403 GEDB_FLAGS_BITS = {
404 "recovered": util.bit(0),
405 "prev-error": util.bit(1),
406 "simulated": util.bit(2),
407 }
408
409 GENERIC_DATA_SIZE = 72
410
411 def argparse(parser):
412 """Prepare a parser group to query generic error data"""
413
414 block_status_bits = ",".join(qmp.BLOCK_STATUS_BITS.keys())
415 error_severity_enum = ",".join(qmp.ERROR_SEVERITY.keys())
416 validation_bits = ",".join(qmp.VALIDATION_BITS.keys())
417 gedb_flags_bits = ",".join(qmp.GEDB_FLAGS_BITS.keys())
418
419 g_gen = parser.add_argument_group("Generic Error Data") # pylint: disable=E1101
420 g_gen.add_argument("--block-status",
421 help=f"block status bits: {block_status_bits}")
422 g_gen.add_argument("--raw-data", nargs="+",
423 help="Raw data inside the Error Status Block")
424 g_gen.add_argument("--error-severity", "--severity",
425 help=f"error severity: {error_severity_enum}")
426 g_gen.add_argument("--gen-err-valid-bits",
427 "--generic-error-validation-bits",
428 help=f"validation bits: {validation_bits}")
429 g_gen.add_argument("--fru-id", type=guid.UUID,
430 help="GUID representing a physical device")
431 g_gen.add_argument("--fru-text",
432 help="ASCII string identifying the FRU hardware")
433 g_gen.add_argument("--timestamp", type=util.time,
434 help="Time when the error info was collected")
435 g_gen.add_argument("--precise", "--precise-timestamp",
436 action='store_true',
437 help="Marks the timestamp as precise if --timestamp is used")
438 g_gen.add_argument("--gedb-flags",
439 help=f"General Error Data Block flags: {gedb_flags_bits}")
440
441 def set_args(self, args):
442 """Set the arguments optionally defined via self.argparse()"""
443
444 if args.block_status:
445 self.block_status = util.get_choice(name="block-status",
446 value=args.block_status,
447 choices=self.BLOCK_STATUS_BITS,
448 bitmask=False)
449 if args.raw_data:
450 self.raw_data = util.get_array("raw-data", args.raw_data,
451 max_val=255)
452 print(self.raw_data)
453
454 if args.error_severity:
455 self.error_severity = util.get_choice(name="error-severity",
456 value=args.error_severity,
457 choices=self.ERROR_SEVERITY,
458 bitmask=False)
459
460 if args.fru_id:
461 self.fru_id = args.fru_id.to_bytes()
462 if not args.gen_err_valid_bits:
463 self.validation_bits |= self.VALIDATION_BITS["fru-id"]
464
465 if args.fru_text:
466 text = bytearray(args.fru_text.encode('ascii'))
467 if len(text) > 20:
468 sys.exit("FRU text is too big to fit")
469
470 self.fru_text = text
471 if not args.gen_err_valid_bits:
472 self.validation_bits |= self.VALIDATION_BITS["fru-text"]
473
474 if args.timestamp:
475 time = args.timestamp
476 century = int(time.year / 100)
477
478 bcd = bytearray()
479 util.data_add(bcd, (time.second // 10) << 4 | (time.second % 10), 1)
480 util.data_add(bcd, (time.minute // 10) << 4 | (time.minute % 10), 1)
481 util.data_add(bcd, (time.hour // 10) << 4 | (time.hour % 10), 1)
482
483 if args.precise:
484 util.data_add(bcd, 1, 1)
485 else:
486 util.data_add(bcd, 0, 1)
487
488 util.data_add(bcd, (time.day // 10) << 4 | (time.day % 10), 1)
489 util.data_add(bcd, (time.month // 10) << 4 | (time.month % 10), 1)
490 util.data_add(bcd,
491 ((time.year % 100) // 10) << 4 | (time.year % 10), 1)
492 util.data_add(bcd, ((century % 100) // 10) << 4 | (century % 10), 1)
493
494 self.timestamp = bcd
495 if not args.gen_err_valid_bits:
496 self.validation_bits |= self.VALIDATION_BITS["timestamp"]
497
498 if args.gen_err_valid_bits:
499 self.validation_bits = util.get_choice(name="validation",
500 value=args.gen_err_valid_bits,
501 choices=self.VALIDATION_BITS)
502
503 def __init__(self, host, port, debug=False):
504 """Initialize variables used by the QMP send logic"""
505
506 self.connected = False
507 self.host = host
508 self.port = port
509 self.debug = debug
510
511 # ACPI 6.1: 18.3.2.7.1 Generic Error Data: Generic Error Status Block
512 self.block_status = self.BLOCK_STATUS_BITS["uncorrectable"]
513 self.raw_data = []
514 self.error_severity = self.ERROR_SEVERITY["recoverable"]
515
516 # ACPI 6.1: 18.3.2.7.1 Generic Error Data: Generic Error Data Entry
517 self.validation_bits = 0
518 self.flags = 0
519 self.fru_id = bytearray(16)
520 self.fru_text = bytearray(20)
521 self.timestamp = bytearray(8)
522
523 self.qmp_monitor = QEMUMonitorProtocol(address=(self.host, self.port))
524
525 #
526 # Socket QMP send command
527 #
528 def send_cper_raw(self, cper_data):
529 """Send a raw CPER data to QEMU though QMP TCP socket"""
530
531 data = b64encode(bytes(cper_data)).decode('ascii')
532
533 cmd_arg = {
534 'cper': data
535 }
536
537 self._connect()
538
539 if self.send_cmd("inject-ghes-v2-error", cmd_arg):
540 print("Error injected.")
541
542 def send_cper(self, notif_type, payload):
543 """Send commands to QEMU though QMP TCP socket"""
544
545 # Fill CPER record header
546
547 # NOTE: bits 4 to 13 of block status contain the number of
548 # data entries in the data section. This is currently unsupported.
549
550 cper_length = len(payload)
551 data_length = cper_length + len(self.raw_data) + self.GENERIC_DATA_SIZE
552
553 # Generic Error Data Entry
554 gede = bytearray()
555
556 gede.extend(notif_type.to_bytes())
557 util.data_add(gede, self.error_severity, 4)
558 util.data_add(gede, 0x300, 2)
559 util.data_add(gede, self.validation_bits, 1)
560 util.data_add(gede, self.flags, 1)
561 util.data_add(gede, cper_length, 4)
562 gede.extend(self.fru_id)
563 gede.extend(self.fru_text)
564 gede.extend(self.timestamp)
565
566 # Generic Error Status Block
567 gebs = bytearray()
568
569 if self.raw_data:
570 raw_data_offset = len(gebs)
571 else:
572 raw_data_offset = 0
573
574 util.data_add(gebs, self.block_status, 4)
575 util.data_add(gebs, raw_data_offset, 4)
576 util.data_add(gebs, len(self.raw_data), 4)
577 util.data_add(gebs, data_length, 4)
578 util.data_add(gebs, self.error_severity, 4)
579
580 cper_data = bytearray()
581 cper_data.extend(gebs)
582 cper_data.extend(gede)
583 cper_data.extend(bytearray(self.raw_data))
584 cper_data.extend(bytearray(payload))
585
586 if self.debug:
587 print(f"GUID: {notif_type}")
588
589 util.dump_bytearray("Generic Error Status Block", gebs)
590 util.dump_bytearray("Generic Error Data Entry", gede)
591
592 if self.raw_data:
593 util.dump_bytearray("Raw data", bytearray(self.raw_data))
594
595 util.dump_bytearray("Payload", payload)
596
597 self.send_cper_raw(cper_data)
598
599
600 def search_qom(self, path, prop, regex):
601 """
602 Return a list of devices that match path array like:
603
604 /machine/unattached/device
605 /machine/peripheral-anon/device
606 ...
607 """
608
609 found = []
610
611 i = 0
612 while 1:
613 dev = f"{path}[{i}]"
614 args = {
615 'path': dev,
616 'property': prop
617 }
618 ret = self.send_cmd("qom-get", args, may_open=True,
619 return_error=False)
620 if not ret:
621 break
622
623 if isinstance(ret, str):
624 if regex.search(ret):
625 found.append(dev)
626
627 i += 1
628 if i > 10000:
629 print("Too many objects returned by qom-get!")
630 break
631
632 return found
633
634 class cper_guid:
635 """
636 Contains CPER GUID, as per:
637 https://uefi.org/specs/UEFI/2.10/Apx_N_Common_Platform_Error_Record.html
638 """
639
640 CPER_PROC_GENERIC = guid(0x9876CCAD, 0x47B4, 0x4bdb,
641 [0xB6, 0x5E, 0x16, 0xF1,
642 0x93, 0xC4, 0xF3, 0xDB])
643
644 CPER_PROC_X86 = guid(0xDC3EA0B0, 0xA144, 0x4797,
645 [0xB9, 0x5B, 0x53, 0xFA,
646 0x24, 0x2B, 0x6E, 0x1D])
647
648 CPER_PROC_ITANIUM = guid(0xe429faf1, 0x3cb7, 0x11d4,
649 [0xbc, 0xa7, 0x00, 0x80,
650 0xc7, 0x3c, 0x88, 0x81])
651
652 CPER_PROC_ARM = guid(0xE19E3D16, 0xBC11, 0x11E4,
653 [0x9C, 0xAA, 0xC2, 0x05,
654 0x1D, 0x5D, 0x46, 0xB0])
655
656 CPER_PLATFORM_MEM = guid(0xA5BC1114, 0x6F64, 0x4EDE,
657 [0xB8, 0x63, 0x3E, 0x83,
658 0xED, 0x7C, 0x83, 0xB1])
659
660 CPER_PLATFORM_MEM2 = guid(0x61EC04FC, 0x48E6, 0xD813,
661 [0x25, 0xC9, 0x8D, 0xAA,
662 0x44, 0x75, 0x0B, 0x12])
663
664 CPER_PCIE = guid(0xD995E954, 0xBBC1, 0x430F,
665 [0xAD, 0x91, 0xB4, 0x4D,
666 0xCB, 0x3C, 0x6F, 0x35])
667
668 CPER_PCI_BUS = guid(0xC5753963, 0x3B84, 0x4095,
669 [0xBF, 0x78, 0xED, 0xDA,
670 0xD3, 0xF9, 0xC9, 0xDD])
671
672 CPER_PCI_DEV = guid(0xEB5E4685, 0xCA66, 0x4769,
673 [0xB6, 0xA2, 0x26, 0x06,
674 0x8B, 0x00, 0x13, 0x26])
675
676 CPER_FW_ERROR = guid(0x81212A96, 0x09ED, 0x4996,
677 [0x94, 0x71, 0x8D, 0x72,
678 0x9C, 0x8E, 0x69, 0xED])
679
680 CPER_DMA_GENERIC = guid(0x5B51FEF7, 0xC79D, 0x4434,
681 [0x8F, 0x1B, 0xAA, 0x62,
682 0xDE, 0x3E, 0x2C, 0x64])
683
684 CPER_DMA_VT = guid(0x71761D37, 0x32B2, 0x45cd,
685 [0xA7, 0xD0, 0xB0, 0xFE,
686 0xDD, 0x93, 0xE8, 0xCF])
687
688 CPER_DMA_IOMMU = guid(0x036F84E1, 0x7F37, 0x428c,
689 [0xA7, 0x9E, 0x57, 0x5F,
690 0xDF, 0xAA, 0x84, 0xEC])
691
692 CPER_CCIX_PER = guid(0x91335EF6, 0xEBFB, 0x4478,
693 [0xA6, 0xA6, 0x88, 0xB7,
694 0x28, 0xCF, 0x75, 0xD7])
695
696 CPER_CXL_PROT_ERR = guid(0x80B9EFB4, 0x52B5, 0x4DE3,
697 [0xA7, 0x77, 0x68, 0x78,
698 0x4B, 0x77, 0x10, 0x48])