diff --git a/build/rpm/pcp.spec.in b/build/rpm/pcp.spec.in index d2600c181b..375332e24b 100755 --- a/build/rpm/pcp.spec.in +++ b/build/rpm/pcp.spec.in @@ -1598,6 +1598,20 @@ This package contains the PCP Performance Metrics Domain Agent (PMDA) for collecting statistics for nVidia RDMA over Converged Ethernet (RoCE) devices. # end pcp-pmda-rocestat +# +# pcp-pmda-rds +# +%package pmda-rds +License: GPL-2.0-or-later +Summary: Performance Co-Pilot (PCP) metrics for RDS protocol +URL: https://pcp.io +Requires: pcp = @package_version@ pcp-libs = @package_version@ +Requires: python3-pcp +%description pmda-rds +This package contains the PCP Performance Metrics Domain Agent (PMDA) for +collecting statistics for RDS connections +# end pcp-pmda-rds + # # pcp-pmda-openvswitch # @@ -2268,6 +2282,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/podman(/|$)' >pcp-pmda-podman-files basic_manifest | keep '(etc/pcp|pmdas)/postfix(/|$)' >pcp-pmda-postfix-files basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-files basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files +basic_manifest | keep '(etc/pcp|pmdas)/rds(/|$)' >pcp-pmda-rds-files basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files @@ -2308,7 +2323,7 @@ for pmda_package in \ nutcracker nvidia \ openmetrics openvswitch oracle \ pdns perfevent podman postfix postgresql \ - rabbitmq redis resctrl rocestat roomtemp rsyslog \ + rabbitmq rds redis resctrl rocestat roomtemp rsyslog \ samba sendmail shping slurm smart snmp \ sockets statsd summary systemd \ unbound uwsgi \ @@ -2744,6 +2759,9 @@ done %preun pmda-rocestat %{pmda_remove "$1" "rocestat"} +%preun pmda-rds +%{pmda_remove "$1" "rds"} + %preun pmda-uwsgi %{pmda_remove "$1" "uwsgi"} %endif @@ -3072,6 +3090,8 @@ fi %files pmda-rocestat -f pcp-pmda-rocestat-files.rpm +%files pmda-rds -f pcp-pmda-rds-files.rpm + %files pmda-uwsgi -f pcp-pmda-uwsgi-files.rpm %files export-pcp2graphite -f pcp-export-pcp2graphite-files.rpm diff --git a/build/rpm/redhat.spec b/build/rpm/redhat.spec index 821e0354fa..0c30ef370f 100644 --- a/build/rpm/redhat.spec +++ b/build/rpm/redhat.spec @@ -1759,6 +1759,21 @@ collecting statistics for nVidia RDMA over Converged Ethernet (RoCE) devices. # end pcp-pmda-rocestat %endif +# +# pcp-pmda-rds +# +%package pmda-rds +License: GPL-2.0-or-later +Summary: Performance Co-Pilot (PCP) metrics for RDS protocol +URL: https://pcp.io +Requires: pcp = %{version}-%{release} pcp-libs = %{version}-%{release} +Requires: python3-pcp +%description pmda-rds +This package contains the PCP Performance Metrics Domain Agent (PMDA) for +collecting statistics for RDS connections. +# end pcp-pmda-rds +%endif + %if !%{disable_mongodb} # # pcp-pmda-mongodb @@ -2478,6 +2493,7 @@ basic_manifest | keep '(etc/pcp|pmdas)/podman(/|$)' >pcp-pmda-podman-files basic_manifest | keep '(etc/pcp|pmdas)/postfix(/|$)' >pcp-pmda-postfix-files basic_manifest | keep '(etc/pcp|pmdas)/postgresql(/|$)' >pcp-pmda-postgresql-files basic_manifest | keep '(etc/pcp|pmdas)/rabbitmq(/|$)' >pcp-pmda-rabbitmq-files +basic_manifest | keep '(etc/pcp|pmdas)/rds(/|$)' >pcp-pmda-rds-files basic_manifest | keep '(etc/pcp|pmdas)/redis(/|$)' >pcp-pmda-redis-files basic_manifest | keep '(etc/pcp|pmdas)/resctrl(/|$)|sys-fs-resctrl' >pcp-pmda-resctrl-files basic_manifest | keep '(etc/pcp|pmdas)/rocestat(/|$)' >pcp-pmda-rocestat-files @@ -2519,7 +2535,7 @@ for pmda_package in \ nutcracker nvidia \ openmetrics openvswitch oracle \ pdns perfevent podman postfix postgresql \ - rabbitmq redis resctrl rocestat roomtemp rpm rsyslog \ + rabbitmq rds redis resctrl rocestat roomtemp rpm rsyslog \ samba sendmail shping slurm smart snmp \ sockets statsd summary systemd \ unbound uwsgi \ @@ -2913,6 +2929,9 @@ exit 0 %preun pmda-rocestat %{pmda_remove "$1" "rocestat"} +%preun pmda-rds +%{pmda_remove "$1" "rds"} + %endif %preun pmda-apache @@ -3263,6 +3282,8 @@ fi %files pmda-rocestat -f pcp-pmda-rocestat-files.rpm +%files pmda-rds -f pcp-pmda-rds-files.rpm + %files pmda-uwsgi -f pcp-pmda-uwsgi-files.rpm %files export-pcp2graphite -f pcp-export-pcp2graphite-files.rpm diff --git a/src/pmdas/GNUmakefile b/src/pmdas/GNUmakefile index d61ec32360..4f37862263 100644 --- a/src/pmdas/GNUmakefile +++ b/src/pmdas/GNUmakefile @@ -39,7 +39,7 @@ PLPMDAS = bonding netfilter zimbra postgresql \ PYPMDAS = bcc gluster zswap unbound mic haproxy \ json libvirt lio openmetrics elasticsearch \ bpftrace mssql netcheck rabbitmq openvswitch \ - nfsclient mongodb uwsgi rocestat hdb + nfsclient mongodb uwsgi rocestat hdb rds SUBDIRS = $(CPMDAS) $(PLPMDAS) $(PYPMDAS) LDIRT = pmcd.conf diff --git a/src/pmdas/rds/GNUmakefile b/src/pmdas/rds/GNUmakefile new file mode 100644 index 0000000000..3c0f1c68a9 --- /dev/null +++ b/src/pmdas/rds/GNUmakefile @@ -0,0 +1,52 @@ +# +# Copyright (c) 2025 Oracle and/or its affiliates. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# + +TOPDIR = ../../.. +include $(TOPDIR)/src/include/builddefs + +IAM = rds +PYSCRIPT = pmda$(IAM).python +LDIRT = domain.h root pmns $(IAM).log +DOMAIN = GLUSTER + +PMDAADMDIR = $(PCP_PMDASADM_DIR)/$(IAM) +PMDATMPDIR = $(PCP_PMDAS_DIR)/$(IAM) + +MAN_SECTION = 1 +MAN_PAGES = pmda$(IAM).$(MAN_SECTION) +MAN_DEST = $(PCP_MAN_DIR)/man$(MAN_SECTION) + +default_pcp default: build-me + +include $(BUILDRULES) + +ifeq "$(HAVE_PYTHON)" "true" +build-me: check_domain +install_pcp install: default + $(INSTALL) -m 755 -d $(PMDAADMDIR) + $(INSTALL) -m 755 -d $(PMDATMPDIR) + $(INSTALL) -m 755 -t $(PMDATMPDIR) Install Remove $(PYSCRIPT) $(PMDAADMDIR) + @$(INSTALL_MAN) +else +build-me: +install_pcp install: + @$(INSTALL_MAN) +endif + +check_domain: ../../pmns/stdpmid + $(DOMAIN_PYTHONRULE) + +check:: $(MAN_PAGES) + $(MANLINT) $^ diff --git a/src/pmdas/rds/Install b/src/pmdas/rds/Install new file mode 100755 index 0000000000..5eada91604 --- /dev/null +++ b/src/pmdas/rds/Install @@ -0,0 +1,35 @@ +# +# Copyright (c) 2025 Oracle and/or its affiliates. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# + +. $PCP_DIR/etc/pcp.env +. $PCP_SHARE_DIR/lib/pmdaproc.sh + +iam=rds +python_opt=true +daemon_opt=false + +# Prepare PMDA Python files (needed for easier packaging) +$PCP_PYTHON_PROG $PCP_PMDAS_DIR/rds/pyprep + +# +# See pmcd(1) man page. PMDA starts up in the "not ready" state. +# When it has finished starting up, it sends a PM_ERR_PMDAREADY +# error PDU to PMCD to indicate it's ready to start processing +# requests. + +ipc_prot="binary notready" +pmdaSetup +pmdaInstall +exit diff --git a/src/pmdas/rds/Remove b/src/pmdas/rds/Remove new file mode 100755 index 0000000000..a77324f961 --- /dev/null +++ b/src/pmdas/rds/Remove @@ -0,0 +1,35 @@ +# +# Copyright (c) 2025 Oracle and/or its affiliates. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# Remove the rds PMDA +# + +# source the PCP configuration environment variables +. $PCP_DIR/etc/pcp.env + +# Get the common procedures and variable assignments +# +. $PCP_SHARE_DIR/lib/pmdaproc.sh + +# The name of the PMDA +# +iam=rds + +# Do it +# +pmdaSetup +pmdaRemove + + +exit diff --git a/src/pmdas/rds/domain.h b/src/pmdas/rds/domain.h new file mode 100644 index 0000000000..925639b909 --- /dev/null +++ b/src/pmdas/rds/domain.h @@ -0,0 +1 @@ +#define RDS 256 diff --git a/src/pmdas/rds/modules/ping.py b/src/pmdas/rds/modules/ping.py new file mode 100755 index 0000000000..9bcb8e3811 --- /dev/null +++ b/src/pmdas/rds/modules/ping.py @@ -0,0 +1,320 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +""" +Python implementation of ping +""" +from collections import defaultdict +import json +import socket +import sys +import os +import time +import select +from datetime import datetime +import argparse +import struct +import ipaddress +import threading +import signal +import re +import concurrent.futures + +sys.path.append("/var/lib/pcp/pmdas/rds/modules") +from rds_info import RdsInfo + +ICMP_ECHO = 8 +ICMP_MAX_RECV = 2048 +RDS_INFO_LEN = 176 +SOL_RDS = 276 +RDS_INFO_IB_CONNECTIONS = 10008 + +pings_sent = 0 +pending = 0 +own_id = os.getpid() & 0xFFFF +pings_json = {} + + +def signal_handler(sig, frame): + sys.exit(0) + + +def create_socket(): + try: + sock_fd = socket.socket( + socket.AF_INET, socket.SOCK_RAW, 1) # 1 for ICMP + return sock_fd + except socket.error as err: + print("socket creation failed with error %s" % (err)) + return None + + +def bind_socket(sock_fd, src_ip): + src_port = 0 + try: + sock_fd.bind((src_ip, src_port)) + except socket.error as err: + print("socket bind failed with error %s" % (err)) + + +def calculate_checksum(source_string): + countTo = (int(len(source_string) / 2)) * 2 + total = 0 + count = 0 + + # Handle bytes in pairs (decoding as short ints) + loByte = 0 + hiByte = 0 + while count < countTo: + if sys.byteorder == "little": + loByte = source_string[count] + hiByte = source_string[count + 1] + else: + loByte = source_string[count + 1] + hiByte = source_string[count] + total = total + ((hiByte) * 256 + (loByte)) + count += 2 + + # Handle last byte if applicable (odd-number of bytes) + # Endianness should be irrelevant in this case + if countTo < len(source_string): # Check for odd length + loByte = source_string[len(source_string) - 1] + total += loByte + + total &= 0xffffffff # Truncate total to 32 bits (a variance from ping.c, which + # uses signed ints, but overflow is unlikely in ping) + + total = (total >> 16) + (total & 0xffff) # Add high 16 bits to low 16 bits + total += (total >> 16) # Add carry from above (if any) + answer = ~total & 0xffff # Invert and truncate to 16 bits + answer = socket.htons(answer) + + return answer + + +def create_rds_socket(): + try: + sock_fd = socket.socket(socket.PF_RDS, socket.SOCK_SEQPACKET, 0) + return sock_fd + except socket.error as err: + print("socket creation failed with error %s" % (err)) + return None + + +def htosi(data, signed): + hex_string = data.hex() + return int.from_bytes(bytes.fromhex(hex_string), byteorder='little', signed=signed) + + +def send_ping(sock_fd, src_ip, dst_ip_list, send_timestamps): + seq_number = 0 + global own_id + global pings_sent + + for dst_ip in dst_ip_list: + checksum = 0 + header = struct.pack("!BBHHH", ICMP_ECHO, 0, + checksum, own_id, seq_number) + checksum = calculate_checksum(header) + header = struct.pack("!BBHHH", ICMP_ECHO, 0, + checksum, own_id, seq_number) + packet = header + try: + sock_fd.sendto(packet, (dst_ip, 1)) + send_timestamps[dst_ip] = datetime.now() + except socket.error as err: + print("socket send failed with error %s" % (err)) + # remove the added entry from the list + del send_timestamps[dst_ip] + pings_sent = pings_sent + 1 + + +def recv_pong(sock_fd, src_ip, num_dest, timeout, send_timestamps, print_all): + + inputs = [sock_fd] + outputs = [] + latencies_info = "" + pongs_rcvd = 0 + sock_fd.settimeout(timeout) + + select_timeout = timeout + + select_timeout_msec = select_timeout * 1000 + while True: + select_start_time = int(round(time.time() * 1000)) + readable, writable, exceptional = select.select( + inputs, outputs, inputs, select_timeout) + + for _ in readable: + try: + packet_data, (ipaddr, _) = sock_fd.recvfrom(ICMP_MAX_RECV) + icmp_header = struct.unpack("!BBHHH", packet_data[20:28]) + + if icmp_header[3] == own_id: + latency = datetime.now() - send_timestamps[ipaddr] + lat_data = ("%s %s - %.3f |" % + (src_ip, ipaddr, latency.microseconds)) + latencies_info = latencies_info + lat_data + pongs_rcvd = pongs_rcvd + 1 + conn = "%s-%s" % (src_ip, ipaddr) + pings_json[conn] = latency.microseconds + del send_timestamps[ipaddr] + except KeyError: + continue + + curr_time = int(round(time.time() * 1000)) + time_elapsed = curr_time - select_start_time + select_timeout_msec = select_timeout_msec - time_elapsed + if select_timeout_msec < 0: + for keys, _ in send_timestamps.items(): + lat_data = ("%s %s - timeout |" % (src_ip, keys)) + latencies_info = latencies_info + lat_data + conn = "%s-%s" % (src_ip, keys) + pings_json[conn] = -1 + break + + select_timeout = select_timeout_msec/1000 + + if num_dest - pongs_rcvd == 0: + break + + if not (readable or writable or exceptional): + for keys, _ in send_timestamps.items(): + lat_data = ("%s %s - timeout |" % (src_ip, keys)) + latencies_info = latencies_info + lat_data + conn = "%s-%s" % (src_ip, keys) + pings_json[conn] = -1 + + break + + if not print_all: + print(latencies_info) + + +def get_each_entry_len(data): + try: + rds_info_len = data[-4:].decode() + except ValueError: + rds_info_len = data[-4:-2].decode() + + if '\x00' in rds_info_len: + rds_info_len = ' '.join(rds_info_len.split('\x00')) + data = data[:-4] + + rds_info_len = re.findall(r'\d+', rds_info_len) + rds_info_len = int(rds_info_len[-1]) + + return data, rds_info_len + +def ping_destinations(src_ip, dest_ip_list, timeout): + sock_fd = create_socket() + bind_socket(sock_fd, src_ip) + send_timestamps = {} + num_dest = len(dest_ip_list) + send_ping(sock_fd, src_ip, dest_ip_list, send_timestamps) + recv_pong(sock_fd, src_ip, num_dest, timeout, send_timestamps, True) + sock_fd.close() + + +def ping_all_avlbl_dest(timeout): + rds_info = RdsInfo() + rds_connections = rds_info.main('-I').split("\n") + + conns_dict = defaultdict(list) + + for conn in rds_connections: + saddr, daddr = conn.split()[0:2] + if(validate_ip(saddr) == 0 and validate_ip(daddr) == 0): + conns_dict[saddr].append(daddr) + + with concurrent.futures.ThreadPoolExecutor() as executor: + for saddr, dest_ip_list in conns_dict.items(): + executor.submit(ping_destinations, saddr, dest_ip_list, timeout) + + res = json.dumps(pings_json) + return res + + +def validate_ip(ip_addr_str): + try: + ipaddress.ip_address(ip_addr_str) + return 0 + except ValueError: + print("Invalid IP %s" % ip_addr_str) + return 1 + + +def ping(args): + dest_ip_list = [str(item) for item in args.list.split(' ')] + for ip_addr in dest_ip_list: + if validate_ip(ip_addr): + print("Invalid IP being passed %s. Please rectify." % (ip_addr)) + sys.exit(1) + + sock_fd = create_socket() + bind_socket(sock_fd, args.source_ip) + + num_destinations = len(dest_ip_list) + send_timestamps = {} + + thread2 = threading.Thread(target=recv_pong, args=( + sock_fd, args.source_ip, num_destinations, args.timeout, send_timestamps, False)) + thread2.start() + + thread1 = threading.Thread(target=send_ping, args=( + sock_fd, args.source_ip, dest_ip_list, send_timestamps)) + thread1.start() + + thread2.join() + thread1.join() + + sock_fd.close() + + +def main(argv): + parser = argparse.ArgumentParser() + parser.add_argument(dest='destination_ip', + help="destination ip is mandatory") + parser = argparse.ArgumentParser(description='python version of ping utility.', + epilog="sample usage: ping.py -d \"dst_ip_1 dst_ip_2\" -I src_ip") + parser.add_argument('-d', '--list', type=str) + parser.add_argument('-I', '--source_ip', type=str) + parser.add_argument('-t', '--timeout', type=float, default=3) + parser.add_argument('-a', '--auto', action='store_true') + parser.add_argument('-c', '--count', type=int, default=-1) + + args = parser.parse_args() + + if args.auto: + ping_all_avlbl_dest(args.timeout) + print(pings_json) + sys.exit(1) + + else: + if args.count == -1: + while True: + ping(args) + time.sleep(1) + else: + iterations = args.count + while iterations > 0: + ping(args) + iterations = iterations - 1 + if iterations: + time.sleep(1) + + +signal.signal(signal.SIGINT, signal_handler) + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/src/pmdas/rds/modules/rds_info.py b/src/pmdas/rds/modules/rds_info.py new file mode 100755 index 0000000000..55a5822f37 --- /dev/null +++ b/src/pmdas/rds/modules/rds_info.py @@ -0,0 +1,407 @@ +# Copyright (c) 2025 Oracle and/or its affiliates. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +import sys +import struct +import psutil +import socket +import ctypes +import binascii +from datetime import datetime + +# Constants for socket operations +PF_RDS = 21 +SOCK_SEQPACKET = 5 +SOL_RDS = 276 + +RDS_INFO_COUNTERS = 10000 +RDS_INFO_CONNECTIONS = 10001 +RDS_INFO_SEND_MESSAGES = 10003 +RDS_INFO_RETRANS_MESSAGES = 10004 +RDS_INFO_RECV_MESSAGES = 10005 +RDS_INFO_SOCKETS = 10006 +RDS_INFO_TCP_SOCKETS = 10007 +RDS_INFO_IB_CONNECTIONS = 10008 +RDS_INFO_CONN_PATHS = 10020 + +# Connection drop reasons +CONN_DROP_REASONS = [ + "--", "user reset", "invalid connection state", "failure to move to DOWN state", + "connection destroy", "conn_connect failure", "hb timeout", "reconnect timeout", + "cancel operation on socket", "race between ESTABLISHED event and drop", + "conn is not in CONNECTING state", "qp event", "incoming REQ in CONN_UP state", + "incoming REQ in CONNECTING state", "passive setup_qp failure", + "rdma_accept failure", "active setup_qp failure", "rdma_connect failure", + "resolve_route failure", "detected rdma_cm_id mismatch", "ROUTE_ERROR event", + "ADDR_ERROR event", "CONNECT_ERROR or UNREACHABLE or DEVICE_REMOVE event", + "CONSUMER_DEFINED reject", "REJECTED event", "ADDR_CHANGE event", + "DISCONNECTED event", "TIMEWAIT_EXIT event", "post_recv failure", + "send_ack failure", "no header in incoming msg", "corrupted header in incoming msg", + "fragment header mismatch", "recv completion error", "send completion error", + "post_send failure", "rds_rdma module unload", "active bonding failover", + "corresponding loopback conn drop", "active bonding failback", "sk_state to TCP_CLOSE", + "tcp_send failure" +] + +class RdsInfo: + """Class to implement standard RDS info operations.""" + + libc = ctypes.CDLL("libc.so.6", use_errno=True) + + def create_rds_socket(self): + sock_fd = self.libc.socket(PF_RDS, SOCK_SEQPACKET, 0) + if sock_fd < 0: + return None + return sock_fd + + def get_rds_info_data(self, sock_fd, query_type): + """Retrieve RDS information data.""" + data_len = ctypes.c_int(0) + res = self.libc.getsockopt(sock_fd, SOL_RDS, query_type, None, ctypes.byref(data_len)) + if res < 0: + data_buffer = ctypes.create_string_buffer(int(data_len.value)) + res = self.libc.getsockopt(sock_fd, SOL_RDS, query_type, data_buffer, ctypes.byref(data_len)) + if res < 0: + return None + + return data_buffer.raw[:data_len.value], res + + return None, None + + @staticmethod + def little_endian_to_unsigned(hex_string, bit): + """Convert little-endian byte sequence to an unsigned integer.""" + bit_format = "") + print("Available options:") + for key, value in infos.items(): + print(f" {key}: {value['description']}") + return "" + + sock_fd = self.create_rds_socket() + res = infos[option]["method"](sock_fd) + self.libc.close(sock_fd) + return res + +if __name__ == "__main__": + # Ensure an argument is passed + if len(sys.argv) != 2: + print("Usage: python rds_info.py