From ee3a7e286fa56663274227a1605264a09bde8d89 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Thu, 7 Aug 2025 21:10:30 +0100 Subject: [PATCH 01/37] remote-connection server API changed. --- src/ansys/workbench/core/workbench_launcher.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index fe72b98..7b4e6b8 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -148,9 +148,9 @@ def launch( ansys_install_path = self.__getenv("AWP_ROOT" + version) if ansys_install_path: - logging.info("Ansys installation is found at: " + ansys_install_path) + logging.info(f"Ansys installation is found at: {ansys_install_path}") else: - raise Exception("Ansys installation is not found.") + raise Exception(f"Ansys {version} installation is not found.") args = [] if platform.system() == "Windows": @@ -172,10 +172,18 @@ def launch( # use forward slash only to avoid escaping as command line argument server_workdir = server_workdir.replace("\\", "/") cmd += ",WorkingDirectory='" + server_workdir + "'" + if host is not None: + cmd += ",AllowRemoteConnection=True" cmd += ")" args.append(cmd) command_line = " ".join(args) + # security precaution statement + if host is not None: + print("""The server started will allow remote access connections to be +established, possibly permitting control of the machine and any data which resides on it. +It is highly recommended to only utilize these features on a trusted, secure network.""") + successful = False process = None if self._wmi: From 83c2a1d0983e60e164692edde82516fe4fc5d521 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 7 Aug 2025 20:22:34 +0000 Subject: [PATCH 02/37] chore: adding changelog file 237.miscellaneous.md [dependabot-skip] --- doc/changelog.d/237.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/237.miscellaneous.md diff --git a/doc/changelog.d/237.miscellaneous.md b/doc/changelog.d/237.miscellaneous.md new file mode 100644 index 0000000..fde4d41 --- /dev/null +++ b/doc/changelog.d/237.miscellaneous.md @@ -0,0 +1 @@ +remote-connection server API changed. \ No newline at end of file From b5e11dd26d36b8add6cc089442ccb00274e85d86 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 8 Aug 2025 17:07:25 +0100 Subject: [PATCH 03/37] internal_wbexit became available at 25.2 --- src/ansys/workbench/core/public_api.py | 6 +++++- src/ansys/workbench/core/workbench_client.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index d3ab22f..76d2cb0 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -112,12 +112,16 @@ def __init__( port = self._launcher.launch(version, show_gui, server_workdir, host, username, password) if port is None or port <= 0: raise Exception("Filed to launch Ansys Workbench service.") + self.server_version = int(version) super().__init__(port, client_workdir, host) def exit(self): """Terminate the Workbench server and disconnect the client.""" self.run_script_string("Reset()") - self.run_script_string("internal_wbexit()") + if self.server_version >= 252: + self.run_script_string("internal_wbexit()") + else: + self.run_script_string("StopServer()") super().exit() diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 2e6bb3d..088d516 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -74,6 +74,9 @@ def _connect(self): self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info("connected to the WB server at " + hnp) + self.server_version = int(self.run_script_string( + """import json +wb_script_result=json.dumps(GetFrameworkVersion())""").replace('.','')) def _disconnect(self): """Disconnect from the server.""" From 3f2a3e093f9fe3d48446db4aa9ffb4fecb4880f8 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 8 Aug 2025 21:34:01 +0100 Subject: [PATCH 04/37] auto exiting server --- src/ansys/workbench/core/public_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 76d2cb0..8577c6d 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -24,6 +24,7 @@ import logging import tempfile +import atexit from ansys.workbench.core.workbench_client import WorkbenchClient from ansys.workbench.core.workbench_launcher import Launcher @@ -114,9 +115,13 @@ def __init__( raise Exception("Filed to launch Ansys Workbench service.") self.server_version = int(version) super().__init__(port, client_workdir, host) + atexit.register(self.exit) + self._exited = False def exit(self): """Terminate the Workbench server and disconnect the client.""" + if self._exited: + return self.run_script_string("Reset()") if self.server_version >= 252: self.run_script_string("internal_wbexit()") @@ -127,6 +132,7 @@ def exit(self): self._launcher.exit() logging.info("Workbench server connection has ended.") + self._exited = True def launch_workbench( From ede2107f42505f332dd24681854d5dc5823c6f19 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 8 Aug 2025 22:07:57 +0100 Subject: [PATCH 05/37] fix code style --- src/ansys/workbench/core/public_api.py | 2 +- src/ansys/workbench/core/workbench_client.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 8577c6d..37b86a2 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -22,9 +22,9 @@ """Module for public API on PyWorkbench.""" +import atexit import logging import tempfile -import atexit from ansys.workbench.core.workbench_client import WorkbenchClient from ansys.workbench.core.workbench_launcher import Launcher diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 088d516..0cd8807 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -74,9 +74,12 @@ def _connect(self): self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info("connected to the WB server at " + hnp) - self.server_version = int(self.run_script_string( - """import json -wb_script_result=json.dumps(GetFrameworkVersion())""").replace('.','')) + self.server_version = int( + self.run_script_string( + """import json +wb_script_result=json.dumps(GetFrameworkVersion())""" + ).replace('.','') + ) def _disconnect(self): """Disconnect from the server.""" From 2ecfe5a7c444a660700735e253f80fb767d3858a Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 8 Aug 2025 22:10:52 +0100 Subject: [PATCH 06/37] fix code style --- src/ansys/workbench/core/workbench_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 0cd8807..88cf43e 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -78,7 +78,7 @@ def _connect(self): self.run_script_string( """import json wb_script_result=json.dumps(GetFrameworkVersion())""" - ).replace('.','') + ).replace(".", "") ) def _disconnect(self): From 401ea66ee0d4cb9203ca38c6187893fa8fbd3b06 Mon Sep 17 00:00:00 2001 From: Jorge Martinez Date: Tue, 12 Aug 2025 09:41:26 +0200 Subject: [PATCH 07/37] feat: ask user to allow remote host --- src/ansys/workbench/core/workbench_client.py | 44 ++++++++++++++++++- .../workbench/core/workbench_launcher.py | 17 +++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 88cf43e..f4bbf22 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -30,6 +30,7 @@ import os import re import time +import warnings import grpc import tqdm @@ -52,13 +53,52 @@ class WorkbenchClient: Port number of the server. """ - def __init__(self, local_workdir, server_host, server_port): + def __init__(self, local_workdir, server_host, server_port, allow_remote_host=False): """Create a Workbench client.""" self.workdir = local_workdir self._server_host = server_host self._server_port = server_port + self.allow_remote_host = allow_remote_host self.__init_logging() + # HACK: use setters to verify values for host and port + self.host = server_host + self.port = server_port + + @property + def host(self): + """Return the hostname value.""" + return self._server_host + + @host.setter + def host(self, value): + """Set the hostname value.""" + if value not in ["localhost", "127.0.0.1"]: + warnings.warn( + "Allowing remote access can expose the server to unauthorized " + "connections and may transmit data over an unencrypted channel " + "if the server is not properly configured." + ) + if not self.allow_remote_host: + raise ValueError( + "Remote host connections are not permitted by default. " + "To enable connections to hosts other than localhost, set " + "the `allow_remote_host` parameter to `True`." + ) + self._server_host = value + + @property + def port(self): + """Return the value of the port.""" + return self._server_port + + @port.setter(self, value) + def port(self, value: int): + """Set the value of the port.""" + if value < 0 or value > 65535: + raise ValueError("Port value must be in the range of [0, 65535].") + self._server_port = value + def __enter__(self): """Connect to the server when entering a context.""" self._connect() @@ -70,7 +110,7 @@ def __exit__(self, exc_type, exc_value, traceback): def _connect(self): """Connect to the server.""" - hnp = self._server_host + ":" + str(self._server_port) + hnp = self.host + ":" + str(self.port) self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info("connected to the WB server at " + hnp) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 7b4e6b8..5ac9f2c 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -29,6 +29,7 @@ import subprocess import time import uuid +import warnings class Launcher: @@ -77,6 +78,7 @@ def launch( host=None, username=None, password=None, + allow_remote_host=False, ): """Launch PyWorkbench server on the local or a remote computer. @@ -98,6 +100,8 @@ def launch( password : str, default: None User's password on the server. The default is ``None``, which launches Workbench on the local computer. + allow_remote_host : bool + If ``True``, remote host connections are allowed. ``False`` otherwise. Raises ------ @@ -118,6 +122,19 @@ def launch( ): raise Exception("Invalid Ansys version: " + version) + if host not in ["localhost", "127.0.0.1"]: + warnings.warn( + "Allowing remote access can expose the server to unauthorized " + "connections and may transmit data over an unencrypted channel " + "if the server is not properly configured." + ) + if not allow_remote_host: + raise ValueError( + "Remote host connections are not permitted by default. " + "To enable connections to hosts other than localhost, set " + "the `allow_remote_host` parameter to `True`." + ) + if host and not self._wmi: raise Exception( "Launching PyWorkbench on a remote machine from Linux is not supported." From b74da83157a52d6f330be69a6a66a9fa3232b17f Mon Sep 17 00:00:00 2001 From: Frank Li Date: Sun, 14 Sep 2025 17:21:52 +0100 Subject: [PATCH 08/37] correct previous commits --- src/ansys/workbench/core/workbench_client.py | 6 ++---- src/ansys/workbench/core/workbench_launcher.py | 17 ----------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index cfa2ea6..334204b 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -30,7 +30,6 @@ import os import re import time -import warnings import grpc import tqdm @@ -53,13 +52,12 @@ class WorkbenchClient: Port number of the server. """ - def __init__(self, local_workdir, server_host, server_port, allow_remote_host=False): + def __init__(self, local_workdir, server_host, server_port): """Create a Workbench client.""" self.workdir = local_workdir self._server_host = server_host self._server_port = server_port self._server_version = -1 - self.allow_remote_host = allow_remote_host self.__init_logging() @property @@ -83,7 +81,7 @@ def __exit__(self, exc_type, exc_value, traceback): def _connect(self): """Connect to the server.""" - hnp = self.host + ":" + str(self.port) + hnp = self._server_host + ":" + str(self._server_port) self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info(f"connected to the WB server at {hnp}") diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 7790092..e22e8d5 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -29,7 +29,6 @@ import subprocess import time import uuid -import warnings class Launcher: @@ -78,7 +77,6 @@ def launch( host=None, username=None, password=None, - allow_remote_host=False, ): """Launch PyWorkbench server on the local or a remote computer. @@ -100,8 +98,6 @@ def launch( password : str, default: None User's password on the server. The default is ``None``, which launches Workbench on the local computer. - allow_remote_host : bool - If ``True``, remote host connections are allowed. ``False`` otherwise. Raises ------ @@ -122,19 +118,6 @@ def launch( ): raise Exception("Invalid Ansys version: " + version) - if host not in ["localhost", "127.0.0.1"]: - warnings.warn( - "Allowing remote access can expose the server to unauthorized " - "connections and may transmit data over an unencrypted channel " - "if the server is not properly configured." - ) - if not allow_remote_host: - raise ValueError( - "Remote host connections are not permitted by default. " - "To enable connections to hosts other than localhost, set " - "the `allow_remote_host` parameter to `True`." - ) - if host and not self._wmi: raise Exception( "Launching PyWorkbench on a remote machine from Linux is not supported." From 3204efbcb5fcae8a1c877709f835c74f54ec09ce Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 16 Sep 2025 21:16:04 +0100 Subject: [PATCH 09/37] security options --- src/ansys/workbench/core/public_api.py | 26 ++++++--- src/ansys/workbench/core/workbench_client.py | 53 ++++++++++++++++++- .../workbench/core/workbench_launcher.py | 16 ++++-- 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index d0dac04..437c862 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -41,16 +41,18 @@ class ClientWrapper(WorkbenchClient): Path to a writable directory on the client computer. host : str, default: None Server computer's name or IP address. + security : str + Transport mode used for connection security. """ - def __init__(self, port, client_workdir=None, host=None): + def __init__(self, port, client_workdir=None, host=None, security): """Create a PyWorkbench client that connects to a Workbench server.""" if host is None: host = "localhost" if client_workdir is None: client_workdir = tempfile.gettempdir() super().__init__(client_workdir, host, port) - super()._connect() + super()._connect(security) def exit(self): """Disconnect from the server.""" @@ -74,6 +76,8 @@ class LaunchWorkbench(ClientWrapper): server_workdir : str, None Path to a writable directory on the server computer. The default is ``None``, in which case the user preference for the Workbench temporary file folder is used. + use_insecure_connection : bool, default: False + whether to use insecure connection between the server and clients host : str, None Server computer's name or IP address. The default is ``None`` for launching on the local computer. @@ -103,6 +107,7 @@ def __init__( version=None, client_workdir=None, server_workdir=None, + use_insecure_connection=False, host=None, username=None, password=None, @@ -111,10 +116,12 @@ def __init__( version = "252" self._launcher = Launcher() - port = self._launcher.launch(version, show_gui, server_workdir, host, username, password) + port, security = self._launcher.launch(version, show_gui, server_workdir, use_insecure_connection, host, username, password) if port is None or port <= 0: raise Exception("Failed to launch Ansys Workbench service.") - super().__init__(port, client_workdir, host) + if use_insecure_connection: + print ("""Using insecure connection is not recommended. Please see the documentation for your installed product for additional information.""") + super().__init__(port, client_workdir, host, security) atexit.register(self.exit) self._exited = False @@ -136,6 +143,7 @@ def launch_workbench( version=None, client_workdir=None, server_workdir=None, + use_insecure_connection=False, host=None, username=None, password=None, @@ -157,6 +165,8 @@ def launch_workbench( server_workdir : str, None Path to a writable directory on the server computer. The default is ``None``, in which case the user preference for the Workbench temporary file folder is used. + use_insecure_connection : bool, default: False + whether to use insecure connection between the server and clients host : str, None Server computer's name or IP address. The default is ``None`` for launching on the local computer. @@ -181,11 +191,11 @@ def launch_workbench( """ return LaunchWorkbench( - show_gui, version, client_workdir, server_workdir, host, username, password + show_gui, version, client_workdir, server_workdir, use_insecure_connection, host, username, password ) -def connect_workbench(port, client_workdir=None, host=None): +def connect_workbench(port, client_workdir=None, host=None, security='mtls'): """Create a PyWorkbench client that connects to an already running Workbench server. Parameters @@ -197,6 +207,8 @@ def connect_workbench(port, client_workdir=None, host=None): in which case the system temp directory is used. host : str, default: None Server computer's name or IP address. The default is ``None`` for the local computer. + security : str among 'mtls', 'wnua', 'insecure', default: 'mtls' + Transport mode used for connection security. The default is `mtls`. Returns ------- @@ -211,7 +223,7 @@ def connect_workbench(port, client_workdir=None, host=None): >>> from ansys.workbench.core import connect_workbench >>> wb = connect_workbench(port = 32588) """ - return ClientWrapper(port, client_workdir, host) + return ClientWrapper(port, client_workdir, host, security) __all__ = ["launch_workbench", "connect_workbench"] diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 334204b..0b5bc9b 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -22,6 +22,7 @@ """Workbench client module for PyWorkbench.""" +from enum import Enum import glob import json import logging @@ -38,6 +39,14 @@ from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub from ansys.workbench.core.example_data import ExampleData +class SecurityType(str, Enum): + """Enum containing the security types for server connection.""" + + (INSECURE, MTLS, WNUA) = ( + "insecure", + "mtls", + "wnua" + ) class WorkbenchClient: """Functions of a PyWorkbench client. @@ -79,13 +88,55 @@ def __exit__(self, exc_type, exc_value, traceback): """Disconnect from the server when exiting a context.""" self._disconnect() - def _connect(self): + def _connect(self, server_security): """Connect to the server.""" hnp = self._server_host + ":" + str(self._server_port) + + match server_security: + case SecurityType.INSECURE: + self.channel = grpc.insecure_channel(hnp) + case SecurityType.MTLS: + sslCreds = _getSslCreds() + self.channel = grpc.insecure_channel(hnp, sslCreds) + case SecurityType.WNUA: + self.channel = grpc.insecure_channel(hnp, + options=(('grpc.default_authority', 'localhost'),)) + case _: + raise RuntimeError(f"Unknown security type: {server_security}") + self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info(f"connected to the WB server at {hnp}") + def _getSslCreds(self): + # TLS certificates location + if os.environ.get("ANSYS_GRPC_CERTIFICATES"): + certs_folder = os.environ.get("ANSYS_GRPC_CERTIFICATES") + else: + certs_folder = "certs" + + # verify the existence of TLS certificates + client_cert = f"{certs_folder}/client.crt" + client_key = f"{certs_folder}/client.key" + ca_cert = f"{certs_folder}/ca.crt" + + missing = [f for f in (client_cert, client_key, ca_cert) if not os.path.exists(f)] + if missing: + raise RuntimeError(f"Missing required TLS file(s) for mutual TLS: {', '.join(missing)}") + + # create TLS credential + with open(client_cert, 'rb') as f: + certificate_chain = f.read() + with open(client_key, 'rb') as f: + private_key = f.read() + with open(ca_cert, 'rb') as f: + root_certificates = f.read() + return grpc.ssl_channel_credentials( + root_certificates=root_certificates, + private_key=private_key, + certificate_chain=certificate_chain + ) + def _disconnect(self): """Disconnect from the server.""" if self.channel: diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index e22e8d5..ceccd2a 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -74,6 +74,7 @@ def launch( version, show_gui=True, server_workdir=None, + use_insecure_connection=False, host=None, username=None, password=None, @@ -89,6 +90,8 @@ def launch( server_workdir : str, default: None Path to a writable directory on the server. The default is ``None``, in which case the user preference for the Workbench temporary file folder is used. + use_insecure_connection : bool, default: False + whether to use insecure connection between the server and clients host : str, default: None Name or IP address of the server. The default is ``None``, which launches Workbench on the local computer. @@ -129,6 +132,12 @@ def launch( "to launch PyWorkbench on a remote machine." ) + security = "mtls" + if (use_insecure_connection): + security = "insecure" + elsif not host and self._wmi: + security = "wnua" + if self._wmi: try: if not host: @@ -168,6 +177,7 @@ def launch( prefix = uuid.uuid4().hex cmd = "StartServer(EnvironmentPrefix='" cmd += prefix + "'" + cmd += ",Security='" + security + "'" if server_workdir is not None: # use forward slash only to avoid escaping as command line argument server_workdir = server_workdir.replace("\\", "/") @@ -210,7 +220,7 @@ def launch( logging.info("Workbench launched on the host with process ID: " + str(self._process_id)) else: logging.error("Workbench failed to launch on the host.") - return 0 + return 0, security # retrieve server port once WB is fully up running port = None @@ -236,9 +246,9 @@ def launch( time.sleep(10) if not port or int(port) <= 0: logging.error("Failed to retrieve the port used by Workbench service.") - return 0 + return 0, security logging.info("Workbench service uses port: " + port) - return int(port) + return int(port), security def __getenv(self, key): value = None From 33353be2fe0c554f32d8c2c7244e68247869e9c9 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 16 Sep 2025 20:17:48 +0000 Subject: [PATCH 10/37] chore: adding changelog file 254.miscellaneous.md [dependabot-skip] --- doc/changelog.d/254.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/254.miscellaneous.md diff --git a/doc/changelog.d/254.miscellaneous.md b/doc/changelog.d/254.miscellaneous.md new file mode 100644 index 0000000..a035c01 --- /dev/null +++ b/doc/changelog.d/254.miscellaneous.md @@ -0,0 +1 @@ +Fli/handle api change for remote connection From 94f7c370060acd286966bf22626201fa6a7b2025 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 16 Sep 2025 22:11:02 +0100 Subject: [PATCH 11/37] fix code style --- src/ansys/workbench/core/public_api.py | 12 ++++++--- src/ansys/workbench/core/workbench_client.py | 27 +++++++++---------- .../workbench/core/workbench_launcher.py | 2 +- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 437c862..07e01fa 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -45,7 +45,7 @@ class ClientWrapper(WorkbenchClient): Transport mode used for connection security. """ - def __init__(self, port, client_workdir=None, host=None, security): + def __init__(self, port, client_workdir=None, host=None, security="default"): """Create a PyWorkbench client that connects to a Workbench server.""" if host is None: host = "localhost" @@ -116,11 +116,14 @@ def __init__( version = "252" self._launcher = Launcher() - port, security = self._launcher.launch(version, show_gui, server_workdir, use_insecure_connection, host, username, password) + port, security = self._launcher.launch(version, show_gui, server_workdir, + use_insecure_connection, host, username, password) if port is None or port <= 0: raise Exception("Failed to launch Ansys Workbench service.") if use_insecure_connection: - print ("""Using insecure connection is not recommended. Please see the documentation for your installed product for additional information.""") + print ("Using insecure connection is not recommended. " + "Please see the documentation for your installed " + "product for additional information.") super().__init__(port, client_workdir, host, security) atexit.register(self.exit) self._exited = False @@ -191,7 +194,8 @@ def launch_workbench( """ return LaunchWorkbench( - show_gui, version, client_workdir, server_workdir, use_insecure_connection, host, username, password + show_gui, version, client_workdir, server_workdir, use_insecure_connection, + host, username, password ) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 0b5bc9b..4f53801 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -39,14 +39,12 @@ from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub from ansys.workbench.core.example_data import ExampleData + class SecurityType(str, Enum): """Enum containing the security types for server connection.""" - (INSECURE, MTLS, WNUA) = ( - "insecure", - "mtls", - "wnua" - ) + (INSECURE, MTLS, WNUA) = ("insecure", "mtls", "wnua") + class WorkbenchClient: """Functions of a PyWorkbench client. @@ -96,11 +94,12 @@ def _connect(self, server_security): case SecurityType.INSECURE: self.channel = grpc.insecure_channel(hnp) case SecurityType.MTLS: - sslCreds = _getSslCreds() - self.channel = grpc.insecure_channel(hnp, sslCreds) + ssl_creds = self._get_ssl_creds() + self.channel = grpc.insecure_channel(hnp, ssl_creds) case SecurityType.WNUA: - self.channel = grpc.insecure_channel(hnp, - options=(('grpc.default_authority', 'localhost'),)) + self.channel = grpc.insecure_channel( + hnp, options=(('grpc.default_authority', 'localhost'),) + ) case _: raise RuntimeError(f"Unknown security type: {server_security}") @@ -108,7 +107,7 @@ def _connect(self, server_security): self.stub = WorkbenchServiceStub(self.channel) logging.info(f"connected to the WB server at {hnp}") - def _getSslCreds(self): + def _get_ssl_creds(self): # TLS certificates location if os.environ.get("ANSYS_GRPC_CERTIFICATES"): certs_folder = os.environ.get("ANSYS_GRPC_CERTIFICATES") @@ -125,16 +124,16 @@ def _getSslCreds(self): raise RuntimeError(f"Missing required TLS file(s) for mutual TLS: {', '.join(missing)}") # create TLS credential - with open(client_cert, 'rb') as f: + with open(client_cert, "rb") as f: certificate_chain = f.read() - with open(client_key, 'rb') as f: + with open(client_key, "rb") as f: private_key = f.read() - with open(ca_cert, 'rb') as f: + with open(ca_cert, "rb") as f: root_certificates = f.read() return grpc.ssl_channel_credentials( root_certificates=root_certificates, private_key=private_key, - certificate_chain=certificate_chain + certificate_chain=certificate_chain, ) def _disconnect(self): diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index ceccd2a..c05c22f 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -135,7 +135,7 @@ def launch( security = "mtls" if (use_insecure_connection): security = "insecure" - elsif not host and self._wmi: + elif not host and self._wmi: security = "wnua" if self._wmi: From 94b2522be4cdf7bc8a1a21452ae0c78e40e2ac73 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 16 Sep 2025 22:24:44 +0100 Subject: [PATCH 12/37] code style --- src/ansys/workbench/core/public_api.py | 25 +++++++++++++------ src/ansys/workbench/core/workbench_client.py | 2 +- .../workbench/core/workbench_launcher.py | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 07e01fa..4add397 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -116,14 +116,17 @@ def __init__( version = "252" self._launcher = Launcher() - port, security = self._launcher.launch(version, show_gui, server_workdir, - use_insecure_connection, host, username, password) + port, security = self._launcher.launch( + version, show_gui, server_workdir, use_insecure_connection, host, username, password + ) if port is None or port <= 0: raise Exception("Failed to launch Ansys Workbench service.") if use_insecure_connection: - print ("Using insecure connection is not recommended. " - "Please see the documentation for your installed " - "product for additional information.") + print ( + "Using insecure connection is not recommended. " + "Please see the documentation for your installed " + "product for additional information." + ) super().__init__(port, client_workdir, host, security) atexit.register(self.exit) self._exited = False @@ -194,12 +197,18 @@ def launch_workbench( """ return LaunchWorkbench( - show_gui, version, client_workdir, server_workdir, use_insecure_connection, - host, username, password + show_gui, + version, + client_workdir, + server_workdir, + use_insecure_connection, + host, + username, + password, ) -def connect_workbench(port, client_workdir=None, host=None, security='mtls'): +def connect_workbench(port, client_workdir=None, host=None, security="mtls"): """Create a PyWorkbench client that connects to an already running Workbench server. Parameters diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 4f53801..ae20bfb 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -98,7 +98,7 @@ def _connect(self, server_security): self.channel = grpc.insecure_channel(hnp, ssl_creds) case SecurityType.WNUA: self.channel = grpc.insecure_channel( - hnp, options=(('grpc.default_authority', 'localhost'),) + hnp, options=(("grpc.default_authority", "localhost"),) ) case _: raise RuntimeError(f"Unknown security type: {server_security}") diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index c05c22f..5ab6df6 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -133,7 +133,7 @@ def launch( ) security = "mtls" - if (use_insecure_connection): + if use_insecure_connection: security = "insecure" elif not host and self._wmi: security = "wnua" From d128dc355d34195698643bb4195ce709eaf5ffbf Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 16 Sep 2025 22:26:33 +0100 Subject: [PATCH 13/37] style --- src/ansys/workbench/core/public_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 4add397..d75048d 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -122,7 +122,7 @@ def __init__( if port is None or port <= 0: raise Exception("Failed to launch Ansys Workbench service.") if use_insecure_connection: - print ( + print( "Using insecure connection is not recommended. " "Please see the documentation for your installed " "product for additional information." From 60168f77352efaf882dde524003b2f960a5d6bc0 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 03:40:23 +0100 Subject: [PATCH 14/37] handle older install --- src/ansys/workbench/core/workbench_client.py | 5 +---- src/ansys/workbench/core/workbench_launcher.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index ae20bfb..7d8c65c 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -89,21 +89,18 @@ def __exit__(self, exc_type, exc_value, traceback): def _connect(self, server_security): """Connect to the server.""" hnp = self._server_host + ":" + str(self._server_port) - match server_security: case SecurityType.INSECURE: self.channel = grpc.insecure_channel(hnp) case SecurityType.MTLS: ssl_creds = self._get_ssl_creds() - self.channel = grpc.insecure_channel(hnp, ssl_creds) + self.channel = grpc.secure_channel(hnp, ssl_creds) case SecurityType.WNUA: self.channel = grpc.insecure_channel( hnp, options=(("grpc.default_authority", "localhost"),) ) case _: raise RuntimeError(f"Unknown security type: {server_security}") - - self.channel = grpc.insecure_channel(hnp) self.stub = WorkbenchServiceStub(self.channel) logging.info(f"connected to the WB server at {hnp}") diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 5ab6df6..7c9e8c3 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -174,18 +174,24 @@ def launch( args.append("--start-and-wait") args.append("-nowindow") args.append("-E") + + # create command string prefix = uuid.uuid4().hex - cmd = "StartServer(EnvironmentPrefix='" - cmd += prefix + "'" - cmd += ",Security='" + security + "'" + cmd1 = "StartServer(EnvironmentPrefix='" + cmd1 += prefix + "'" if server_workdir is not None: # use forward slash only to avoid escaping as command line argument server_workdir = server_workdir.replace("\\", "/") - cmd += ",WorkingDirectory='" + server_workdir + "'" + cmd1 += ",WorkingDirectory='" + server_workdir + "'" + cmd2 = str(cmd1) + cmd1 += ")" + cmd2 += ",Security='" + security + "'" if host is not None: - cmd += ",AllowRemoteConnection=True" - cmd += ")" + cmd2 += ",AllowRemoteConnection=True" + cmd2 += ")" + cmd = "\"" + cmd2 + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin('Ansys.RemoteWB.Addin').Version.Major > 1 else """ + cmd1 + "\"" args.append(cmd) + command_line = " ".join(args) # security precaution statement From 3d10b890503c292204badf5f0d238f94a7ac94c8 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 03:54:47 +0100 Subject: [PATCH 15/37] code style --- src/ansys/workbench/core/workbench_launcher.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 7c9e8c3..5f44cc4 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -189,7 +189,13 @@ def launch( if host is not None: cmd2 += ",AllowRemoteConnection=True" cmd2 += ")" - cmd = "\"" + cmd2 + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin('Ansys.RemoteWB.Addin').Version.Major > 1 else """ + cmd1 + "\"" + cmd = ( + '"' + + cmd2 + + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin('Ansys.RemoteWB.Addin').Version.Major > 1 else """ + + cmd1 + + '"' + ) args.append(cmd) command_line = " ".join(args) From 00c5b803c93d6ec0a5cce900f1488f089015fb02 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 04:36:49 +0100 Subject: [PATCH 16/37] fix long line --- src/ansys/workbench/core/workbench_launcher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 5f44cc4..394a529 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -192,7 +192,8 @@ def launch( cmd = ( '"' + cmd2 - + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin('Ansys.RemoteWB.Addin').Version.Major > 1 else """ + + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin(""" + + """'Ansys.RemoteWB.Addin').Version.Major > 1 else """ + cmd1 + '"' ) From f6f7cc0fcaade60c7edfb5c9219e67b807c74cd5 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 04:52:28 +0100 Subject: [PATCH 17/37] fix unit tests --- tests/test_workbench_client.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index fec0ef7..301b70a 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -61,7 +61,7 @@ def mock_wb(): def test_connect(mock_grpc, mock_workbench_service_stub): """Test the connect method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_grpc.assert_called_once_with("localhost:5000") mock_workbench_service_stub.assert_called_once() @@ -76,7 +76,7 @@ def test_connect_workbench(): def test_disconnect(): """Test the disconnect method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") client._disconnect() assert client.channel is None assert client.stub is None @@ -85,7 +85,7 @@ def test_disconnect(): def test_is_connected(): """Test the is_connected method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") assert client._is_connected() client._disconnect() assert not client._is_connected() @@ -100,7 +100,7 @@ def test_is_connected(): def test_run_script_string(mock_workbench_service_stub): """Test the run_script_string method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_stub.RunScript.return_value = mock_response @@ -111,7 +111,7 @@ def test_run_script_string(mock_workbench_service_stub): def test_log_file(mock_wb, mock_workbench_service_stub): """Test the log file functionality.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.log.messages = [{"level": 2, "message": "Hello World!"}] @@ -144,7 +144,7 @@ def test_run_script_file(mock_workbench_service_stub): local_workdir = workdir = pathlib.Path(__file__).parent script_dir = workdir / "scripts" client = WorkbenchClient(local_workdir=local_workdir, server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_stub.RunScript.return_value = mock_response @@ -160,7 +160,7 @@ def test_run_script_file(mock_workbench_service_stub): def test_upload_file(mock_workbench_service_stub): """Test the upload_file method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.error = None @@ -178,7 +178,7 @@ def test_upload_file(mock_workbench_service_stub): def test_upload_file_from_example_repo(mock_workbench_service_stub): """Test the upload_file_from_example_repo method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.error = None @@ -204,7 +204,7 @@ def test_upload_iterator(): try: # Create a WorkbenchClient instance client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") # Get the temporary file path file_path = tmp_file.name @@ -226,7 +226,7 @@ def test_upload_iterator(): def test_download_file(mock_workbench_service_stub): """Test the download_file method.""" client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect() + client._connect("insecure") mock_stub = mock_workbench_service_stub.return_value client.stub = mock_stub From dc4db2492ea353bfd4194263bb0e73d5ff6254f8 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 14:12:34 +0100 Subject: [PATCH 18/37] update comment --- src/ansys/workbench/core/public_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index d75048d..19766c9 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -41,7 +41,7 @@ class ClientWrapper(WorkbenchClient): Path to a writable directory on the client computer. host : str, default: None Server computer's name or IP address. - security : str + security : str, default: 'default' Transport mode used for connection security. """ From c5501c84cf270523d5db87c6e72c2438c8aa805e Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 14:22:54 +0100 Subject: [PATCH 19/37] fix unit test --- tests/test_workbench_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index 301b70a..50e9034 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -68,7 +68,7 @@ def test_connect(mock_grpc, mock_workbench_service_stub): def test_connect_workbench(): """Test the connect_workbench method.""" - client = connect_workbench(port=5000, client_workdir="/tmp", host="localhost") + client = connect_workbench(port=5000, client_workdir="/tmp", host="localhost", security="insecure") assert isinstance(client, WorkbenchClient) client.exit() From 02ce3f56e0de329a5d5f9fd4c30dc28404798528 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 17 Sep 2025 14:32:26 +0100 Subject: [PATCH 20/37] code style --- tests/test_workbench_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index 50e9034..dac9a56 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -68,7 +68,9 @@ def test_connect(mock_grpc, mock_workbench_service_stub): def test_connect_workbench(): """Test the connect_workbench method.""" - client = connect_workbench(port=5000, client_workdir="/tmp", host="localhost", security="insecure") + client = connect_workbench( + port=5000, client_workdir="/tmp", host="localhost", security="insecure" + ) assert isinstance(client, WorkbenchClient) client.exit() From 22a226b81133547371d41b8cc2bb6f72c51b15eb Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 1 Oct 2025 01:06:45 +0100 Subject: [PATCH 21/37] typo --- src/ansys/workbench/core/workbench_launcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 394a529..924c1a3 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -128,7 +128,7 @@ def launch( if host and (not username or not password): raise Exception( - "Username and passwork must be specified " + "Username and password must be specified " "to launch PyWorkbench on a remote machine." ) From b26883f4e192404f8c36a44d4d7362b9d1861e4a Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 3 Oct 2025 05:22:21 +0100 Subject: [PATCH 22/37] fix command line quotes --- src/ansys/workbench/core/workbench_launcher.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 26aa503..4bd8a88 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -191,13 +191,18 @@ def launch( if host is not None: cmd2 += ",AllowRemoteConnection=True" cmd2 += ")" + + if self._wmi: + quote_or_not = '"' # quotes needed when constructing command line + else: + quote_or_not = '' cmd = ( - '"' + quote_or_not + cmd2 + """ if __scriptingEngine__.CommandContext.AddinManager.GetAddin(""" + """'Ansys.RemoteWB.Addin').Version.Major > 1 else """ + cmd1 - + '"' + + quote_or_not ) args.append(cmd) From 31549b294a1fa5610797ce7131c6812fab20d8e8 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Fri, 3 Oct 2025 04:24:00 +0000 Subject: [PATCH 23/37] chore: adding changelog file 260.miscellaneous.md [dependabot-skip] --- doc/changelog.d/260.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/260.miscellaneous.md diff --git a/doc/changelog.d/260.miscellaneous.md b/doc/changelog.d/260.miscellaneous.md new file mode 100644 index 0000000..a035c01 --- /dev/null +++ b/doc/changelog.d/260.miscellaneous.md @@ -0,0 +1 @@ +Fli/handle api change for remote connection From 93f10f2ccaf4d2933cf251e0d815f3b06cadca97 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Fri, 3 Oct 2025 05:24:50 +0100 Subject: [PATCH 24/37] fix code style --- src/ansys/workbench/core/workbench_launcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 4bd8a88..a4ebf0f 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -195,7 +195,7 @@ def launch( if self._wmi: quote_or_not = '"' # quotes needed when constructing command line else: - quote_or_not = '' + quote_or_not = "" cmd = ( quote_or_not + cmd2 From d4901595070f79cb8d10e564eec4b56a2ab0205d Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 21 Oct 2025 22:17:44 +0100 Subject: [PATCH 25/37] fix Linux workflow --- .../workbench/core/workbench_launcher.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index a4ebf0f..5579376 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -128,6 +128,11 @@ def launch( "Launching PyWorkbench on a remote machine from Linux is not supported." ) + if not host and not self._wmi and int(version) < 252: + raise Exception( + "Launching PyWorkbench 25.1 or below on Linux is not supported." + ) + if host and (not username or not password): raise Exception( "Username and password must be specified " @@ -248,24 +253,26 @@ def launch( port = None timeout = 60 * 8 # set 8 minutes as upper limit for WB startup start_time = time.time() - while True: - if self._wmi: + if self._wmi: + while True: port = self.__getenv("ANSYS_FRAMEWORK_SERVER_PORT") - else: - for line in process.stdout: - line = line.rstrip() - if line.startswith("ANSYS_FRAMEWORK_SERVER_PORT="): - port = line[28:] + if port and port.startswith(prefix): + port = port[len(prefix) :] + break + else: + port = None + if time.time() - start_time > timeout: + logging.error("Failed to start Workbench service within reasonable timeout.") + break + time.sleep(10) + else: + for line in process.stdout: + line = line.rstrip() + if line.startswith("ANSYS_FRAMEWORK_SERVER_PORT="): + port = line[28:] + if port.startswith(prefix): + port = port[len(prefix) :] break - if port and port.startswith(prefix): - port = port[len(prefix) :] - break - else: - port = None - if time.time() - start_time > timeout: - logging.error("Failed to start Workbench service within reasonable timeout.") - break - time.sleep(10) if not port or int(port) <= 0: logging.error("Failed to retrieve the port used by Workbench service.") return 0, security From 8160f90b19ebee35ef10e32266170b60ea0ceb95 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 5 Nov 2025 16:49:29 +0000 Subject: [PATCH 26/37] testing using cyberchannel package --- src/ansys/workbench/core/cyberchannel.py | 482 +++++++++++++++++++ src/ansys/workbench/core/public_api.py | 6 +- src/ansys/workbench/core/workbench_client.py | 55 +-- 3 files changed, 495 insertions(+), 48 deletions(-) create mode 100644 src/ansys/workbench/core/cyberchannel.py diff --git a/src/ansys/workbench/core/cyberchannel.py b/src/ansys/workbench/core/cyberchannel.py new file mode 100644 index 0000000..d05a6a3 --- /dev/null +++ b/src/ansys/workbench/core/cyberchannel.py @@ -0,0 +1,482 @@ +"""Module to create gRPC channels with different transport modes. + +This module provides functions to create gRPC channels based on the specified +transport mode, including insecure, Unix Domain Sockets (UDS), Windows Named User +Authentication (WNUA), and Mutual TLS (mTLS). + +Example +------- + channel = create_channel( + host="localhost", + port=50051, + transport_mode="mtls", + certs_dir="path/to/certs", + grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)], + ) + stub = hello_pb2_grpc.GreeterStub(channel) + +""" + +# Only the create_channel function is exposed for external use +__all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"] + +import logging +import os +from dataclasses import dataclass +from pathlib import Path +from typing import cast +from warnings import warn +from typing import TypeGuard + +import grpc + +_IS_WINDOWS = os.name == "nt" +LOOPBACK_HOSTS = ("localhost", "127.0.0.1") + +logger = logging.getLogger(__name__) + +@dataclass +class CertificateFiles: + cert_file: str | Path | None = None + key_file: str | Path | None = None + ca_file: str | Path | None = None + +def create_channel( + transport_mode: str, + host: str | None = None, + port: int | str | None = None, + uds_service: str | None = None, + uds_dir: str | Path | None = None, + uds_id: str | None = None, + certs_dir: str | Path | None = None, + cert_files: CertificateFiles | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel based on the transport mode. + + Parameters + ---------- + transport_mode : str + Transport mode selected by the user. + Options are: "insecure", "uds", "wnua", "mtls" + host : str | None + Hostname or IP address of the server. + By default `None` - however, if not using UDS transport mode, + it will be requested. + port : int | str | None + Port in which the server is running. + By default `None` - however, if not using UDS transport mode, + it will be requested. + uds_service : str | None + Optional service name for the UDS socket. + By default `None` - however, if UDS is selected, it will + be requested. + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Optional ID to use for the UDS socket filename. + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + certs_dir : str | Path | None + Directory to use for TLS certificates. + By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. + If not found, it will use the "certs" folder assuming it is in the current working + directory. + cert_files: CertificateFiles | None = None + Path to the client certificate file, client key file, and issuing certificate authority. + By default `None`. + If all three file paths are not all provided, use the certs_dir parameter. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: + if host is None: + raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.") + if port is None: + raise ValueError(f"When using {transport_mode.lower()} transport mode, 'port' must be provided.") + return transport_mode, host, port + + match transport_mode.lower(): + case "insecure": + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_insecure_channel(host, port, grpc_options) + case "uds": + return create_uds_channel(uds_service, uds_dir, uds_id, grpc_options) + case "wnua": + host = host or "localhost" # Default to localhost if not provided + if host not in LOOPBACK_HOSTS: + raise ValueError("Remote host connections are not supported with WNUA.") + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_wnua_channel(host, port, grpc_options) + case "mtls": + transport_mode, host, port = check_host_port(transport_mode, host, port) + return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options) + case _: + raise ValueError( + f"Unknown transport mode: {transport_mode}. " + "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'." + ) + + +##################################### TRANSPORT MODE CHANNELS ##################################### + + +def create_insecure_channel( + host: str, port: int | str, grpc_options: list[tuple[str, object]] | None = None +) -> grpc.Channel: + """Create an insecure gRPC channel without TLS. + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + target = f"{host}:{port}" + warn( + f"Starting gRPC client without TLS on {target}. This is INSECURE. " + "Consider using a secure connection." + ) + logger.info(f"Connecting using INSECURE -> {target}") + return grpc.insecure_channel(target, options=grpc_options) + + +def create_uds_channel( + uds_service: str | None, + uds_dir: str | Path | None = None, + uds_id: str | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Unix Domain Sockets (UDS). + + Parameters + ---------- + uds_service : str + Service name for the UDS socket. + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Optional ID to use for the UDS socket filename. + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus only the default authority option is added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + if not is_uds_supported(): + raise RuntimeError( + "Unix Domain Sockets are not supported on this platform or gRPC version." + ) + + if not uds_service: + raise ValueError("When using UDS transport mode, 'uds_service' must be provided.") + + # Determine UDS folder + uds_folder = determine_uds_folder(uds_dir) + + # Make sure the folder exists + uds_folder.mkdir(parents=True, exist_ok=True) + + # Generate socket filename with optional ID + socket_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" + target = f"unix:{uds_folder / socket_filename}" + # Set default authority to "localhost" for UDS connection + # This is needed to avoid issues with some gRPC implementations, + # see https://github.com/grpc/grpc/issues/34305 + options: list[tuple[str, object]] = [ + ("grpc.default_authority", "localhost"), + ] + if grpc_options: + options.extend(grpc_options) + logger.info(f"Connecting using UDS -> {target}") + return grpc.insecure_channel(target, options=options) + + +def create_wnua_channel( + host: str, + port: int | str, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Windows Named User Authentication (WNUA). + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus only the default authority option is added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + if not _IS_WINDOWS: + raise ValueError("Windows Named User Authentication (WNUA) is only supported on Windows.") + if host not in LOOPBACK_HOSTS: + raise ValueError("Remote host connections are not supported with WNUA.") + + target = f"{host}:{port}" + # Set default authority to "localhost" for WNUA connection + # This is needed to avoid issues with some gRPC implementations, + # see https://github.com/grpc/grpc/issues/34305 + options: list[tuple[str, object]] = [ + ("grpc.default_authority", "localhost"), + ] + if grpc_options: + options.extend(grpc_options) + logger.info(f"Connecting using WNUA -> {target}") + return grpc.insecure_channel(target, options=options) + + +def create_mtls_channel( + host: str, + port: int | str, + certs_dir: str | Path | None = None, + cert_files: CertificateFiles | None = None, + grpc_options: list[tuple[str, object]] | None = None, +) -> grpc.Channel: + """Create a gRPC channel using Mutual TLS (mTLS). + + Parameters + ---------- + host : str + Hostname or IP address of the server. + port : int | str + Port in which the server is running. + certs_dir : str | Path | None + Directory to use for TLS certificates. + By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. + If not found, it will use the "certs" folder assuming it is in the current working + directory. + cert_files: CertificateFiles | None + Path to the client certificate file, client key file, and issuing certificate authority. + By default `None`. + If all three file paths are not all provided, use the certs_dir parameter. + grpc_options: list[tuple[str, object]] | None + gRPC channel options to pass when creating the channel. + Each option is a tuple of the form ("option_name", value). + By default `None` and thus no extra options are added. + + Returns + ------- + grpc.Channel + The created gRPC channel + + """ + certs_folder = None + if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None: + cert_file = Path(cert_files.cert_file).resolve() + key_file = Path(cert_files.key_file).resolve() + ca_file = Path(cert_files.ca_file).resolve() + else: + # Determine certificates folder + if certs_dir: + certs_folder = Path(certs_dir) + elif os.environ.get("ANSYS_GRPC_CERTIFICATES"): + certs_folder = Path(cast(str, os.environ.get("ANSYS_GRPC_CERTIFICATES"))) + else: + certs_folder = Path("certs") + ca_file = certs_folder / "ca.crt" + cert_file = certs_folder / "client.crt" + key_file = certs_folder / "client.key" + + # Load certificates + try: + with (ca_file).open("rb") as f: + trusted_certs = f.read() + with (cert_file).open("rb") as f: + client_cert = f.read() + with (key_file).open("rb") as f: + client_key = f.read() + except FileNotFoundError as e: + error_message = f"Certificate file not found: {e.filename}. " + if certs_folder is not None: + error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ + "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + raise FileNotFoundError(error_message) from e + + # Create SSL credentials + credentials = grpc.ssl_channel_credentials( + root_certificates=trusted_certs, private_key=client_key, certificate_chain=client_cert + ) + + target = f"{host}:{port}" + logger.info(f"Connecting using mTLS -> {target}") + return grpc.secure_channel(target, credentials, options=grpc_options) + + +######################################## HELPER FUNCTIONS ######################################## + + +def version_tuple(version_str: str) -> tuple[int, ...]: + """Convert a version string into a tuple of integers for comparison. + + Parameters + ---------- + version_str : str + The version string to convert. + + Returns + ------- + tuple[int, ...] + A tuple of integers representing the version. + + """ + return tuple(int(x) for x in version_str.split(".")) + + +def check_grpc_version(): + """Check if the installed gRPC version meets the minimum requirement. + + Returns + ------- + bool + True if the gRPC version is sufficient, False otherwise. + + """ + min_version = "1.63.0" + current_version = grpc.__version__ + + try: + return version_tuple(current_version) >= version_tuple(min_version) + except ValueError: + logger.warning("Unable to parse gRPC version.") + return False + + +def is_uds_supported(): + """Check if Unix Domain Sockets (UDS) are supported on the current platform. + + Returns + ------- + bool + True if UDS is supported, False otherwise. + + """ + is_grpc_version_ok = check_grpc_version() + return is_grpc_version_ok if _IS_WINDOWS else True + + +def determine_uds_folder(uds_dir: str | Path | None = None) -> Path: + """Determine the directory to use for Unix Domain Sockets (UDS). + + Parameters + ---------- + uds_dir : str | Path | None + Directory to use for Unix Domain Sockets (UDS) transport mode. + By default `None` and thus it will use the "~/.conn" folder. + + Returns + ------- + Path + The path to the UDS directory. + + """ + # If no directory is provided, use default based on OS + if uds_dir: + return uds_dir if isinstance(uds_dir, Path) else Path(uds_dir) + else: + if _IS_WINDOWS: + return Path(os.environ["USERPROFILE"]) / ".conn" + else: + # Linux/POSIX + return Path(os.environ["HOME"], ".conn") + + +def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None: + """Verify that the provided transport mode is valid. + + Parameters + ---------- + transport_mode : str + The transport mode to verify. + mode : str | None + Can be one of "all", "local" or "remote" to restrict the valid transport modes. + By default `None` and thus all transport modes are accepted. + + Raises + ------ + ValueError + If the transport mode is not one of the accepted values. + + """ + if mode == "local": + valid_modes = {"insecure", "uds", "wnua"} + elif mode == "remote": + valid_modes = {"insecure", "mtls"} + elif mode == "all" or mode is None: + valid_modes = {"insecure", "uds", "wnua", "mtls"} + else: + raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.") + + if transport_mode.lower() not in valid_modes: + raise ValueError( + f"Invalid transport mode: {transport_mode}. " + f"Valid options are: {', '.join(valid_modes)}." + ) + + +def verify_uds_socket( + uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None +) -> bool: + """Verify that the UDS socket file has been created. + + Parameters + ---------- + uds_service : str + Service name for the UDS socket. + uds_dir : Path | None + Directory where the UDS socket file is expected to be (optional). + By default `None` and thus it will use the "~/.conn" folder. + uds_id : str | None + Unique identifier for the UDS socket (optional). + By default `None` and thus it will use ".sock". + Otherwise, the socket filename will be "-.sock". + + Returns + ------- + bool + True if the UDS socket file exists, False otherwise. + """ + # Generate socket filename with optional ID + uds_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" + + # Full path to the UDS socket file + uds_socket_path = determine_uds_folder(uds_dir) / uds_filename + + # Check if the UDS socket file exists + return uds_socket_path.exists() diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 19766c9..36fdc77 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -45,14 +45,14 @@ class ClientWrapper(WorkbenchClient): Transport mode used for connection security. """ - def __init__(self, port, client_workdir=None, host=None, security="default"): + def __init__(self, port, client_workdir=None, host=None, security="mtls"): """Create a PyWorkbench client that connects to a Workbench server.""" if host is None: host = "localhost" if client_workdir is None: client_workdir = tempfile.gettempdir() - super().__init__(client_workdir, host, port) - super()._connect(security) + super().__init__(client_workdir, host, port, security) + super()._connect() def exit(self): """Disconnect from the server.""" diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 7d8c65c..02e3cf1 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -38,6 +38,7 @@ from ansys.api.workbench.v0 import workbench_pb2 as wb from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub from ansys.workbench.core.example_data import ExampleData +from ansys.workbench.core.cyberchannel import create_channel class SecurityType(str, Enum): @@ -59,11 +60,12 @@ class WorkbenchClient: Port number of the server. """ - def __init__(self, local_workdir, server_host, server_port): + def __init__(self, local_workdir, server_host, server_port, server_security): """Create a Workbench client.""" self.workdir = local_workdir self._server_host = server_host self._server_port = server_port + self._server_security = server_security self._server_version = -1 self.__init_logging() @@ -86,52 +88,15 @@ def __exit__(self, exc_type, exc_value, traceback): """Disconnect from the server when exiting a context.""" self._disconnect() - def _connect(self, server_security): + def _connect(self): """Connect to the server.""" - hnp = self._server_host + ":" + str(self._server_port) - match server_security: - case SecurityType.INSECURE: - self.channel = grpc.insecure_channel(hnp) - case SecurityType.MTLS: - ssl_creds = self._get_ssl_creds() - self.channel = grpc.secure_channel(hnp, ssl_creds) - case SecurityType.WNUA: - self.channel = grpc.insecure_channel( - hnp, options=(("grpc.default_authority", "localhost"),) - ) - case _: - raise RuntimeError(f"Unknown security type: {server_security}") - self.stub = WorkbenchServiceStub(self.channel) - logging.info(f"connected to the WB server at {hnp}") - - def _get_ssl_creds(self): - # TLS certificates location - if os.environ.get("ANSYS_GRPC_CERTIFICATES"): - certs_folder = os.environ.get("ANSYS_GRPC_CERTIFICATES") - else: - certs_folder = "certs" - - # verify the existence of TLS certificates - client_cert = f"{certs_folder}/client.crt" - client_key = f"{certs_folder}/client.key" - ca_cert = f"{certs_folder}/ca.crt" - - missing = [f for f in (client_cert, client_key, ca_cert) if not os.path.exists(f)] - if missing: - raise RuntimeError(f"Missing required TLS file(s) for mutual TLS: {', '.join(missing)}") - - # create TLS credential - with open(client_cert, "rb") as f: - certificate_chain = f.read() - with open(client_key, "rb") as f: - private_key = f.read() - with open(ca_cert, "rb") as f: - root_certificates = f.read() - return grpc.ssl_channel_credentials( - root_certificates=root_certificates, - private_key=private_key, - certificate_chain=certificate_chain, + self.channel = create_channel( + host=self._server_host, port=self._server_port, + transport_mode=self._server_security, certs_dir=None, + grpc_options=None, ) + self.stub = WorkbenchServiceStub(self.channel) + logging.info(f"connected to the WB server at {self._server_host}:{self._server_port} using {self._server_security} connection") def _disconnect(self): """Disconnect from the server.""" From 19189974ec023ceeb0c4184b08e2af5b84d06915 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:51:18 +0000 Subject: [PATCH 27/37] chore: adding changelog file 269.miscellaneous.md [dependabot-skip] --- doc/changelog.d/269.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/269.miscellaneous.md diff --git a/doc/changelog.d/269.miscellaneous.md b/doc/changelog.d/269.miscellaneous.md new file mode 100644 index 0000000..a035c01 --- /dev/null +++ b/doc/changelog.d/269.miscellaneous.md @@ -0,0 +1 @@ +Fli/handle api change for remote connection From f7f666ec21e04dc9ae5e41d85356f334a2d096d7 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 5 Nov 2025 17:15:31 +0000 Subject: [PATCH 28/37] fix code style --- src/ansys/workbench/core/cyberchannel.py | 29 ++++++++++++++----- src/ansys/workbench/core/workbench_client.py | 13 +++++---- .../workbench/core/workbench_launcher.py | 4 +-- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/ansys/workbench/core/cyberchannel.py b/src/ansys/workbench/core/cyberchannel.py index d05a6a3..f3c08df 100644 --- a/src/ansys/workbench/core/cyberchannel.py +++ b/src/ansys/workbench/core/cyberchannel.py @@ -20,13 +20,12 @@ # Only the create_channel function is exposed for external use __all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"] +from dataclasses import dataclass import logging import os -from dataclasses import dataclass from pathlib import Path from typing import cast from warnings import warn -from typing import TypeGuard import grpc @@ -35,12 +34,14 @@ logger = logging.getLogger(__name__) + @dataclass class CertificateFiles: cert_file: str | Path | None = None key_file: str | Path | None = None ca_file: str | Path | None = None + def create_channel( transport_mode: str, host: str | None = None, @@ -98,11 +99,16 @@ def create_channel( The created gRPC channel """ + def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: if host is None: - raise ValueError(f"When using {transport_mode.lower()} transport mode, 'host' must be provided.") + raise ValueError( + f"When using {transport_mode.lower()} transport mode, 'host' must be provided." + ) if port is None: - raise ValueError(f"When using {transport_mode.lower()} transport mode, 'port' must be provided.") + raise ValueError( + f"When using {transport_mode.lower()} transport mode, 'port' must be provided." + ) return transport_mode, host, port match transport_mode.lower(): @@ -112,7 +118,7 @@ def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: case "uds": return create_uds_channel(uds_service, uds_dir, uds_id, grpc_options) case "wnua": - host = host or "localhost" # Default to localhost if not provided + host = host or "localhost" # Default to localhost if not provided if host not in LOOPBACK_HOSTS: raise ValueError("Remote host connections are not supported with WNUA.") transport_mode, host, port = check_host_port(transport_mode, host, port) @@ -298,7 +304,12 @@ def create_mtls_channel( """ certs_folder = None - if cert_files is not None and cert_files.cert_file is not None and cert_files.key_file is not None and cert_files.ca_file is not None: + if ( + cert_files is not None + and cert_files.cert_file is not None + and cert_files.key_file is not None + and cert_files.ca_file is not None + ): cert_file = Path(cert_files.cert_file).resolve() key_file = Path(cert_files.key_file).resolve() ca_file = Path(cert_files.ca_file).resolve() @@ -325,8 +336,10 @@ def create_mtls_channel( except FileNotFoundError as e: error_message = f"Certificate file not found: {e.filename}. " if certs_folder is not None: - error_message += f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ - "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + error_message += ( + f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ + "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." + ) raise FileNotFoundError(error_message) from e # Create SSL credentials diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 02e3cf1..6be3b03 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -32,13 +32,12 @@ import re import time -import grpc import tqdm from ansys.api.workbench.v0 import workbench_pb2 as wb from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub -from ansys.workbench.core.example_data import ExampleData from ansys.workbench.core.cyberchannel import create_channel +from ansys.workbench.core.example_data import ExampleData class SecurityType(str, Enum): @@ -91,12 +90,16 @@ def __exit__(self, exc_type, exc_value, traceback): def _connect(self): """Connect to the server.""" self.channel = create_channel( - host=self._server_host, port=self._server_port, - transport_mode=self._server_security, certs_dir=None, + host=self._server_host, + port=self._server_port, + transport_mode=self._server_security, + certs_dir=None, grpc_options=None, ) self.stub = WorkbenchServiceStub(self.channel) - logging.info(f"connected to the WB server at {self._server_host}:{self._server_port} using {self._server_security} connection") + logging.info( + f"connected to the WB server at {self._server_host}:{self._server_port} using {self._server_security} connection" + ) def _disconnect(self): """Disconnect from the server.""" diff --git a/src/ansys/workbench/core/workbench_launcher.py b/src/ansys/workbench/core/workbench_launcher.py index 5579376..2822110 100644 --- a/src/ansys/workbench/core/workbench_launcher.py +++ b/src/ansys/workbench/core/workbench_launcher.py @@ -129,9 +129,7 @@ def launch( ) if not host and not self._wmi and int(version) < 252: - raise Exception( - "Launching PyWorkbench 25.1 or below on Linux is not supported." - ) + raise Exception("Launching PyWorkbench 25.1 or below on Linux is not supported.") if host and (not username or not password): raise Exception( From 2cfcc5b8e4142edd1df219029a239113b109bca3 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 5 Nov 2025 17:29:30 +0000 Subject: [PATCH 29/37] update code style --- src/ansys/workbench/core/cyberchannel.py | 2 +- src/ansys/workbench/core/workbench_client.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ansys/workbench/core/cyberchannel.py b/src/ansys/workbench/core/cyberchannel.py index f3c08df..0dba783 100644 --- a/src/ansys/workbench/core/cyberchannel.py +++ b/src/ansys/workbench/core/cyberchannel.py @@ -337,7 +337,7 @@ def create_mtls_channel( error_message = f"Certificate file not found: {e.filename}. " if certs_folder is not None: error_message += ( - f"Ensure that the certificates are present in the '{certs_folder}' folder or " \ + f"Ensure that the certificates are present in the '{certs_folder}' folder or " "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." ) raise FileNotFoundError(error_message) from e diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 6be3b03..f718f80 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -98,7 +98,8 @@ def _connect(self): ) self.stub = WorkbenchServiceStub(self.channel) logging.info( - f"connected to the WB server at {self._server_host}:{self._server_port} using {self._server_security} connection" + f"connected to the WB server at {self._server_host}:{self._server_port} " + "using {self._server_security} connection" ) def _disconnect(self): From fde061c40f51675bd13db14549f9b3c355a631b5 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 5 Nov 2025 17:32:32 +0000 Subject: [PATCH 30/37] add copyright --- src/ansys/workbench/core/cyberchannel.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/ansys/workbench/core/cyberchannel.py b/src/ansys/workbench/core/cyberchannel.py index 0dba783..1b4f93b 100644 --- a/src/ansys/workbench/core/cyberchannel.py +++ b/src/ansys/workbench/core/cyberchannel.py @@ -1,3 +1,25 @@ +# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + """Module to create gRPC channels with different transport modes. This module provides functions to create gRPC channels based on the specified From 35320e9a79c7a028e4bcc528150466f175166f01 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 25 Nov 2025 15:44:53 +0000 Subject: [PATCH 31/37] use ansys-common-tools --- pyproject.toml | 1 + src/ansys/workbench/core/cyberchannel.py | 517 ------------------- src/ansys/workbench/core/workbench_client.py | 2 +- 3 files changed, 2 insertions(+), 518 deletions(-) delete mode 100644 src/ansys/workbench/core/cyberchannel.py diff --git a/pyproject.toml b/pyproject.toml index 98bc5ea..eebd9aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "ansys-platform-instancemanagement>=1.0.1", "ansys-pythonnet>=3.1.0rc1", "ansys-tools-path>=0.3.1", + "ansys-tools-common>=0.3.0", "tqdm>=4.65.0", "WMI>=1.4.9; platform_system=='Windows'", ] diff --git a/src/ansys/workbench/core/cyberchannel.py b/src/ansys/workbench/core/cyberchannel.py deleted file mode 100644 index 1b4f93b..0000000 --- a/src/ansys/workbench/core/cyberchannel.py +++ /dev/null @@ -1,517 +0,0 @@ -# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates. -# SPDX-License-Identifier: MIT -# -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -"""Module to create gRPC channels with different transport modes. - -This module provides functions to create gRPC channels based on the specified -transport mode, including insecure, Unix Domain Sockets (UDS), Windows Named User -Authentication (WNUA), and Mutual TLS (mTLS). - -Example -------- - channel = create_channel( - host="localhost", - port=50051, - transport_mode="mtls", - certs_dir="path/to/certs", - grpc_options=[('grpc.max_receive_message_length', 50 * 1024 * 1024)], - ) - stub = hello_pb2_grpc.GreeterStub(channel) - -""" - -# Only the create_channel function is exposed for external use -__all__ = ["create_channel", "verify_transport_mode", "verify_uds_socket"] - -from dataclasses import dataclass -import logging -import os -from pathlib import Path -from typing import cast -from warnings import warn - -import grpc - -_IS_WINDOWS = os.name == "nt" -LOOPBACK_HOSTS = ("localhost", "127.0.0.1") - -logger = logging.getLogger(__name__) - - -@dataclass -class CertificateFiles: - cert_file: str | Path | None = None - key_file: str | Path | None = None - ca_file: str | Path | None = None - - -def create_channel( - transport_mode: str, - host: str | None = None, - port: int | str | None = None, - uds_service: str | None = None, - uds_dir: str | Path | None = None, - uds_id: str | None = None, - certs_dir: str | Path | None = None, - cert_files: CertificateFiles | None = None, - grpc_options: list[tuple[str, object]] | None = None, -) -> grpc.Channel: - """Create a gRPC channel based on the transport mode. - - Parameters - ---------- - transport_mode : str - Transport mode selected by the user. - Options are: "insecure", "uds", "wnua", "mtls" - host : str | None - Hostname or IP address of the server. - By default `None` - however, if not using UDS transport mode, - it will be requested. - port : int | str | None - Port in which the server is running. - By default `None` - however, if not using UDS transport mode, - it will be requested. - uds_service : str | None - Optional service name for the UDS socket. - By default `None` - however, if UDS is selected, it will - be requested. - uds_dir : str | Path | None - Directory to use for Unix Domain Sockets (UDS) transport mode. - By default `None` and thus it will use the "~/.conn" folder. - uds_id : str | None - Optional ID to use for the UDS socket filename. - By default `None` and thus it will use ".sock". - Otherwise, the socket filename will be "-.sock". - certs_dir : str | Path | None - Directory to use for TLS certificates. - By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. - If not found, it will use the "certs" folder assuming it is in the current working - directory. - cert_files: CertificateFiles | None = None - Path to the client certificate file, client key file, and issuing certificate authority. - By default `None`. - If all three file paths are not all provided, use the certs_dir parameter. - grpc_options: list[tuple[str, object]] | None - gRPC channel options to pass when creating the channel. - Each option is a tuple of the form ("option_name", value). - By default `None` and thus no extra options are added. - - Returns - ------- - grpc.Channel - The created gRPC channel - - """ - - def check_host_port(transport_mode, host, port) -> tuple[str, str, str]: - if host is None: - raise ValueError( - f"When using {transport_mode.lower()} transport mode, 'host' must be provided." - ) - if port is None: - raise ValueError( - f"When using {transport_mode.lower()} transport mode, 'port' must be provided." - ) - return transport_mode, host, port - - match transport_mode.lower(): - case "insecure": - transport_mode, host, port = check_host_port(transport_mode, host, port) - return create_insecure_channel(host, port, grpc_options) - case "uds": - return create_uds_channel(uds_service, uds_dir, uds_id, grpc_options) - case "wnua": - host = host or "localhost" # Default to localhost if not provided - if host not in LOOPBACK_HOSTS: - raise ValueError("Remote host connections are not supported with WNUA.") - transport_mode, host, port = check_host_port(transport_mode, host, port) - return create_wnua_channel(host, port, grpc_options) - case "mtls": - transport_mode, host, port = check_host_port(transport_mode, host, port) - return create_mtls_channel(host, port, certs_dir, cert_files, grpc_options) - case _: - raise ValueError( - f"Unknown transport mode: {transport_mode}. " - "Valid options are: 'insecure', 'uds', 'wnua', 'mtls'." - ) - - -##################################### TRANSPORT MODE CHANNELS ##################################### - - -def create_insecure_channel( - host: str, port: int | str, grpc_options: list[tuple[str, object]] | None = None -) -> grpc.Channel: - """Create an insecure gRPC channel without TLS. - - Parameters - ---------- - host : str - Hostname or IP address of the server. - port : int | str - Port in which the server is running. - grpc_options: list[tuple[str, object]] | None - gRPC channel options to pass when creating the channel. - Each option is a tuple of the form ("option_name", value). - By default `None` and thus no extra options are added. - - Returns - ------- - grpc.Channel - The created gRPC channel - - """ - target = f"{host}:{port}" - warn( - f"Starting gRPC client without TLS on {target}. This is INSECURE. " - "Consider using a secure connection." - ) - logger.info(f"Connecting using INSECURE -> {target}") - return grpc.insecure_channel(target, options=grpc_options) - - -def create_uds_channel( - uds_service: str | None, - uds_dir: str | Path | None = None, - uds_id: str | None = None, - grpc_options: list[tuple[str, object]] | None = None, -) -> grpc.Channel: - """Create a gRPC channel using Unix Domain Sockets (UDS). - - Parameters - ---------- - uds_service : str - Service name for the UDS socket. - uds_dir : str | Path | None - Directory to use for Unix Domain Sockets (UDS) transport mode. - By default `None` and thus it will use the "~/.conn" folder. - uds_id : str | None - Optional ID to use for the UDS socket filename. - By default `None` and thus it will use ".sock". - Otherwise, the socket filename will be "-.sock". - grpc_options: list[tuple[str, object]] | None - gRPC channel options to pass when creating the channel. - Each option is a tuple of the form ("option_name", value). - By default `None` and thus only the default authority option is added. - - Returns - ------- - grpc.Channel - The created gRPC channel - - """ - if not is_uds_supported(): - raise RuntimeError( - "Unix Domain Sockets are not supported on this platform or gRPC version." - ) - - if not uds_service: - raise ValueError("When using UDS transport mode, 'uds_service' must be provided.") - - # Determine UDS folder - uds_folder = determine_uds_folder(uds_dir) - - # Make sure the folder exists - uds_folder.mkdir(parents=True, exist_ok=True) - - # Generate socket filename with optional ID - socket_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" - target = f"unix:{uds_folder / socket_filename}" - # Set default authority to "localhost" for UDS connection - # This is needed to avoid issues with some gRPC implementations, - # see https://github.com/grpc/grpc/issues/34305 - options: list[tuple[str, object]] = [ - ("grpc.default_authority", "localhost"), - ] - if grpc_options: - options.extend(grpc_options) - logger.info(f"Connecting using UDS -> {target}") - return grpc.insecure_channel(target, options=options) - - -def create_wnua_channel( - host: str, - port: int | str, - grpc_options: list[tuple[str, object]] | None = None, -) -> grpc.Channel: - """Create a gRPC channel using Windows Named User Authentication (WNUA). - - Parameters - ---------- - host : str - Hostname or IP address of the server. - port : int | str - Port in which the server is running. - grpc_options: list[tuple[str, object]] | None - gRPC channel options to pass when creating the channel. - Each option is a tuple of the form ("option_name", value). - By default `None` and thus only the default authority option is added. - - Returns - ------- - grpc.Channel - The created gRPC channel - - """ - if not _IS_WINDOWS: - raise ValueError("Windows Named User Authentication (WNUA) is only supported on Windows.") - if host not in LOOPBACK_HOSTS: - raise ValueError("Remote host connections are not supported with WNUA.") - - target = f"{host}:{port}" - # Set default authority to "localhost" for WNUA connection - # This is needed to avoid issues with some gRPC implementations, - # see https://github.com/grpc/grpc/issues/34305 - options: list[tuple[str, object]] = [ - ("grpc.default_authority", "localhost"), - ] - if grpc_options: - options.extend(grpc_options) - logger.info(f"Connecting using WNUA -> {target}") - return grpc.insecure_channel(target, options=options) - - -def create_mtls_channel( - host: str, - port: int | str, - certs_dir: str | Path | None = None, - cert_files: CertificateFiles | None = None, - grpc_options: list[tuple[str, object]] | None = None, -) -> grpc.Channel: - """Create a gRPC channel using Mutual TLS (mTLS). - - Parameters - ---------- - host : str - Hostname or IP address of the server. - port : int | str - Port in which the server is running. - certs_dir : str | Path | None - Directory to use for TLS certificates. - By default `None` and thus search for the "ANSYS_GRPC_CERTIFICATES" environment variable. - If not found, it will use the "certs" folder assuming it is in the current working - directory. - cert_files: CertificateFiles | None - Path to the client certificate file, client key file, and issuing certificate authority. - By default `None`. - If all three file paths are not all provided, use the certs_dir parameter. - grpc_options: list[tuple[str, object]] | None - gRPC channel options to pass when creating the channel. - Each option is a tuple of the form ("option_name", value). - By default `None` and thus no extra options are added. - - Returns - ------- - grpc.Channel - The created gRPC channel - - """ - certs_folder = None - if ( - cert_files is not None - and cert_files.cert_file is not None - and cert_files.key_file is not None - and cert_files.ca_file is not None - ): - cert_file = Path(cert_files.cert_file).resolve() - key_file = Path(cert_files.key_file).resolve() - ca_file = Path(cert_files.ca_file).resolve() - else: - # Determine certificates folder - if certs_dir: - certs_folder = Path(certs_dir) - elif os.environ.get("ANSYS_GRPC_CERTIFICATES"): - certs_folder = Path(cast(str, os.environ.get("ANSYS_GRPC_CERTIFICATES"))) - else: - certs_folder = Path("certs") - ca_file = certs_folder / "ca.crt" - cert_file = certs_folder / "client.crt" - key_file = certs_folder / "client.key" - - # Load certificates - try: - with (ca_file).open("rb") as f: - trusted_certs = f.read() - with (cert_file).open("rb") as f: - client_cert = f.read() - with (key_file).open("rb") as f: - client_key = f.read() - except FileNotFoundError as e: - error_message = f"Certificate file not found: {e.filename}. " - if certs_folder is not None: - error_message += ( - f"Ensure that the certificates are present in the '{certs_folder}' folder or " - "set the 'ANSYS_GRPC_CERTIFICATES' environment variable." - ) - raise FileNotFoundError(error_message) from e - - # Create SSL credentials - credentials = grpc.ssl_channel_credentials( - root_certificates=trusted_certs, private_key=client_key, certificate_chain=client_cert - ) - - target = f"{host}:{port}" - logger.info(f"Connecting using mTLS -> {target}") - return grpc.secure_channel(target, credentials, options=grpc_options) - - -######################################## HELPER FUNCTIONS ######################################## - - -def version_tuple(version_str: str) -> tuple[int, ...]: - """Convert a version string into a tuple of integers for comparison. - - Parameters - ---------- - version_str : str - The version string to convert. - - Returns - ------- - tuple[int, ...] - A tuple of integers representing the version. - - """ - return tuple(int(x) for x in version_str.split(".")) - - -def check_grpc_version(): - """Check if the installed gRPC version meets the minimum requirement. - - Returns - ------- - bool - True if the gRPC version is sufficient, False otherwise. - - """ - min_version = "1.63.0" - current_version = grpc.__version__ - - try: - return version_tuple(current_version) >= version_tuple(min_version) - except ValueError: - logger.warning("Unable to parse gRPC version.") - return False - - -def is_uds_supported(): - """Check if Unix Domain Sockets (UDS) are supported on the current platform. - - Returns - ------- - bool - True if UDS is supported, False otherwise. - - """ - is_grpc_version_ok = check_grpc_version() - return is_grpc_version_ok if _IS_WINDOWS else True - - -def determine_uds_folder(uds_dir: str | Path | None = None) -> Path: - """Determine the directory to use for Unix Domain Sockets (UDS). - - Parameters - ---------- - uds_dir : str | Path | None - Directory to use for Unix Domain Sockets (UDS) transport mode. - By default `None` and thus it will use the "~/.conn" folder. - - Returns - ------- - Path - The path to the UDS directory. - - """ - # If no directory is provided, use default based on OS - if uds_dir: - return uds_dir if isinstance(uds_dir, Path) else Path(uds_dir) - else: - if _IS_WINDOWS: - return Path(os.environ["USERPROFILE"]) / ".conn" - else: - # Linux/POSIX - return Path(os.environ["HOME"], ".conn") - - -def verify_transport_mode(transport_mode: str, mode: str | None = None) -> None: - """Verify that the provided transport mode is valid. - - Parameters - ---------- - transport_mode : str - The transport mode to verify. - mode : str | None - Can be one of "all", "local" or "remote" to restrict the valid transport modes. - By default `None` and thus all transport modes are accepted. - - Raises - ------ - ValueError - If the transport mode is not one of the accepted values. - - """ - if mode == "local": - valid_modes = {"insecure", "uds", "wnua"} - elif mode == "remote": - valid_modes = {"insecure", "mtls"} - elif mode == "all" or mode is None: - valid_modes = {"insecure", "uds", "wnua", "mtls"} - else: - raise ValueError(f"Invalid mode: {mode}. Valid options are: 'all', 'local', 'remote'.") - - if transport_mode.lower() not in valid_modes: - raise ValueError( - f"Invalid transport mode: {transport_mode}. " - f"Valid options are: {', '.join(valid_modes)}." - ) - - -def verify_uds_socket( - uds_service: str, uds_dir: Path | None = None, uds_id: str | None = None -) -> bool: - """Verify that the UDS socket file has been created. - - Parameters - ---------- - uds_service : str - Service name for the UDS socket. - uds_dir : Path | None - Directory where the UDS socket file is expected to be (optional). - By default `None` and thus it will use the "~/.conn" folder. - uds_id : str | None - Unique identifier for the UDS socket (optional). - By default `None` and thus it will use ".sock". - Otherwise, the socket filename will be "-.sock". - - Returns - ------- - bool - True if the UDS socket file exists, False otherwise. - """ - # Generate socket filename with optional ID - uds_filename = f"{uds_service}-{uds_id}.sock" if uds_id else f"{uds_service}.sock" - - # Full path to the UDS socket file - uds_socket_path = determine_uds_folder(uds_dir) / uds_filename - - # Check if the UDS socket file exists - return uds_socket_path.exists() diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index f718f80..582bddc 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -36,7 +36,7 @@ from ansys.api.workbench.v0 import workbench_pb2 as wb from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub -from ansys.workbench.core.cyberchannel import create_channel +from ansys.common.tools.cyberchannel import create_channel from ansys.workbench.core.example_data import ExampleData From 38cefd2135b1e2da11f37a87bc563ed8fbe0c625 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 25 Nov 2025 15:47:56 +0000 Subject: [PATCH 32/37] chore: adding changelog file 277.maintenance.md [dependabot-skip] --- doc/changelog.d/277.maintenance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/277.maintenance.md diff --git a/doc/changelog.d/277.maintenance.md b/doc/changelog.d/277.maintenance.md new file mode 100644 index 0000000..a035c01 --- /dev/null +++ b/doc/changelog.d/277.maintenance.md @@ -0,0 +1 @@ +Fli/handle api change for remote connection From 28b4b43eb46afd4800dd0f769aaee7d08e16185a Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 25 Nov 2025 16:06:26 +0000 Subject: [PATCH 33/37] correct namaspace --- src/ansys/workbench/core/workbench_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 582bddc..fbeeb56 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -36,7 +36,7 @@ from ansys.api.workbench.v0 import workbench_pb2 as wb from ansys.api.workbench.v0.workbench_pb2_grpc import WorkbenchServiceStub -from ansys.common.tools.cyberchannel import create_channel +from ansys.tools.common.cyberchannel import create_channel from ansys.workbench.core.example_data import ExampleData From 98ff98899ca1c51cef625927434c5488accc5058 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 25 Nov 2025 19:41:50 +0000 Subject: [PATCH 34/37] fix unit tests --- src/ansys/workbench/core/public_api.py | 3 +- src/ansys/workbench/core/workbench_client.py | 9 ++-- tests/test_workbench_client.py | 45 ++++++++++---------- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/ansys/workbench/core/public_api.py b/src/ansys/workbench/core/public_api.py index 36fdc77..2245761 100644 --- a/src/ansys/workbench/core/public_api.py +++ b/src/ansys/workbench/core/public_api.py @@ -41,8 +41,9 @@ class ClientWrapper(WorkbenchClient): Path to a writable directory on the client computer. host : str, default: None Server computer's name or IP address. - security : str, default: 'default' + security : str, default: 'mtls' Transport mode used for connection security. + Options are: "insecure", "uds", "wnua", "mtls" """ def __init__(self, port, client_workdir=None, host=None, security="mtls"): diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index fbeeb56..1885ce5 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -40,12 +40,6 @@ from ansys.workbench.core.example_data import ExampleData -class SecurityType(str, Enum): - """Enum containing the security types for server connection.""" - - (INSECURE, MTLS, WNUA) = ("insecure", "mtls", "wnua") - - class WorkbenchClient: """Functions of a PyWorkbench client. @@ -57,6 +51,9 @@ class WorkbenchClient: Hostname or IP address of the server. server_port : int Port number of the server. + server_security : string + Security mode of the server. + Options are: "insecure", "uds", "wnua", "mtls" """ def __init__(self, local_workdir, server_host, server_port, server_security): diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index dac9a56..1d149c9 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -25,6 +25,7 @@ import pathlib import tempfile from unittest.mock import MagicMock, patch +import grpc import pytest @@ -36,7 +37,7 @@ @pytest.fixture def mock_grpc(): """Mock the insecure_channel method.""" - with patch("ansys.workbench.core.workbench_client.grpc.insecure_channel") as mock_channel: + with patch("grpc.insecure_channel") as mock_channel: yield mock_channel @@ -60,8 +61,8 @@ def mock_wb(): def test_connect(mock_grpc, mock_workbench_service_stub): """Test the connect method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_grpc.assert_called_once_with("localhost:5000") mock_workbench_service_stub.assert_called_once() @@ -77,8 +78,8 @@ def test_connect_workbench(): def test_disconnect(): """Test the disconnect method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() client._disconnect() assert client.channel is None assert client.stub is None @@ -86,23 +87,23 @@ def test_disconnect(): def test_is_connected(): """Test the is_connected method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() assert client._is_connected() client._disconnect() assert not client._is_connected() # def test_set_console_log_level(mock_wb): -# client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) +# client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") # client.set_console_log_level("warning") # assert client.__log_console_handler.level == logging.DEBUG def test_run_script_string(mock_workbench_service_stub): """Test the run_script_string method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_stub.RunScript.return_value = mock_response @@ -112,8 +113,8 @@ def test_run_script_string(mock_workbench_service_stub): def test_log_file(mock_wb, mock_workbench_service_stub): """Test the log file functionality.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.log.messages = [{"level": 2, "message": "Hello World!"}] @@ -145,8 +146,8 @@ def test_run_script_file(mock_workbench_service_stub): """Test the run_script_file method.""" local_workdir = workdir = pathlib.Path(__file__).parent script_dir = workdir / "scripts" - client = WorkbenchClient(local_workdir=local_workdir, server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir=local_workdir, server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_stub.RunScript.return_value = mock_response @@ -161,8 +162,8 @@ def test_run_script_file(mock_workbench_service_stub): def test_upload_file(mock_workbench_service_stub): """Test the upload_file method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.error = None @@ -179,8 +180,8 @@ def test_upload_file(mock_workbench_service_stub): def test_upload_file_from_example_repo(mock_workbench_service_stub): """Test the upload_file_from_example_repo method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() mock_response.error = None @@ -205,8 +206,8 @@ def test_upload_iterator(): try: # Create a WorkbenchClient instance - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() # Get the temporary file path file_path = tmp_file.name @@ -227,8 +228,8 @@ def test_upload_iterator(): def test_download_file(mock_workbench_service_stub): """Test the download_file method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000) - client._connect("insecure") + client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client._connect() mock_stub = mock_workbench_service_stub.return_value client.stub = mock_stub From f345a778526ece61422a8ec9f9c0f9dad946752c Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 25 Nov 2025 20:36:18 +0000 Subject: [PATCH 35/37] fix code style --- src/ansys/workbench/core/workbench_client.py | 1 - tests/test_workbench_client.py | 51 +++++++++++++++----- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/ansys/workbench/core/workbench_client.py b/src/ansys/workbench/core/workbench_client.py index 1885ce5..34ecf63 100644 --- a/src/ansys/workbench/core/workbench_client.py +++ b/src/ansys/workbench/core/workbench_client.py @@ -22,7 +22,6 @@ """Workbench client module for PyWorkbench.""" -from enum import Enum import glob import json import logging diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index 1d149c9..13594f1 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -25,7 +25,6 @@ import pathlib import tempfile from unittest.mock import MagicMock, patch -import grpc import pytest @@ -61,7 +60,9 @@ def mock_wb(): def test_connect(mock_grpc, mock_workbench_service_stub): """Test the connect method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_grpc.assert_called_once_with("localhost:5000") mock_workbench_service_stub.assert_called_once() @@ -78,7 +79,9 @@ def test_connect_workbench(): def test_disconnect(): """Test the disconnect method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() client._disconnect() assert client.channel is None @@ -87,7 +90,9 @@ def test_disconnect(): def test_is_connected(): """Test the is_connected method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() assert client._is_connected() client._disconnect() @@ -95,14 +100,18 @@ def test_is_connected(): # def test_set_console_log_level(mock_wb): -# client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") +# client = WorkbenchClient( +# local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" +# ) # client.set_console_log_level("warning") # assert client.__log_console_handler.level == logging.DEBUG def test_run_script_string(mock_workbench_service_stub): """Test the run_script_string method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() @@ -113,7 +122,9 @@ def test_run_script_string(mock_workbench_service_stub): def test_log_file(mock_wb, mock_workbench_service_stub): """Test the log file functionality.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() @@ -146,7 +157,12 @@ def test_run_script_file(mock_workbench_service_stub): """Test the run_script_file method.""" local_workdir = workdir = pathlib.Path(__file__).parent script_dir = workdir / "scripts" - client = WorkbenchClient(local_workdir=local_workdir, server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir=local_workdir, + server_host="localhost", + server_port=5000, + server_security="insecure", + ) client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() @@ -162,7 +178,9 @@ def test_run_script_file(mock_workbench_service_stub): def test_upload_file(mock_workbench_service_stub): """Test the upload_file method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() @@ -180,7 +198,9 @@ def test_upload_file(mock_workbench_service_stub): def test_upload_file_from_example_repo(mock_workbench_service_stub): """Test the upload_file_from_example_repo method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_stub = mock_workbench_service_stub.return_value mock_response = MagicMock() @@ -206,7 +226,12 @@ def test_upload_iterator(): try: # Create a WorkbenchClient instance - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", + server_host="localhost", + server_port=5000, + server_security="insecure", + ) client._connect() # Get the temporary file path @@ -228,7 +253,9 @@ def test_upload_iterator(): def test_download_file(mock_workbench_service_stub): """Test the download_file method.""" - client = WorkbenchClient(local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure") + client = WorkbenchClient( + local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" + ) client._connect() mock_stub = mock_workbench_service_stub.return_value client.stub = mock_stub From 6340ef9d94c09a8ce1af9054bc01fab63e238e4f Mon Sep 17 00:00:00 2001 From: Frank Li Date: Tue, 25 Nov 2025 20:59:59 +0000 Subject: [PATCH 36/37] fix code style --- tests/test_workbench_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index 13594f1..5c4df8b 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -101,7 +101,10 @@ def test_is_connected(): # def test_set_console_log_level(mock_wb): # client = WorkbenchClient( -# local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" +# local_workdir="/tmp", +# server_host="localhost", +# server_port=5000, +# server_security="insecure", # ) # client.set_console_log_level("warning") # assert client.__log_console_handler.level == logging.DEBUG From f5fb1a50ef29bf7cffbced030d4c638cf24a9846 Mon Sep 17 00:00:00 2001 From: Frank Li Date: Wed, 26 Nov 2025 18:20:40 +0000 Subject: [PATCH 37/37] fix unit test --- tests/test_workbench_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workbench_client.py b/tests/test_workbench_client.py index 5c4df8b..14a689c 100644 --- a/tests/test_workbench_client.py +++ b/tests/test_workbench_client.py @@ -64,7 +64,7 @@ def test_connect(mock_grpc, mock_workbench_service_stub): local_workdir="/tmp", server_host="localhost", server_port=5000, server_security="insecure" ) client._connect() - mock_grpc.assert_called_once_with("localhost:5000") + mock_grpc.assert_called_once_with("localhost:5000", options=None) mock_workbench_service_stub.assert_called_once()