Skip to content

Commit e8bb574

Browse files
authored
Add WSManFaultError (#382)
Adds the new exception WSManFaultError which inherits from WinRMError and will be raised when receiving a WSManFault from the server. This new exception type contains detailed information that could be relevant to the caller when trying to handle the specific exception.
1 parent fbb05e8 commit e8bb574

File tree

4 files changed

+368
-28
lines changed

4 files changed

+368
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
### Version 0.5.0
44
- Dropped Python 2.7, 3.6, and 3.7 support, minimum supported version is 3.8
55
- Migrate to PEP 517 compliant build with a `pyproject.toml` file
6+
- Added type annotation
7+
- Added `WSManFaultError` which contains WSManFault specific information when receiving a 500 WSMan fault response
8+
- This contains pre-parsed values like the code, subcode, wsman fault code, wmi error code, and raw response
9+
- It can be used by the caller to implement fallback behaviour based on specific error codes
610

711
### Version 0.4.3
812
- Fix invalid regex escape sequences.

winrm/exceptions.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,64 @@ class WinRMError(Exception):
77
code = 500
88

99

10+
class WSManFaultError(WinRMError):
11+
"""WSMan Fault Error.
12+
13+
Exception that is raised when receiving a WSMan fault message. It
14+
contains the raw response as well as the fault details parsed from the
15+
response.
16+
17+
The wsman_fault_code is returned by the Microsoft WSMan server rather than
18+
the WSMan protocol error code strings. The wmierror_code can contain more
19+
fatal service error codes returned as a MSFT_WmiError object, for example
20+
quota violations.
21+
22+
@param int code: The HTTP status code of the response.
23+
@param str message: The error message.
24+
@param str response: The raw WSMan response text.
25+
@param str reason: The WSMan fault reason.
26+
@param string fault_code: The WSMan fault code.
27+
@param string fault_subcode: The WSMan fault subcode.
28+
@param int wsman_fault_code: The MS WSManFault specific code.
29+
@param int wmierror_code: The MS WMI error code.
30+
"""
31+
32+
def __init__(
33+
self,
34+
code: int,
35+
message: str,
36+
response: str,
37+
reason: str,
38+
fault_code: str | None = None,
39+
fault_subcode: str | None = None,
40+
wsman_fault_code: int | None = None,
41+
wmierror_code: int | None = None,
42+
) -> None:
43+
self.code = code
44+
self.response = response
45+
self.fault_code = fault_code
46+
self.fault_subcode = fault_subcode
47+
self.reason = reason
48+
self.wsman_fault_code = wsman_fault_code
49+
self.wmierror_code = wmierror_code
50+
51+
# Using the dict repr is for backwards compatibility.
52+
fault_data = {
53+
"transport_message": message,
54+
"http_status_code": code,
55+
}
56+
if wsman_fault_code is not None:
57+
fault_data["wsmanfault_code"] = wsman_fault_code
58+
59+
if fault_code is not None:
60+
fault_data["fault_code"] = fault_code
61+
62+
if fault_subcode is not None:
63+
fault_data["fault_subcode"] = fault_subcode
64+
65+
super().__init__("{0} (extended fault data: {1})".format(reason, fault_data))
66+
67+
1068
class WinRMTransportError(Exception):
1169
"""WinRM errors specific to transport-level problems (unexpected HTTP error codes, etc)"""
1270

winrm/protocol.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@
1010

1111
import xmltodict
1212

13-
from winrm.exceptions import WinRMError, WinRMOperationTimeoutError, WinRMTransportError
13+
from winrm.exceptions import (
14+
WinRMError,
15+
WinRMOperationTimeoutError,
16+
WinRMTransportError,
17+
WSManFaultError,
18+
)
1419
from winrm.transport import Transport
1520

1621
xmlns = {
1722
"soapenv": "http://www.w3.org/2003/05/soap-envelope",
1823
"soapaddr": "http://schemas.xmlsoap.org/ws/2004/08/addressing",
1924
"wsmanfault": "http://schemas.microsoft.com/wbem/wsman/1/wsmanfault",
25+
"wmierror": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/MSFT_WmiError",
2026
}
2127

2228

@@ -247,33 +253,49 @@ def send_message(self, message: str) -> bytes:
247253
raise ex
248254

249255
fault = root.find("soapenv:Body/soapenv:Fault", xmlns)
250-
if fault is not None:
251-
fault_data = dict(transport_message=ex.message, http_status_code=ex.code)
252-
wsmanfault_code = fault.find("soapenv:Detail/wsmanfault:WSManFault[@Code]", xmlns)
253-
if wsmanfault_code is not None:
254-
fault_data["wsmanfault_code"] = wsmanfault_code.get("Code")
255-
# convert receive timeout code to WinRMOperationTimeoutError
256-
if fault_data["wsmanfault_code"] == "2150858793":
257-
# TODO: this fault code is specific to the Receive operation; convert all op timeouts?
258-
raise WinRMOperationTimeoutError()
259-
260-
fault_code = fault.find("soapenv:Code/soapenv:Value", xmlns)
261-
if fault_code is not None:
262-
fault_data["fault_code"] = fault_code.text
263-
264-
fault_subcode = fault.find("soapenv:Code/soapenv:Subcode/soapenv:Value", xmlns)
265-
if fault_subcode is not None:
266-
fault_data["fault_subcode"] = fault_subcode.text
267-
268-
error_message_node = fault.find("soapenv:Reason/soapenv:Text", xmlns)
269-
if error_message_node is not None:
270-
error_message = error_message_node.text
271-
else:
272-
error_message = "(no error message in fault)"
273-
274-
raise WinRMError("{0} (extended fault data: {1})".format(error_message, fault_data))
275-
276-
raise
256+
if fault is None:
257+
raise
258+
259+
wsmanfault_code_raw = fault.find("soapenv:Detail/wsmanfault:WSManFault[@Code]", xmlns)
260+
wsmanfault_code: int | None = None
261+
if wsmanfault_code_raw is not None:
262+
wsmanfault_code = int(wsmanfault_code_raw.attrib["Code"])
263+
264+
# convert receive timeout code to WinRMOperationTimeoutError
265+
if wsmanfault_code == 2150858793:
266+
# TODO: this fault code is specific to the Receive operation; convert all op timeouts?
267+
raise WinRMOperationTimeoutError()
268+
269+
fault_code_raw = fault.find("soapenv:Code/soapenv:Value", xmlns)
270+
fault_code: str | None = None
271+
if fault_code_raw is not None and fault_code_raw.text:
272+
fault_code = fault_code_raw.text
273+
274+
fault_subcode_raw = fault.find("soapenv:Code/soapenv:Subcode/soapenv:Value", xmlns)
275+
fault_subcode: str | None = None
276+
if fault_subcode_raw is not None and fault_subcode_raw.text:
277+
fault_subcode = fault_subcode_raw.text
278+
279+
error_message_node = fault.find("soapenv:Reason/soapenv:Text", xmlns)
280+
reason: str | None = None
281+
if error_message_node is not None:
282+
reason = error_message_node.text
283+
284+
wmi_error_code_raw = fault.find("soapenv:Detail/wmierror:MSFT_WmiError/wmierror:error_Code", xmlns)
285+
wmi_error_code: int | None = None
286+
if wmi_error_code_raw is not None and wmi_error_code_raw.text:
287+
wmi_error_code = int(wmi_error_code_raw.text)
288+
289+
raise WSManFaultError(
290+
code=ex.code,
291+
message=ex.message,
292+
response=ex.response_text,
293+
reason=reason or "(no error message in fault)",
294+
fault_code=fault_code,
295+
fault_subcode=fault_subcode,
296+
wsman_fault_code=wsmanfault_code,
297+
wmierror_code=wmi_error_code,
298+
)
277299

278300
def close_shell(self, shell_id: str, close_session: bool = True) -> None:
279301
"""

0 commit comments

Comments
 (0)