diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 303d64e66a..39642f894d 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -15,8 +15,8 @@ on: env: DOCKER_IMAGE_NAME: ghcr.io/ansys/prime - DOCKER_IMAGE_TAG: '25.1.2' - MAIN_PYTHON_VERSION: '3.12' + DOCKER_IMAGE_TAG: '25.1.4' + MAIN_PYTHON_VERSION: '3.10' PACKAGE_NAME: 'ansys-meshing-prime' PACKAGE_NAMESPACE: 'ansys.meshing.prime' DOCUMENTATION_CNAME: 'prime.docs.pyansys.com' @@ -102,6 +102,9 @@ jobs: - name: Setup headless display uses: pyvista/setup-headless-display-action@v4 + - name: Install compatible pyvista and vtk + run: pip install "pyvista>=0.42" vtk + - name: "Run Ansys documentation building action" uses: ansys/actions/doc-build@v9 with: @@ -114,6 +117,7 @@ jobs: PYPRIMEMESH_SPHINX_BUILD: 1 PYPRIMEMESH_IMAGE_TAG: ${{ env.DOCKER_IMAGE_TAG }} ANSYSLMD_LICENSE_FILE: '1055@${{ secrets.LICENSE_SERVER }}' + PRIME_MODE: "GRPC_INSECURE" testing: name: Run Unit Tests @@ -137,6 +141,9 @@ jobs: path: tests/graphics/image_cache key: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }}-${{ hashFiles('pyproject.toml') }} restore-keys: pyvista-image-cache-${{ runner.os }}-v-${{ env.RESET_IMAGE_CACHE }} + + - name: Install compatible pyvista and vtk + run: pip install "pyvista>=0.42" vtk - name: "Run pytest" uses: ansys/actions/tests-pytest@v9 diff --git a/doc/changelog.d/1188.documentation.md b/doc/changelog.d/1188.documentation.md new file mode 100644 index 0000000000..1b05d09373 --- /dev/null +++ b/doc/changelog.d/1188.documentation.md @@ -0,0 +1 @@ +updates to release/0.8 \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index ba5436dae6..fdd5ab0c15 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -10,6 +10,7 @@ from ansys.meshing.prime import __version__ +os.environ["PRIME_MODE"] = "GRPC_INSECURE" viz_interface.DOCUMENTATION_BUILD = True # Project information diff --git a/doc/source/getting_started/index.rst b/doc/source/getting_started/index.rst index ea814d85ae..2f5015be99 100644 --- a/doc/source/getting_started/index.rst +++ b/doc/source/getting_started/index.rst @@ -65,6 +65,60 @@ To install a basic version of the client, use this command instead: pip install -e . +Connecting through gRPC +----------------------- + +PyPrimeMesh uses gRPC to provide secure communications between client and server. +When you run the client and server on the same machine: + +- For Linux OS, PyPrimeMesh uses UDS (Unix Domain Socket) for communications. + +- For Windows OS, PyPrimeMesh uses interceptor to validate gRPC connections, + ensures the client is running on the same Windows user account as the server and authenticates the client. + +When you launch PyPrimeMesh, gRPC establishes a connection between the Client and Server +through the secure option. **Secure** is the default option when you use launch_prime(). +You should always use the **Secure** option to establish a secured connection between the client and server. + +When you want to make an insecure connection between the client and server, +you may need to specify the connection type as follows: + +.. code-block:: python + + client = prime.launch_prime( + connection_type=prime.internals.config.ConnectionType.GRPC_INSECURE + ) + +.. note:: + Insecure option is not recommended. + +Connect securely using certificates +-------------------------------------- + +PyPrimeMesh offers secure connection using certificates. For secure connection with mutual TLS (mTLS), you may pass a client certificate directory and server certificate directory using client_certs_dir and server_certs_dir respectively to launch_prime(). + +client_certs_dir should contain the following files: + +- client.crt + +- client.key + +- ca.crt + +server_certs_dir should contain the following files: + +- server.crt + +- server.key + +- ca.crt + +.. note:: + - Ensure that ca.crt file is the same for the client and the server. You should not modify the + file names in the client_certs_dir and server_certs_dir respectively. + + - The path of input the files must be the same for server and client and should be on the shared network. + Dependencies ------------ diff --git a/doc/styles/config/vocabularies/ANSYS/accept.txt b/doc/styles/config/vocabularies/ANSYS/accept.txt index 2187858b64..70833f0aa4 100644 --- a/doc/styles/config/vocabularies/ANSYS/accept.txt +++ b/doc/styles/config/vocabularies/ANSYS/accept.txt @@ -6,6 +6,7 @@ automesh Boolean BRep CAD +client_certs_dir conformally [Dd]efeature defeaturing diff --git a/docker/build_docker_linux.py b/docker/build_docker_linux.py index 15fa381814..074a47fbd4 100644 --- a/docker/build_docker_linux.py +++ b/docker/build_docker_linux.py @@ -143,12 +143,38 @@ def create_docker_image(dest_package_path): # Build the docker image print(">>> Building docker image. This might take some time...") + # Parse version from AWP_ROOT or command line argument + version = "latest" # default + if len(sys.argv) >= 3: + version = sys.argv[2] + + # Get the script directory for robust file path handling + script_dir = os.path.dirname(os.path.abspath(__file__)) + dockerfile_path = os.path.join(script_dir, "linux", "Dockerfile") + + # Verify Dockerfile exists + if not os.path.exists(dockerfile_path): + print(f"XXXXXXX Dockerfile not found at {dockerfile_path}. Exiting process. XXXXXXX") + exit(1) + + # Build Docker image with version tag + image_tag = f"ghcr.io/ansys/prime:{version}" + print(f">>> Building Docker image with tag: {image_tag}") + out = subprocess.run( - ["docker", "build", "-f", "linux/Dockerfile", "-t", "ghcr.io/ansys/prime:latest", "."], - cwd=os.path.dirname(os.path.abspath(__file__)), + ["docker", "build", "-f", dockerfile_path, "-t", image_tag, script_dir], capture_output=True, ) + # Check if docker build was successful + if out.returncode != 0: + print("XXXXXXX Docker build failed. XXXXXXX") + print("STDOUT:", out.stdout.decode()) + print("STDERR:", out.stderr.decode()) + exit(1) + else: + print(f">>> Docker image built successfully with tag: {image_tag}") + # ------------------------------------------------------------------------------- # diff --git a/docker/linux/Dockerfile b/docker/linux/Dockerfile index 3e1a6f272f..5ea991848f 100644 --- a/docker/linux/Dockerfile +++ b/docker/linux/Dockerfile @@ -37,4 +37,4 @@ ENV AWP_ROOT251="/prime" LABEL org.opencontainers.image.authors="ANSYS Inc." LABEL org.opencontainers.image.vendor="ANSYS Inc." -ENTRYPOINT [ "/prime/meshing/Prime/runPrime.sh", "server", "--ip", "0.0.0.0" ] +ENTRYPOINT [ "/prime/meshing/Prime/runPrime.sh", "server", "--ip", "0.0.0.0" ] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 581727e946..57e2f08f58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "flit_core.buildapi" [project] name = "ansys-meshing-prime" -version = "0.8.1" +version = "0.8.2" description = "PyPrimeMesh is a Python client to Ansys Prime Server, which delivers core Ansys meshing technology." readme = "README.md" requires-python = ">=3.10,<4" @@ -37,17 +37,17 @@ tests = [ "pytest-cov==6.1.1", "pytest-pyvista==0.1.9", "pytest-xvfb==3.1.1", - "pyvista[trame]==0.44.2" + "pyvista[trame]<=0.45.3" ] doc = [ "ansys-sphinx-theme[autoapi]==1.4.2", - "ansys-tools-visualization-interface==0.8.3", + "ansys-tools-visualization-interface<=0.11.0", "jupyter-sphinx==0.5.3", "numpydoc==1.8.0", "sphinx>=8.0.0,<8.2.0", "sphinx_design==0.6.1", "pyvista==0.44.2", - "sphinx-autodoc-typehints==3.1.0", + "sphinx-autodoc-typehints==3.0.1", "sphinx-copybutton==0.5.2", "sphinx-gallery==0.19.0", "sphinx-notfound-page==1.1.0", diff --git a/src/ansys/meshing/prime/internals/client.py b/src/ansys/meshing/prime/internals/client.py index 29764709c8..de5271286e 100644 --- a/src/ansys/meshing/prime/internals/client.py +++ b/src/ansys/meshing/prime/internals/client.py @@ -1,7 +1,6 @@ # Copyright (C) 2024 - 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 @@ -24,6 +23,7 @@ import logging import os +from typing import Optional import ansys.meshing.prime.examples as examples import ansys.meshing.prime.internals.config as config @@ -50,7 +50,8 @@ class Client(object): Maximum time to wait for connection. The default is ``defaults.connection_timeout()``. credentials : Any, optional Credentials to connect to the server. The default is ``None``. - + client_certs_dir : Optional[str] + Directory containing client certificates for mutual TLS. Raises ------ ValueError @@ -65,6 +66,9 @@ def __init__( port: int = defaults.port(), timeout: float = defaults.connection_timeout(), credentials=None, + connection_type: config.ConnectionType = config.ConnectionType.GRPC_SECURE, + uds_file: Optional[str] = None, + client_certs_dir: Optional[str] = None, **kwargs, ): """Initialize the client.""" @@ -72,33 +76,78 @@ def __init__( local = kwargs.get('local', False) if local and server_process is not None: raise ValueError('Local client cannot be instantiated with a server process') + + if connection_type == config.ConnectionType.GRPC_INSECURE: + print("Warning (Client): Modification of these configurations is not recommended.") + print( + "Please see the documentation for your installed product for additional information" + ) + self._local = local self._process = server_process self._comm = None if not local: - try: - from ansys.meshing.prime.internals.grpc_communicator import ( - GRPCCommunicator, - ) + if ( + connection_type == config.ConnectionType.GRPC_SECURE + or connection_type == config.ConnectionType.GRPC_INSECURE + ): + try: + from ansys.meshing.prime.internals.grpc_communicator import ( + GRPCCommunicator, + ) - channel = kwargs.get('channel', None) - if channel is not None: - self._comm = GRPCCommunicator(channel=channel, timeout=timeout) - else: - self._comm = GRPCCommunicator( - ip=ip, port=port, timeout=timeout, credentials=credentials + channel = kwargs.get('channel', None) + + if channel is not None: + self._comm = GRPCCommunicator(channel=channel, timeout=timeout) + else: + if ( + os.name == 'nt' + or connection_type == config.ConnectionType.GRPC_INSECURE + ): + if ( + connection_type == config.ConnectionType.GRPC_INSECURE + and client_certs_dir is not None + ): + print( + "Warning: Ignoring client certificate \ +directory for insecure connections" + ) + client_certs_dir = None + self._comm = GRPCCommunicator( + ip=ip, + port=port, + timeout=timeout, + credentials=credentials, + client_certs_dir=client_certs_dir, + ) + else: + if uds_file is None: + self._comm = GRPCCommunicator( + ip=ip, + port=port, + client_certs_dir=client_certs_dir, + timeout=timeout, + ) + else: + self._comm = GRPCCommunicator( + uds_file=uds_file, timeout=timeout, credentials=credentials + ) + setattr(self, 'port', port) + except ImportError as err: + logging.getLogger('PyPrimeMesh').error( + f'Failed to load grpc_communicator with message: {err.msg}' ) - setattr(self, 'port', port) - except ImportError as err: - logging.getLogger('PyPrimeMesh').error( - f'Failed to load grpc_communicator with message: {err.msg}' - ) + raise + except ConnectionError: + self.exit() + + logging.getLogger('PyPrimeMesh').error('Failed to connect to PRIME GRPC server') + raise + else: + logging.getLogger('PyPrimeMesh').error(f'Invalid server type: {connection_type}') raise - except ConnectionError: - self.exit() - logging.getLogger('PyPrimeMesh').error('Failed to connect to PRIME GRPC server') - raise else: try: from ansys.meshing.prime.internals.prime_communicator import ( @@ -161,14 +210,12 @@ def exit(self): assert self._local == False terminate_process(self._process) self._process = None - if config.using_container(): container_name = getattr(self, 'container_name') utils.stop_prime_github_container(container_name) elif config.has_pim(): self.remote_instance.delete() self.pim_client.close() - clear_examples = bool(int(os.environ.get('PYPRIMEMESH_CLEAR_EXAMPLES', '1'))) if clear_examples: download_manager = examples.DownloadManager() diff --git a/src/ansys/meshing/prime/internals/config.py b/src/ansys/meshing/prime/internals/config.py index c0a26c1e48..ecd7a2a994 100644 --- a/src/ansys/meshing/prime/internals/config.py +++ b/src/ansys/meshing/prime/internals/config.py @@ -23,6 +23,7 @@ """Configuration utility for PyPrimeMesh.""" from contextlib import contextmanager +from enum import Enum __all__ = [ 'enable_optimizing_numpy_arrays', @@ -45,6 +46,11 @@ from ansys.meshing.prime.internals.logger import PrimeLogger +class ConnectionType(Enum): + GRPC_SECURE = (1,) + GRPC_INSECURE = (2,) + + def _optimize_vectors(): """Get the value of the flag for optimizing vectors.""" return __DEFAULT_USE_BINARY diff --git a/src/ansys/meshing/prime/internals/grpc_communicator.py b/src/ansys/meshing/prime/internals/grpc_communicator.py index ffc9110742..d9795a1f02 100644 --- a/src/ansys/meshing/prime/internals/grpc_communicator.py +++ b/src/ansys/meshing/prime/internals/grpc_communicator.py @@ -1,7 +1,6 @@ # Copyright (C) 2024 - 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 @@ -22,6 +21,7 @@ """Module for communications with the gRPC server.""" __all__ = ['GRPCCommunicator'] +import os from typing import Optional import grpc @@ -42,6 +42,50 @@ BUFFER_MESSAGE_LENGTH = defaults.max_message_length() - 100 +def get_secure_channel(client_certs_dir: str, server_host: str, server_port: int): + """Create a secure gRPC channel using the provided TLS files. + + Parameters + ---------- + tls_client_files : list + List of paths to the TLS files. The list should contain: + - client certificate file path + - client key file path + - CA certificate file path + Returns + ------- + grpc.Channel + A secure gRPC channel. + """ + target = f"{server_host}:{server_port}" + + if not os.path.exists(client_certs_dir): + raise FileNotFoundError(f"Client certificates directory does not exist: {client_certs_dir}") + + cert_file = f"{client_certs_dir}/client.crt" + key_file = f"{client_certs_dir}/client.key" + ca_file = f"{client_certs_dir}/ca.crt" + + with open(cert_file, 'rb') as f: + certificate_chain = f.read() + with open(key_file, 'rb') as f: + private_key = f.read() + with open(ca_file, 'rb') as f: + root_certificates = f.read() + + try: + creds = grpc.ssl_channel_credentials( + root_certificates=root_certificates, + private_key=private_key, + certificate_chain=certificate_chain, + ) + except Exception as e: + raise RuntimeError(f"Failed to create SSL channel credentials: {e}") + + channel = grpc.secure_channel(target, creds) + return channel + + def make_chunks(data, chunk_size): n = max(1, chunk_size) return (data[i : i + n] for i in range(0, len(data), n)) @@ -80,7 +124,10 @@ class GRPCCommunicator(Communicator): Maximum time to wait for connection. The default is ``10.0``. credentials : Any, optional Credentials for connecting to the server. The default is ``None``. - + uds_file : Optional[str], optional + Path to the Unix Domain Socket (UDS) file. The default is ``None``. + client_certs_dir : Optional[str], optional + Directory containing client certificates for mutual TLS. The default is ``None``. Raises ------ ConnectionError @@ -93,18 +140,30 @@ def __init__( port: Optional[int] = None, timeout: float = 10.0, credentials=None, + uds_file: Optional[str] = None, + client_certs_dir: Optional[str] = None, **kwargs, ): """Initialize the server connection.""" import os self._channel = kwargs.get('channel', None) + if self._channel is None and client_certs_dir is not None: + self._channel = get_secure_channel( + client_certs_dir=client_certs_dir, server_host=ip, server_port=port + ) + self._models = [] if self._channel is None: ip_addr = f"{ip}:{port}" channel_options = grpc_utils.get_default_channel_args() if credentials is None: - self._channel = grpc.insecure_channel(ip_addr, options=channel_options) + if uds_file is not None: + options = (('grpc.default_authority', 'localhost'),) + self._channel = grpc.insecure_channel(uds_file, options=options) + else: + options = (('grpc.default_authority', 'localhost'),) + self._channel = grpc.insecure_channel(ip_addr, options=options) else: self._channel = grpc.secure_channel(ip_addr, credentials, options=channel_options) @@ -279,47 +338,6 @@ def run_on_server(self, model: Model, recipe: str) -> dict: else: raise RuntimeError("No connection with server") - def server_command(self, command: str, *args) -> dict: - """Run commands on the server. - - Parameters - ---------- - command : str - Commands to run. - - Returns - ------- - dict - Result from the server side. - - Raises - ------ - RuntimeError - Bad response from server. - RuntimeError - Can not connect to server. - """ - if self._stub is not None: - command = {"Command": command} - if len(args) > 0: - command.update({"Args": args[0]}) - - response = self._stub.ServerCommand( - request_iterator( - 0, - json.dumps(command), - prime_pb2.StringMessage, - prime_pb2.Model, - prime_pb2.StringJsonContent, - prime_pb2.MessageCompletionToken, - ) - ) - message = get_response(response, '') - return message - else: - raise RuntimeError("No connection with server") - return {} - def close(self): """Close opened channels.""" self._stub = None diff --git a/src/ansys/meshing/prime/internals/launcher.py b/src/ansys/meshing/prime/internals/launcher.py index 87317c2882..11d40ef25a 100644 --- a/src/ansys/meshing/prime/internals/launcher.py +++ b/src/ansys/meshing/prime/internals/launcher.py @@ -1,7 +1,6 @@ # Copyright (C) 2024 - 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 @@ -25,6 +24,7 @@ import os import subprocess import sys +import uuid from typing import Optional import ansys.meshing.prime.internals.config as config @@ -73,6 +73,8 @@ def launch_server_process( ip: str = defaults.ip(), port: int = defaults.port(), n_procs: Optional[int] = None, + connection_type: config.ConnectionType = None, + server_certs_dir: Optional[str] = None, **kw, ) -> subprocess.Popen: """Launch a server process for Ansys Prime Server. @@ -90,6 +92,8 @@ def launch_server_process( processes to spawn. The default is ``None``, in which case the server is launched as the only process (normal mode). The process marked as ``Node 0`` hosts the gRPC server. + server_certs_dir : Optional[str] + Directory containing server certificates for mutual TLS. Returns ------- @@ -113,7 +117,7 @@ def launch_server_process( run_prime_script = f'runPrime.{script_ext}' exec_path = os.path.join(prime_root, run_prime_script) - print('Using Ansys Prime Server from {prime_root}'.format(prime_root=prime_root)) + print(f'Launching Ansys Prime Server from {prime_root}') logging.getLogger('PyPrimeMesh').info('Using server from %s', prime_root) if not os.path.isfile(exec_path): raise FileNotFoundError(f'{run_prime_script} not found in {prime_root}') @@ -124,6 +128,7 @@ def launch_server_process( kw = {} enable_python_server = kw.get('server', 'release') + communicator_type = kw.get('communicator_type', 'grpc') scheduler = kw.get('scheduler', None) if not isinstance(enable_python_server, str): @@ -146,6 +151,7 @@ def launch_server_process( if enable_python_server == 'debug': server_args.append('-debug') + server_args.append(f'--type={communicator_type}') server_args.append(f'--ip={ip}') server_args.append(f'--port={port}') if n_procs is not None and isinstance(n_procs, int): @@ -155,6 +161,9 @@ def launch_server_process( server_args.append(f'--scheduler') server_args.append(f'{scheduler}') + if os.name != 'nt' and connection_type == config.ConnectionType.GRPC_SECURE: + server_args.append(f'--uds={kw.get("uds_file", "")}') + kwargs = { 'stdin': subprocess.DEVNULL, } @@ -164,6 +173,14 @@ def launch_server_process( if sys.platform.startswith('win32'): kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP + if connection_type == config.ConnectionType.GRPC_SECURE: + server_args.append("--secure=yes") + else: + server_args.append("--secure=no") + + if server_certs_dir is not None: + server_args.append(f"--server_cert_dir={server_certs_dir}") + logging.getLogger('PyPrimeMesh').info('Launching Ansys Prime Server') server = subprocess.Popen(server_args, **kwargs) return server @@ -178,7 +195,7 @@ def launch_remote_prime( This method creates a file transfer service that is available on Ansys Lab. """ if version is None: - version = '251-sp2' + version = 'latest' pim = pypim.connect() instance = pim.create_instance(product_name='prime', product_version=version) @@ -213,6 +230,9 @@ def launch_prime( ip: str = defaults.ip(), port: int = defaults.port(), timeout: float = defaults.connection_timeout(), + connection_type: config.ConnectionType = config.ConnectionType.GRPC_SECURE, + client_certs_dir: Optional[str] = None, + server_certs_dir: Optional[str] = None, n_procs: Optional[int] = None, version: Optional[str] = None, **kwargs, @@ -235,6 +255,10 @@ def launch_prime( processes to spawn. The default is ``None``, in which case the server is launched as the only process (normal mode). The process marked as ``Node 0`` hosts the gRPC server. + client_certs_dir : Optional[str] + Directory containing client certificates for mutual TLS. + server_certs_dir : Optional[str] + Directory containing server certificates for mutual TLS. Returns ------- @@ -255,19 +279,61 @@ def launch_prime( if ip == defaults.ip(): port = utils.get_available_local_port(port) + channel = None + if ( + ip not in ["127.0.0.1", "localhost"] + and connection_type == config.ConnectionType.GRPC_SECURE + ): + if client_certs_dir is None or server_certs_dir is None: + raise RuntimeError(f"Please provide certificate directory for remote connections.") + missing = [ + f + for f in [ + f"{client_certs_dir}/client.crt", + f"{client_certs_dir}/client.key", + f"{client_certs_dir}/ca.crt", + ] + if not os.path.exists(f) + ] + if missing: + raise RuntimeError( + f"Missing required client TLS file(s) for mutual TLS: {', '.join(missing)}" + ) + launch_container = bool(int(os.environ.get('PYPRIMEMESH_LAUNCH_CONTAINER', '0'))) if launch_container: container_name = utils.make_unique_container_name('ansys-prime-server') - utils.launch_prime_github_container(port=port, name=container_name, version=version) + utils.launch_prime_github_container( + port=port, name=container_name, version=version, connection_type=connection_type + ) config.set_using_container(True) - client = Client(port=port, timeout=timeout) + client = Client(port=port, timeout=timeout, client_certs_dir=client_certs_dir) client.container_name = container_name - print('using server from docker : The container name %s', container_name) - logging.getLogger('PyPrimeMesh').info('uses server from container : %s', container_name) + print('using server from docker : The container name ', container_name) return client + uds_file = None + if os.name != 'nt' and client_certs_dir is None: + uds_file = f'unix:/tmp/pyprimemesh-{uuid.uuid4()}.sock' + server = launch_server_process( - prime_root=prime_root, ip=ip, port=port, n_procs=n_procs, **kwargs + prime_root=prime_root, + ip=ip, + port=port, + n_procs=n_procs, + connection_type=connection_type, + uds_file=uds_file, + server_certs_dir=server_certs_dir, + **kwargs, ) - return Client(server_process=server, ip=ip, port=port, timeout=timeout) + return Client( + server_process=server, + ip=ip, + port=port, + timeout=timeout, + uds_file=uds_file, + connection_type=connection_type, + client_certs_dir=client_certs_dir, + channel=channel, + ) diff --git a/src/ansys/meshing/prime/internals/utils.py b/src/ansys/meshing/prime/internals/utils.py index 238e11ef19..ec1a9d7b8a 100644 --- a/src/ansys/meshing/prime/internals/utils.py +++ b/src/ansys/meshing/prime/internals/utils.py @@ -213,6 +213,7 @@ def launch_prime_github_container( port: int = defaults.port(), name: str = "ansys-prime-server", version: Optional[str] = None, + connection_type: Optional['config.ConnectionType'] = None, ): """Launch a container. @@ -229,6 +230,9 @@ def launch_prime_github_container( Name of the container. The default is ``"ansys-prime-server"``. version : str, optional Version of the container to retrieve. The default is ``None``. + connection_type : config.ConnectionType, optional + Type of connection to use. The default is ``None``, which defaults to + ``config.ConnectionType.GRPC_SECURE``. Raises ------ @@ -254,11 +258,29 @@ def launch_prime_github_container( f'{mount_host}:{mount_image}', '-e', f'ANSYSLMD_LICENSE_FILE={license_file}', + ] + graphics_port = int(os.environ.get('PRIME_GRAPHICS_PORT', '0')) + if graphics_port > 0: + print(f'PyPrimeMesh: using Prime graphics port {graphics_port}') + docker_command += ['-p', f'{graphics_port}:{graphics_port}'] + prime_arguments = [ f'{image_name}:{version}', '--port', f'{port}', ] - subprocess.run(docker_command, stdout=subprocess.DEVNULL) + + # Set default connection type if not provided + if connection_type is None: + connection_type = config.ConnectionType.GRPC_SECURE + + # Handle connection type + if ( + connection_type == config.ConnectionType.GRPC_INSECURE + or os.environ.get('PRIME_MODE', '').upper() == "GRPC_INSECURE" + ): + prime_arguments.append('--secure=no') + + subprocess.run(docker_command + prime_arguments, stdout=subprocess.DEVNULL) def stop_prime_github_container(name): diff --git a/src/ansys/meshing/prime/params/primestructs.py b/src/ansys/meshing/prime/params/primestructs.py index 004a5af739..941b925b61 100644 --- a/src/ansys/meshing/prime/params/primestructs.py +++ b/src/ansys/meshing/prime/params/primestructs.py @@ -1,6 +1,7 @@ # Copyright (C) 2024 - 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 diff --git a/tests/conftest.py b/tests/conftest.py index 3353fe4f5d..96e52adafe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,10 +57,19 @@ def start_ansys_prime_server(self, prime_root=None, ip='127.0.0.1', port=50055, by default ``1``. """ if n_procs == 1: - self.client = prime.launch_prime(prime_root=prime_root, ip=ip, port=port) + self.client = prime.launch_prime( + prime_root=prime_root, + ip=ip, + port=port, + connection_type=prime.internals.config.ConnectionType.GRPC_INSECURE, + ) else: self.client = prime.launch_prime( - prime_root=prime_root, ip=ip, port=port, n_procs=n_procs + prime_root=prime_root, + ip=ip, + port=port, + n_procs=n_procs, + connection_type=prime.internals.config.ConnectionType.GRPC_INSECURE, ) def start_remote_client(self):