mfd-dcb 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mfd_dcb/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ # Copyright (C) 2025 Intel Corporation
2
+ # SPDX-License-Identifier: MIT
3
+ """Module for MFD DCB."""
4
+
5
+ from .mfd_dcb import Dcb
6
+ from .dcbtool_wrapper import DcbTool
mfd_dcb/const.py ADDED
@@ -0,0 +1,7 @@
1
+ # Copyright (C) 2025 Intel Corporation
2
+ # SPDX-License-Identifier: MIT
3
+ """Const for DCBmodule."""
4
+
5
+ PFC_STATS = ["GLPRT_PXONRXCNT", "GLPRT_PXOFFRXCNT", "GLPRT_PXONTXCNT", "GLPRT_PXOFFTXCNT"]
6
+
7
+ QOS_ENABLED = ["*QOS"]
@@ -0,0 +1,12 @@
1
+ # Copyright (C) 2025 Intel Corporation
2
+ # SPDX-License-Identifier: MIT
3
+ """Module for DCB common data structures."""
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class State(Enum):
9
+ """States."""
10
+
11
+ ENABLED = "enabled"
12
+ DISABLED = "disabled"
mfd_dcb/dcb_wrapper.py ADDED
@@ -0,0 +1,268 @@
1
+ # Copyright (C) 2026 Intel Corporation
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Module for the iproute2 dcb tool."""
5
+
6
+ import re
7
+
8
+ from typing import TYPE_CHECKING, Any
9
+ from mfd_const.qos import LOCAL_ETS, LOCAL_PFC, LOCAL_APP
10
+ from .exceptions import DcbExecutionError, DcbExecutionProcessError
11
+
12
+ if TYPE_CHECKING:
13
+ from mfd_connect import RPyCConnection
14
+
15
+ APP_SELECTORS = (
16
+ "default-prio",
17
+ "ethtype-prio",
18
+ "stream-port-prio",
19
+ "dgram-port-prio",
20
+ "port-prio",
21
+ "dscp-prio",
22
+ "pcp-prio",
23
+ )
24
+
25
+
26
+ class DcbCli:
27
+ """DcbCli class provides DCB configuration read-out using the iproute2 dcb tool (dcbnl netlink)."""
28
+
29
+ def __init__(self, interface_name: str, connection: "RPyCConnection"):
30
+ """Initialize class parameters.
31
+
32
+ :param interface_name: tested interface
33
+ :param connection: connection
34
+ """
35
+ self.interface_name = interface_name
36
+ self._connection = connection
37
+
38
+ def get_dcb(self, numeric: bool = True) -> dict[str, dict[str, Any]]:
39
+ """Get complete DCB configuration for ETS, PFC and APP TLVs.
40
+
41
+ :param numeric: when True (default), APP protocol values are read numerically (``dcb -N app show``)
42
+ :return: Dictionary of QoS configuration
43
+ """
44
+ qos = {}
45
+ qos.update(self.get_ets())
46
+ qos.update(self.get_pfc())
47
+ qos.update(self.get_app(numeric=numeric))
48
+ return qos
49
+
50
+ def get_ets(self) -> dict[str, dict[str, Any]]:
51
+ """Get local ETS configuration of the interface via ``dcb ets show``.
52
+
53
+ :return: Dictionary of local ETS configuration
54
+ """
55
+ output = self._run("ets")
56
+ tc_tsa = self._extract_pairs(output, "tc-tsa")
57
+ tc_bw = self._extract_pairs(output, "tc-bw")
58
+ prio_tc = self._extract_pairs(output, "prio-tc")
59
+
60
+ ets_config: dict[str, dict[str, Any]] = {}
61
+ for tc, tsa in tc_tsa.items():
62
+ ets_config[str(tc)] = {
63
+ "TSA": tsa.upper(),
64
+ "Bandwidth": int(tc_bw.get(tc, 0)),
65
+ "Priorities": [],
66
+ }
67
+ for prio, tc in prio_tc.items():
68
+ ets_config.setdefault(
69
+ str(tc),
70
+ {"TSA": "", "Bandwidth": int(tc_bw.get(tc, 0)), "Priorities": []},
71
+ )
72
+ ets_config[str(tc)]["Priorities"].append(prio)
73
+ return {LOCAL_ETS: ets_config}
74
+
75
+ def get_pfc(self) -> dict[str, list[bool]]:
76
+ """Get local PFC configuration of the interface via ``dcb pfc show``.
77
+
78
+ :return: Dictionary of local PFC configuration
79
+ """
80
+ output = self._run("pfc")
81
+ prio_pfc = self._extract_pairs(output, "prio-pfc")
82
+ pfc_config = [False] * 8
83
+ for prio, state in prio_pfc.items():
84
+ if 0 <= prio < 8:
85
+ pfc_config[prio] = state == "on"
86
+ return {LOCAL_PFC: pfc_config}
87
+
88
+ def get_app(self, numeric: bool = True) -> dict[str, dict[str, Any]]:
89
+ """Get local APP table of the interface via ``dcb app show``.
90
+
91
+ :param numeric: when True (default), protocol values are read numerically via ``dcb -N app show``.
92
+ :return: Dictionary of local APP configuration
93
+ """
94
+ output = self._run("app", numeric=numeric)
95
+ app_config: dict[int, dict[str, Any]] = {}
96
+ for no, (selector, protocol, priority) in enumerate(self._extract_app_entries(output)):
97
+ app_config[no] = {
98
+ "selector": selector.replace("-prio", ""),
99
+ "priority": int(priority),
100
+ "protocol": protocol,
101
+ }
102
+ return {LOCAL_APP: app_config}
103
+
104
+ def set_ets(
105
+ self,
106
+ user_priority_mapping: list[tuple],
107
+ bandwidth_per_traffic_class: list[int],
108
+ mode: str = "ieee",
109
+ ) -> None:
110
+ """Set ETS configuration via ``dcb ets set``.
111
+
112
+ :param user_priority_mapping: User Priority mapping to Traffic Classes, list of tuples, each tuple is a next
113
+ Traffic Class with given User Priorities as integer values, e.g. [(0, 1), (2, 3), (5, 4), (6, 7)]
114
+ :param bandwidth_per_traffic_class: list of bandwidth percentage values, one per Traffic Class
115
+ :param mode: accepted for a common setter API across wrappers; not used by the iproute2 dcb tool
116
+ :raises DcbExecutionError: when the command writes to stderr
117
+ """
118
+ tc_tsa = " ".join(f"{tc}:ets" for tc in range(len(user_priority_mapping)))
119
+ tc_bw = " ".join(f"{tc}:{bw}" for tc, bw in enumerate(bandwidth_per_traffic_class))
120
+ prio_tc = " ".join(f"{prio}:{tc}" for tc, priorities in enumerate(user_priority_mapping) for prio in priorities)
121
+ command = f"dcb ets set dev {self.interface_name} willing off tc-tsa {tc_tsa} tc-bw {tc_bw} prio-tc {prio_tc}"
122
+ self._execute_dcb_command(command)
123
+
124
+ def set_pfc(self, pfc_per_priority: list[bool], mode: str = "ieee") -> None:
125
+ """Set Priority Flow Control via ``dcb pfc set``.
126
+
127
+ :param pfc_per_priority: list of 8 Bool values for each priority, True is for PFC ON
128
+ :param mode: accepted for a common setter API across wrappers; not used by the iproute2 dcb tool
129
+ :raises DcbExecutionError: when the command writes to stderr
130
+ """
131
+ prio_pfc = " ".join(f"{prio}:{'on' if pfc else 'off'}" for prio, pfc in enumerate(pfc_per_priority))
132
+ command = f"dcb pfc set dev {self.interface_name} prio-pfc {prio_pfc}"
133
+ self._execute_dcb_command(command)
134
+
135
+ def set_app(self, proto_prio_map: dict | list[tuple], *, selector: str = "dscp-prio") -> None:
136
+ """Add APP (protocol to priority) entries via ``dcb app add``.
137
+
138
+ :param proto_prio_map: mapping of protocol to priority, e.g. {26: 3} (DSCP 26 -> priority 3, RoCEv2)
139
+ :param selector: APP selector, e.g. "dscp-prio" (default), "ethtype-prio", "port-prio"
140
+ :raises DcbExecutionError: when the mapping is invalid or the command writes to stderr
141
+ """
142
+ proto_prio = self._format_app_map(proto_prio_map)
143
+ self._execute_dcb_command(f"dcb app add dev {self.interface_name} {selector} {proto_prio}")
144
+
145
+ def del_app(self, proto_prio_map: dict | list[tuple], *, selector: str = "dscp-prio") -> None:
146
+ """Delete specific APP (protocol to priority) entries via ``dcb app del``.
147
+
148
+ :param proto_prio_map: mapping of protocol to priority to remove, e.g. {26: 3}
149
+ :param selector: APP selector, e.g. "dscp-prio" (default), "ethtype-prio", "port-prio"
150
+ :raises DcbExecutionError: when the mapping is invalid or the command writes to stderr
151
+ """
152
+ proto_prio = self._format_app_map(proto_prio_map)
153
+ self._execute_dcb_command(f"dcb app del dev {self.interface_name} {selector} {proto_prio}")
154
+
155
+ def replace_app(self, proto_prio_map: dict | list[tuple], *, selector: str = "dscp-prio") -> None:
156
+ """Replace APP (protocol to priority) entries via ``dcb app replace``.
157
+
158
+ :param proto_prio_map: mapping of protocol to the new priority, e.g. {26: 5} (DSCP 26 -> priority 5)
159
+ :param selector: APP selector, e.g. "dscp-prio" (default), "ethtype-prio", "port-prio"
160
+ :raises DcbExecutionError: when the mapping is invalid or the command writes to stderr
161
+ """
162
+ proto_prio = self._format_app_map(proto_prio_map)
163
+ self._execute_dcb_command(f"dcb app replace dev {self.interface_name} {selector} {proto_prio}")
164
+
165
+ def flush_app(self, selector: str | None = "dscp-prio") -> None:
166
+ """Flush the APP table via ``dcb app flush``.
167
+
168
+ :param selector: APP selector to flush; when None, all APP entries are flushed
169
+ :raises DcbExecutionError: when the command writes to stderr
170
+ """
171
+ command = f"dcb app flush dev {self.interface_name}"
172
+ if selector:
173
+ command += f" {selector}"
174
+ self._execute_dcb_command(command)
175
+
176
+ @staticmethod
177
+ def _format_app_map(proto_prio_map: dict | list[tuple]) -> str:
178
+ """Format a protocol-to-priority mapping into the ``dcb app`` key argument.
179
+
180
+ :param proto_prio_map: mapping of protocol to priority, e.g. {26: 3, 30: 3}, [(26, 3)] or {"0x8906": 3}
181
+ :return: space separated "PROTO:PRIO" pairs, e.g. "26:3 30:3"
182
+ :raises DcbExecutionError: when the mapping is empty or malformed
183
+ """
184
+ pairs = proto_prio_map.items() if isinstance(proto_prio_map, dict) else proto_prio_map
185
+ try:
186
+ formatted = " ".join(f"{proto}:{int(prio)}" for proto, prio in pairs)
187
+ except (TypeError, ValueError) as err:
188
+ raise DcbExecutionError(f"Invalid proto_prio_map: {proto_prio_map}") from err
189
+ if not formatted:
190
+ raise DcbExecutionError("proto_prio_map must contain at least one protocol:priority entry")
191
+ return formatted
192
+
193
+ def _execute_dcb_command(self, cmd_list: list[str] | str) -> None:
194
+ """Execute iproute2 dcb tool command(s) and validate the return code and stderr.
195
+
196
+ :param cmd_list: command or list of commands to execute
197
+ :raises DcbExecutionProcessError: when a command returns a non-zero return code
198
+ :raises DcbExecutionError: when a command writes to stderr
199
+ """
200
+ cmd_list = cmd_list if isinstance(cmd_list, list) else [cmd_list]
201
+ for cmd in cmd_list:
202
+ output = self._connection.execute_command(
203
+ cmd,
204
+ custom_exception=DcbExecutionProcessError,
205
+ expected_return_codes=[0],
206
+ )
207
+ if output.stderr:
208
+ raise DcbExecutionError(f"Error while executing '{cmd}': {output.stderr}")
209
+
210
+ def _run(self, attribute: str, numeric: bool = False) -> str:
211
+ """Run ``dcb <attribute> show`` for the interface and return stdout.
212
+
213
+ :param attribute: dcb object to read - pfc, ets or app
214
+ :param numeric: when True, pass the global ``-N`` option so values are printed numerically
215
+ :return: command stdout
216
+ :raises DcbExecutionError: when the command reports an error on stderr
217
+ """
218
+ numeric_opt = "-N " if numeric else ""
219
+ command = f"dcb {numeric_opt}{attribute} show dev {self.interface_name}"
220
+ output = self._connection.execute_command(
221
+ command,
222
+ custom_exception=DcbExecutionProcessError,
223
+ expected_return_codes=[0],
224
+ )
225
+ if output.stderr:
226
+ raise DcbExecutionError(f"Error while executing dcb {command}: {output.stderr}")
227
+ return output.stdout
228
+
229
+ @staticmethod
230
+ def _extract_app_entries(output: str) -> list[tuple[str, str, str]]:
231
+ """Extract APP table entries as an ordered sequence of ``(selector, protocol, priority)``.
232
+
233
+ :param output: ``dcb app show`` stdout
234
+ :return: ordered list of ``(selector, protocol, priority)`` tuples
235
+ """
236
+ entries: list[tuple[str, str, str]] = []
237
+ current_selector = None
238
+ for token in output.split():
239
+ if token in APP_SELECTORS:
240
+ current_selector = token
241
+ continue
242
+ if not current_selector:
243
+ continue
244
+ if current_selector == "default-prio":
245
+ entries.append((current_selector, "0", token))
246
+ elif ":" in token:
247
+ protocol, _, priority = token.rpartition(":")
248
+ entries.append((current_selector, protocol, priority))
249
+ return entries
250
+
251
+ @staticmethod
252
+ def _extract_pairs(output: str, keyword: str) -> dict[Any, str]:
253
+ """Extract the ``KEY:VALUE`` pairs that follow a dcb attribute keyword.
254
+
255
+ :param output: dcb command stdout
256
+ :param keyword: attribute keyword to read the pairs for, e.g. "prio-pfc", "tc-tsa", "dscp-prio"
257
+ :return: mapping of parsed key to value string
258
+ """
259
+ match = re.search(rf"(?<![-\w]){re.escape(keyword)}\s+((?:\S+:\S+\s*)+)", output)
260
+ pairs: dict[Any, str] = {}
261
+ if not match:
262
+ return pairs
263
+ for token in match.group(1).split():
264
+ if ":" not in token:
265
+ break
266
+ key, _, value = token.partition(":")
267
+ pairs[int(key) if key.isdigit() else key] = value
268
+ return pairs
@@ -0,0 +1,144 @@
1
+ # Copyright (C) 2025 Intel Corporation
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Module for dcbnl."""
5
+
6
+ import re
7
+
8
+ from typing import TYPE_CHECKING, Any
9
+ from mfd_const.qos import LOCAL_ETS, LOCAL_PFC, LOCAL_APP, DCB_TOOL_PATH_LNX
10
+ from .exceptions import DcbExecutionError, DcbExecutionProcessError
11
+
12
+ if TYPE_CHECKING:
13
+ from mfd_connect import RPyCConnection
14
+
15
+
16
+ class Dcbnl:
17
+ """Dcbnl class provides DCB configuration using dcbnl python script."""
18
+
19
+ def __init__(self, interface_name: str, connection: "RPyCConnection"):
20
+ """Initialize class parameters.
21
+
22
+ :param interface_name: tested interface
23
+ :param connection: connection
24
+ """
25
+ self.interface_name = interface_name
26
+ self._connection = connection
27
+
28
+ def get_dcb(self) -> dict[str, dict[str, Any]]:
29
+ """Get complete DCB configuration for ETS, PFC and APP TLVs, local and remote.
30
+
31
+ :return: Dictionary of QoS configuration
32
+ """
33
+ qos = {}
34
+ qos.update(self.get_ets())
35
+ qos.update(self.get_pfc(from_cache=True))
36
+ qos.update(self.get_app(from_cache=True))
37
+ return qos
38
+
39
+ def get_ets(self, from_cache: bool = False) -> dict[str, dict[str, Any]]:
40
+ """Get local ETS TLV configuration of the interface_name.
41
+
42
+ :param from_cache: use current configuration output
43
+ :return: Dictionary of local ETS TLV configuration
44
+ """
45
+ if not from_cache:
46
+ self._read_qos_configuration()
47
+ return self._parse_ets(LOCAL_ETS)
48
+
49
+ def get_pfc(self, from_cache: bool = False) -> dict[str, bool]:
50
+ """Get local PFC TLV configuration of the interface_name.
51
+
52
+ :param from_cache: use current configuration output
53
+ :return: Dictionary of local PFC TLV configuration
54
+ """
55
+ if not from_cache:
56
+ self._read_qos_configuration()
57
+ return self._parse_pfc(LOCAL_PFC)
58
+
59
+ def get_app(self, from_cache: bool = False) -> dict[str, dict[str, Any]]:
60
+ """Get local APP TLV configuration of the interface_name.
61
+
62
+ :param from_cache: use current configuration output
63
+ :return: Dictionary of local APP TLV configuration
64
+ """
65
+ if not from_cache:
66
+ self._read_qos_configuration()
67
+ return self._parse_app(LOCAL_APP)
68
+
69
+ def _read_qos_configuration(self) -> None:
70
+ """Run dcbnl.py script for given interface_name and store it in self._qos_output for future use.
71
+
72
+ dcbnl.py - Netlink message generation/parsing - DCB_TOOL_PATH_LNX = /tmp/tools/dcb
73
+ :raises DcbExecutionError: when return code of bash command is unexpected
74
+ """
75
+ dcbnl_path = self._connection.path(DCB_TOOL_PATH_LNX, "dcbnl.py")
76
+ command = f"{self._connection.modules().sys.executable} {dcbnl_path} {self.interface_name}"
77
+ output = self._connection.execute_command(
78
+ command, custom_exception=DcbExecutionProcessError, expected_return_codes=[0]
79
+ )
80
+ if output.stderr:
81
+ raise DcbExecutionError(f"Error while executing dcbnl {command}: {output.stderr}")
82
+ self._qos_output = output.stdout
83
+
84
+ def _parse_ets(self, key: str) -> dict[str, dict[str, int]]:
85
+ """Get local or remote QoS traffic classes as dictionary.
86
+
87
+ :param key: key in configuration dictionary for ETS
88
+ :return: Dictionary of QoS traffic classes configuration
89
+ """
90
+ ets_config = {}
91
+ current_tc = ""
92
+ for line in self._qos_output.splitlines():
93
+ line = line.strip()
94
+ exp = re.compile(r"tc: (?P<tc>[0-7]) tsa: (?P<tsa>ets|strict)(?:, bw: (?P<bw>\d+)%)")
95
+ match = exp.match(line)
96
+ if match:
97
+ current_tc = match.group("tc")
98
+ ets_config[current_tc] = {
99
+ "TSA": match.group("tsa").upper(),
100
+ "Bandwidth": int(match.group("bw") or "0"),
101
+ "Priorities": [],
102
+ }
103
+ continue
104
+ match = re.match(r"up:\s+(?P<up>[0-7])", line)
105
+ if match:
106
+ up = int(match.group("up"))
107
+ ets_config[current_tc]["Priorities"].append(up)
108
+ continue
109
+ return {key: ets_config}
110
+
111
+ def _parse_pfc(self, key: str) -> dict[str, bool]:
112
+ """Get operational or remote QoS flow control as dictionary.
113
+
114
+ :param key: key in configuration dictionary for PFC
115
+ :return: Dictionary of QoS flow control configuration
116
+ """
117
+ pfc_config = [False] * 8
118
+ match = re.search(r"pfc_enable:\s+(?P<pfc>0x[0-9a-fA-F]+)", self._qos_output)
119
+ if match:
120
+ pfc_bin = int(match.group("pfc"), 16)
121
+ for i in range(8):
122
+ pfc_config[i] = pfc_bin & (pow(2, i)) > 0
123
+ return {key: pfc_config}
124
+
125
+ def _parse_app(self, key: str) -> dict[str, dict[str, str]]:
126
+ """Get operational or remote QoS apps table.
127
+
128
+ :param key: key in configuration dictionary for PFC
129
+ :return: Dictionary of QoS flow control configuration
130
+ """
131
+ app_config = {}
132
+ no = 0
133
+ for line in self._qos_output.splitlines():
134
+ line = line.strip()
135
+ exp = re.compile(r"selector: (?P<selector>\d+) priority: (?P<priority>\d+) protocol: (?P<protocol>\S*)")
136
+ match = exp.match(line)
137
+ if match:
138
+ app_config[no] = {
139
+ "selector": int(match.group("selector")),
140
+ "priority": int(match.group("priority")),
141
+ "protocol": match.group("protocol"),
142
+ }
143
+ no += 1
144
+ return {key: app_config}