diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e7187c5c..de3f196dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ #### [nidcpower] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -556,7 +556,7 @@ #### [nidigital] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -798,7 +798,7 @@ #### [nidmm] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1122,7 +1122,7 @@ #### [nifgen] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1736,7 +1736,7 @@ #### [nirfsa] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1754,7 +1754,7 @@ #### [nirfsg] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1893,7 +1893,7 @@ #### [niscope] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -2505,7 +2505,7 @@ #### [niswitch] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed diff --git a/build/templates/_grpc_stub_interpreter.py.mako b/build/templates/_grpc_stub_interpreter.py.mako index 5af49bf86..b5ce4d98a 100644 --- a/build/templates/_grpc_stub_interpreter.py.mako +++ b/build/templates/_grpc_stub_interpreter.py.mako @@ -13,6 +13,7 @@ are_complex_parameters_used = helper.are_complex_parameters_used(functions) import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -69,7 +70,11 @@ class GrpcStubInterpreter(object): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/build/templates/session.py.mako b/build/templates/session.py.mako index df755677b..b3a19c599 100644 --- a/build/templates/session.py.mako +++ b/build/templates/session.py.mako @@ -276,7 +276,18 @@ if grpc_supported: # if ${init_function['python_name']} fails, the error handler can reference it. # And then here, once ${init_function['python_name']} succeeds, we call set_session_handle # with the actual session handle. +% if grpc_supported: + connected = False + try: + self._interpreter.set_session_handle(self.${init_function['python_name']}(${init_call_params})) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('${config['driver_name']}', grpc_options.grpc_channel, connected) +% else: self._interpreter.set_session_handle(self.${init_function['python_name']}(${init_call_params})) +% endif % if config['uses_nitclk']: % if grpc_supported: diff --git a/build/templates/setup.py.mako b/build/templates/setup.py.mako index 6ae7c2aef..7251e26c2 100644 --- a/build/templates/setup.py.mako +++ b/build/templates/setup.py.mako @@ -54,7 +54,7 @@ setup( 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, % endif diff --git a/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py b/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py index 6b479e4b2..353b856a1 100644 --- a/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py +++ b/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -54,7 +55,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidcpower/nidcpower/session.py b/generated/nidcpower/nidcpower/session.py index b4dc5a828..3a9ff0071 100644 --- a/generated/nidcpower/nidcpower/session.py +++ b/generated/nidcpower/nidcpower/session.py @@ -7681,7 +7681,14 @@ def __init__(self, resource_name, channels=None, reset=False, options={}, indepe # if _fancy_initialize fails, the error handler can reference it. # And then here, once _fancy_initialize succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._fancy_initialize(resource_name, channels, reset, options, independent_channels)) + connected = False + try: + self._interpreter.set_session_handle(self._fancy_initialize(resource_name, channels, reset, options, independent_channels)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-DCPower', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/nidcpower/setup.py b/generated/nidcpower/setup.py index 8c6e7aab0..b2d01ae8c 100644 --- a/generated/nidcpower/setup.py +++ b/generated/nidcpower/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nidigital/nidigital/_grpc_stub_interpreter.py b/generated/nidigital/nidigital/_grpc_stub_interpreter.py index 49589cda1..d733b9e7f 100644 --- a/generated/nidigital/nidigital/_grpc_stub_interpreter.py +++ b/generated/nidigital/nidigital/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -52,7 +53,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidigital/nidigital/session.py b/generated/nidigital/nidigital/session.py index 3678e5dc3..858375095 100644 --- a/generated/nidigital/nidigital/session.py +++ b/generated/nidigital/nidigital/session.py @@ -3280,7 +3280,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-Digital Pattern Driver', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nidigital/setup.py b/generated/nidigital/setup.py index 35586e8c5..ec3377e3c 100644 --- a/generated/nidigital/setup.py +++ b/generated/nidigital/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nidmm/nidmm/_grpc_stub_interpreter.py b/generated/nidmm/nidmm/_grpc_stub_interpreter.py index 25b22ab85..91bf0fa8b 100644 --- a/generated/nidmm/nidmm/_grpc_stub_interpreter.py +++ b/generated/nidmm/nidmm/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidmm/nidmm/session.py b/generated/nidmm/nidmm/session.py index 2d04e204e..8f52477db 100644 --- a/generated/nidmm/nidmm/session.py +++ b/generated/nidmm/nidmm/session.py @@ -1049,7 +1049,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-DMM', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/nidmm/setup.py b/generated/nidmm/setup.py index 0d0b67ec4..0a6113944 100644 --- a/generated/nidmm/setup.py +++ b/generated/nidmm/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nifake/nifake/_grpc_stub_interpreter.py b/generated/nifake/nifake/_grpc_stub_interpreter.py index 8991074a0..a3e34085b 100644 --- a/generated/nifake/nifake/_grpc_stub_interpreter.py +++ b/generated/nifake/nifake/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -57,7 +58,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nifake/nifake/session.py b/generated/nifake/nifake/session.py index 2fcfb3755..66741bcab 100644 --- a/generated/nifake/nifake/session.py +++ b/generated/nifake/nifake/session.py @@ -728,7 +728,14 @@ def __init__(self, resource_name, options={}, id_query=False, reset_device=False # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, options, id_query, reset_device)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, options, id_query, reset_device)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-FAKE', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nifake/nifake/unit_tests/conftest.py b/generated/nifake/nifake/unit_tests/conftest.py new file mode 100644 index 000000000..a4aeca550 --- /dev/null +++ b/generated/nifake/nifake/unit_tests/conftest.py @@ -0,0 +1,19 @@ +import grpc +import nitlsconfig.channel_tag +import pytest + + +@pytest.fixture +def nitls_tagged_channel(): + """A real gRPC channel tagged the way nitlsconfig.create_grpc_device_channel tags one. + + nitlsconfig.create_grpc_device_channel itself requires the nitlsconfig library to be + installed on the system in order to access the CLI. As a result, we are unable to directly + leverage it in our unit tests. But, this can at least allow us to unit-test our code + that relies on channels being created from nitlsconfig.create_grpc_device_channel. + """ + target = 'localhost:31763' + with grpc.insecure_channel(target) as channel: + nitlsconfig.channel_tag.tag_channel_target(channel, target) + assert nitlsconfig.channel_tag.is_nitls_channel(channel), 'nitlsconfig no longer recognizes a channel it tagged' + yield channel diff --git a/generated/nifake/nifake/unit_tests/test_grpc.py b/generated/nifake/nifake/unit_tests/test_grpc.py index bd47be561..f47e7e72e 100644 --- a/generated/nifake/nifake/unit_tests/test_grpc.py +++ b/generated/nifake/nifake/unit_tests/test_grpc.py @@ -4,6 +4,7 @@ import math import nifake import nifake.errors +import nitlsconfig import numpy import pytest import session_pb2 @@ -184,6 +185,24 @@ def test_server_unavailable(self): assert e.description == expected_error_message assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_server_unavailable_with_tls_elaboration(self, nitls_tagged_channel): + library_func = 'InitWithOptions' + grpc_error = grpc.StatusCode.UNAVAILABLE + self._set_side_effect(library_func, side_effect=MyRpcError(None, '', grpc_error=grpc_error)) + # The real elaboration, so this fails if nitlsconfig stops recognizing our channel. + expected_error_message = nitlsconfig.get_tls_connection_error_elaboration(nitls_tagged_channel) + assert expected_error_message is not None + assert expected_error_message != 'Failed to connect to server' + grpc_options = nifake.GrpcSessionOptions(nitls_tagged_channel, '', initialization_behavior=nifake.SessionInitializationBehavior.AUTO) + interpreter = nifake._grpc_stub_interpreter.GrpcStubInterpreter(grpc_options) + try: + interpreter.init_with_options('dev1', False, False, '') + assert False + except nifake.Error as e: + assert e.rpc_code == grpc_error + assert e.description == expected_error_message + assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_function_not_implemented(self): library_func = 'PoorlyNamedSimpleFunction' grpc_error = grpc.StatusCode.UNIMPLEMENTED diff --git a/generated/nifake/nifake/unit_tests/test_session.py b/generated/nifake/nifake/unit_tests/test_session.py index 36e45fb06..5166d14a5 100644 --- a/generated/nifake/nifake/unit_tests/test_session.py +++ b/generated/nifake/nifake/unit_tests/test_session.py @@ -68,6 +68,12 @@ def test_init_with_options_nondefault_and_close(self): session.close() self.patched_library_interpreter.close.assert_called_once_with() + def test_init_without_grpc_options_does_not_audit(self): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1'): + pass + patched_audit.assert_not_called() + def test_close(self): session = nifake.Session('dev1') assert session._interpreter._vi == SESSION_NUM_FOR_TEST @@ -966,6 +972,19 @@ def test_init_with_options_and_close(self): session.close() self.patched_grpc_interpreter.close.assert_called_once_with() + def test_init_audits_successful_connect(self, nitls_tagged_channel): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')): + pass + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, True) + + def test_init_audits_failed_connect(self, nitls_tagged_channel): + self.patched_grpc_interpreter.init_with_options.side_effect = nifake.Error('Fake init failure') + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with pytest.raises(nifake.Error): + nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')) + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, False) + # Session locking def test_lock_session_none(self): diff --git a/generated/nifake/setup.py b/generated/nifake/setup.py index 6790a9e3e..5fec3f14d 100644 --- a/generated/nifake/setup.py +++ b/generated/nifake/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nifgen/nifgen/_grpc_stub_interpreter.py b/generated/nifgen/nifgen/_grpc_stub_interpreter.py index 19b06e336..21f50c24d 100644 --- a/generated/nifgen/nifgen/_grpc_stub_interpreter.py +++ b/generated/nifgen/nifgen/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nifgen/nifgen/session.py b/generated/nifgen/nifgen/session.py index 0edd4dec4..0429bfcb3 100644 --- a/generated/nifgen/nifgen/session.py +++ b/generated/nifgen/nifgen/session.py @@ -3166,7 +3166,14 @@ def __init__(self, resource_name, channel_name=None, reset_device=False, options # if _initialize_with_channels fails, the error handler can reference it. # And then here, once _initialize_with_channels succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._initialize_with_channels(resource_name, channel_name, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._initialize_with_channels(resource_name, channel_name, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-FGEN', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nifgen/setup.py b/generated/nifgen/setup.py index 39c7bc426..76ee778cc 100644 --- a/generated/nifgen/setup.py +++ b/generated/nifgen/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py b/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py index d5dc23c2a..e23c12a58 100644 --- a/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py +++ b/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -57,7 +58,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nirfsa/nirfsa/session.py b/generated/nirfsa/nirfsa/session.py index 363cd4862..b241e59f8 100644 --- a/generated/nirfsa/nirfsa/session.py +++ b/generated/nirfsa/nirfsa/session.py @@ -6777,7 +6777,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-RFSA', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nirfsa/setup.py b/generated/nirfsa/setup.py index 0c552d39b..6252a35e7 100644 --- a/generated/nirfsa/setup.py +++ b/generated/nirfsa/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py b/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py index 015f85fd2..a2e2ff3a3 100644 --- a/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py +++ b/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -51,7 +52,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nirfsg/nirfsg/session.py b/generated/nirfsg/nirfsg/session.py index 83ec53bd2..210f8fbc1 100644 --- a/generated/nirfsg/nirfsg/session.py +++ b/generated/nirfsg/nirfsg/session.py @@ -5713,7 +5713,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-RFSG', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nirfsg/setup.py b/generated/nirfsg/setup.py index 63eb43951..baa52e289 100644 --- a/generated/nirfsg/setup.py +++ b/generated/nirfsg/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/niscope/niscope/_grpc_stub_interpreter.py b/generated/niscope/niscope/_grpc_stub_interpreter.py index cf6cd3561..93d12cedb 100644 --- a/generated/niscope/niscope/_grpc_stub_interpreter.py +++ b/generated/niscope/niscope/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -54,7 +55,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/niscope/niscope/session.py b/generated/niscope/niscope/session.py index b17b43b90..c89ddc81b 100644 --- a/generated/niscope/niscope/session.py +++ b/generated/niscope/niscope/session.py @@ -4035,7 +4035,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-SCOPE', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/niscope/setup.py b/generated/niscope/setup.py index 2658d0c76..dc67785cc 100644 --- a/generated/niscope/setup.py +++ b/generated/niscope/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/niswitch/niswitch/_grpc_stub_interpreter.py b/generated/niswitch/niswitch/_grpc_stub_interpreter.py index f25ce9563..26d382e94 100644 --- a/generated/niswitch/niswitch/_grpc_stub_interpreter.py +++ b/generated/niswitch/niswitch/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/niswitch/niswitch/session.py b/generated/niswitch/niswitch/session.py index ad4063da6..c7ab4eb43 100644 --- a/generated/niswitch/niswitch/session.py +++ b/generated/niswitch/niswitch/session.py @@ -1374,7 +1374,14 @@ def __init__(self, resource_name, topology="Configured Topology", simulate=False # if _init_with_topology fails, the error handler can reference it. # And then here, once _init_with_topology succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_topology(resource_name, topology, simulate, reset_device)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_topology(resource_name, topology, simulate, reset_device)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-SWITCH', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/niswitch/setup.py b/generated/niswitch/setup.py index ee9e6e92b..8a81e9577 100644 --- a/generated/niswitch/setup.py +++ b/generated/niswitch/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/src/nifake/unit_tests/conftest.py b/src/nifake/unit_tests/conftest.py new file mode 100644 index 000000000..a4aeca550 --- /dev/null +++ b/src/nifake/unit_tests/conftest.py @@ -0,0 +1,19 @@ +import grpc +import nitlsconfig.channel_tag +import pytest + + +@pytest.fixture +def nitls_tagged_channel(): + """A real gRPC channel tagged the way nitlsconfig.create_grpc_device_channel tags one. + + nitlsconfig.create_grpc_device_channel itself requires the nitlsconfig library to be + installed on the system in order to access the CLI. As a result, we are unable to directly + leverage it in our unit tests. But, this can at least allow us to unit-test our code + that relies on channels being created from nitlsconfig.create_grpc_device_channel. + """ + target = 'localhost:31763' + with grpc.insecure_channel(target) as channel: + nitlsconfig.channel_tag.tag_channel_target(channel, target) + assert nitlsconfig.channel_tag.is_nitls_channel(channel), 'nitlsconfig no longer recognizes a channel it tagged' + yield channel diff --git a/src/nifake/unit_tests/test_grpc.py b/src/nifake/unit_tests/test_grpc.py index bd47be561..f47e7e72e 100644 --- a/src/nifake/unit_tests/test_grpc.py +++ b/src/nifake/unit_tests/test_grpc.py @@ -4,6 +4,7 @@ import math import nifake import nifake.errors +import nitlsconfig import numpy import pytest import session_pb2 @@ -184,6 +185,24 @@ def test_server_unavailable(self): assert e.description == expected_error_message assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_server_unavailable_with_tls_elaboration(self, nitls_tagged_channel): + library_func = 'InitWithOptions' + grpc_error = grpc.StatusCode.UNAVAILABLE + self._set_side_effect(library_func, side_effect=MyRpcError(None, '', grpc_error=grpc_error)) + # The real elaboration, so this fails if nitlsconfig stops recognizing our channel. + expected_error_message = nitlsconfig.get_tls_connection_error_elaboration(nitls_tagged_channel) + assert expected_error_message is not None + assert expected_error_message != 'Failed to connect to server' + grpc_options = nifake.GrpcSessionOptions(nitls_tagged_channel, '', initialization_behavior=nifake.SessionInitializationBehavior.AUTO) + interpreter = nifake._grpc_stub_interpreter.GrpcStubInterpreter(grpc_options) + try: + interpreter.init_with_options('dev1', False, False, '') + assert False + except nifake.Error as e: + assert e.rpc_code == grpc_error + assert e.description == expected_error_message + assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_function_not_implemented(self): library_func = 'PoorlyNamedSimpleFunction' grpc_error = grpc.StatusCode.UNIMPLEMENTED diff --git a/src/nifake/unit_tests/test_session.py b/src/nifake/unit_tests/test_session.py index 36e45fb06..5166d14a5 100644 --- a/src/nifake/unit_tests/test_session.py +++ b/src/nifake/unit_tests/test_session.py @@ -68,6 +68,12 @@ def test_init_with_options_nondefault_and_close(self): session.close() self.patched_library_interpreter.close.assert_called_once_with() + def test_init_without_grpc_options_does_not_audit(self): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1'): + pass + patched_audit.assert_not_called() + def test_close(self): session = nifake.Session('dev1') assert session._interpreter._vi == SESSION_NUM_FOR_TEST @@ -966,6 +972,19 @@ def test_init_with_options_and_close(self): session.close() self.patched_grpc_interpreter.close.assert_called_once_with() + def test_init_audits_successful_connect(self, nitls_tagged_channel): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')): + pass + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, True) + + def test_init_audits_failed_connect(self, nitls_tagged_channel): + self.patched_grpc_interpreter.init_with_options.side_effect = nifake.Error('Fake init failure') + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with pytest.raises(nifake.Error): + nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')) + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, False) + # Session locking def test_lock_session_none(self): diff --git a/tox.ini b/tox.ini index 41333b29e..b42df19fd 100644 --- a/tox.ini +++ b/tox.ini @@ -150,6 +150,7 @@ deps = test: grpcio == 1.75.1 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: protobuf == 5.27.2 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: ni.grpcdevice.v1.proto >= 1.0.0 # Provides session_pb2 package at runtime since we no longer generate it per-driver + test: nitlsconfig >= 1.0.0a2 build_test: pytest build_test: coverage build_test: mako