From dc6f03ed9e5cc3988c5229fb1c14bdfbb1c2a4c7 Mon Sep 17 00:00:00 2001 From: Roberto Pastor Muela <37798125+RobPasMue@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:07:17 +0100 Subject: [PATCH] feat: create launcher for core services (#1587) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- .github/workflows/docker_cleanup.yml | 2 +- doc/changelog.d/1587.added.md | 1 + src/ansys/geometry/core/__init__.py | 1 + .../geometry/core/connection/__init__.py | 1 + .../core/connection/docker_instance.py | 18 +-- .../geometry/core/connection/launcher.py | 147 +++++++++++++++++- .../core/connection/product_instance.py | 111 +++++++++++-- 7 files changed, 255 insertions(+), 26 deletions(-) create mode 100644 doc/changelog.d/1587.added.md diff --git a/.github/workflows/docker_cleanup.yml b/.github/workflows/docker_cleanup.yml index 06cd0a4f9e..d322a8860b 100644 --- a/.github/workflows/docker_cleanup.yml +++ b/.github/workflows/docker_cleanup.yml @@ -26,4 +26,4 @@ jobs: with: package-name: 'geometry' token: ${{ secrets.GITHUB_TOKEN }} - tags-kept: 'windows-latest, windows-latest-unstable, core-windows-latest, core-windows-latest-unstable, core-linux-latest, core-linux-latest-unstable, 24.1, 24.2, 25.1, windows-24.1, windows-24.2, windows-25.1, windows-25.2, core-windows-25.2, linux-24.1, linux-24.2, linux-25.1, core-linux-25.2' + tags-kept: 'windows-latest, windows-latest-unstable, core-windows-latest, core-windows-latest-unstable, core-linux-latest, core-linux-latest-unstable, 24.1, 24.2, 25.1, windows-24.1, windows-24.2, windows-25.1, windows-25.2, core-windows-25.2, core-linux-25.2' diff --git a/doc/changelog.d/1587.added.md b/doc/changelog.d/1587.added.md new file mode 100644 index 0000000000..dbb5c7b5fa --- /dev/null +++ b/doc/changelog.d/1587.added.md @@ -0,0 +1 @@ +create launcher for core services \ No newline at end of file diff --git a/src/ansys/geometry/core/__init__.py b/src/ansys/geometry/core/__init__.py index 7917438ccf..aee42ea54d 100644 --- a/src/ansys/geometry/core/__init__.py +++ b/src/ansys/geometry/core/__init__.py @@ -37,6 +37,7 @@ from ansys.geometry.core.connection.launcher import ( launch_docker_modeler, launch_modeler, + launch_modeler_with_core_service, launch_modeler_with_discovery, launch_modeler_with_discovery_and_pimlight, launch_modeler_with_geometry_service, diff --git a/src/ansys/geometry/core/connection/__init__.py b/src/ansys/geometry/core/connection/__init__.py index b8ab3ea019..9f9d1c41ea 100644 --- a/src/ansys/geometry/core/connection/__init__.py +++ b/src/ansys/geometry/core/connection/__init__.py @@ -50,6 +50,7 @@ from ansys.geometry.core.connection.launcher import ( launch_docker_modeler, launch_modeler, + launch_modeler_with_core_service, launch_modeler_with_discovery, launch_modeler_with_discovery_and_pimlight, launch_modeler_with_geometry_service, diff --git a/src/ansys/geometry/core/connection/docker_instance.py b/src/ansys/geometry/core/connection/docker_instance.py index da8d61373f..9205e3824c 100644 --- a/src/ansys/geometry/core/connection/docker_instance.py +++ b/src/ansys/geometry/core/connection/docker_instance.py @@ -68,18 +68,14 @@ class GeometryContainers(Enum): CORE_LINUX_LATEST_UNSTABLE = 3, "linux", "core-linux-latest-unstable" # TO BE REMOVED at some point... - START WINDOWS_LATEST = 4, "windows", "windows-latest" - LINUX_LATEST = 5, "linux", "linux-latest" - WINDOWS_LATEST_UNSTABLE = 6, "windows", "windows-latest-unstable" - LINUX_LATEST_UNSTABLE = 7, "linux", "linux-latest-unstable" + WINDOWS_LATEST_UNSTABLE = 5, "windows", "windows-latest-unstable" # TO BE REMOVED at some point... - END - WINDOWS_24_1 = 8, "windows", "windows-24.1" - LINUX_24_1 = 9, "linux", "linux-24.1" - WINDOWS_24_2 = 10, "windows", "windows-24.2" - LINUX_24_2 = 11, "linux", "linux-24.2" - WINDOWS_25_1 = 12, "windows", "windows-25.1" - LINUX_25_1 = 13, "linux", "linux-25.1" - WINDOWS_25_2 = 14, "windows", "core-windows-25.2" - LINUX_25_2 = 15, "linux", "core-linux-25.2" + WINDOWS_24_1 = 6, "windows", "windows-24.1" + WINDOWS_24_2 = 7, "windows", "windows-24.2" + WINDOWS_25_1 = 8, "windows", "windows-25.1" + WINDOWS_25_2 = 9, "windows", "windows-25.2" + CORE_WINDOWS_25_2 = 10, "windows", "core-windows-25.2" + CORE_LINUX_25_2 = 11, "linux", "core-linux-25.2" class LocalDockerInstance: diff --git a/src/ansys/geometry/core/connection/launcher.py b/src/ansys/geometry/core/connection/launcher.py index 1a08e80afa..772dcf56fb 100644 --- a/src/ansys/geometry/core/connection/launcher.py +++ b/src/ansys/geometry/core/connection/launcher.py @@ -75,6 +75,7 @@ def launch_modeler(mode: str = None, **kwargs: dict | None) -> "Modeler": * For ``"docker"`` mode, see the :func:`launch_docker_modeler` method. * For ``"geometry_service"`` mode, see the :func:`launch_modeler_with_geometry_service` method. + * For ``"core_service"`` mode, see the :func:`launch_modeler_with_core_service` method. * For ``"spaceclaim"`` mode, see the :func:`launch_modeler_with_spaceclaim` method. * For ``"discovery"`` mode, see the :func:`launch_modeler_with_discovery` method. @@ -108,6 +109,8 @@ def _launch_with_launchmode(mode: str, **kwargs: dict | None) -> "Modeler": * ``"docker"``: Launches the ``Modeler`` service locally using Docker. * ``"geometry_service"``: Launches the ``Modeler`` service locally using the Ansys Geometry Service. + * ``"core_service"``: Launches the ``Modeler`` service locally using the + Ansys Geometry Core Service. * ``"spaceclaim"``: Launches the ``Modeler`` service locally using Ansys SpaceClaim. * ``"discovery"``: Launches the ``Modeler`` service locally using Ansys Discovery. @@ -132,6 +135,8 @@ def _launch_with_launchmode(mode: str, **kwargs: dict | None) -> "Modeler": return launch_remote_modeler(**kwargs) elif mode == "docker": return launch_docker_modeler(**kwargs) + elif mode == "core_service": + return launch_modeler_with_core_service(**kwargs) elif mode == "geometry_service": return launch_modeler_with_geometry_service(**kwargs) elif mode == "spaceclaim": @@ -167,8 +172,9 @@ def _launch_with_automatic_detection(**kwargs: dict | None) -> "Modeler": # # 1. Check if PyPIM is configured and if the environment is configured for it. # 2. Check if Docker is installed and if the environment is configured for it. - # 3. If you are on a Windows machine: - # - check if the Ansys Geometry service is installed. + # 3. Check if the Ansys Geometry Core service is installed. (OS agnostic) + # 4. If you are on a Windows machine: + # - check if the Ansys Geometry DMS service is installed. # - check if Ansys SpaceClaim is installed. # - check if Ansys Discovery is installed. @@ -189,15 +195,23 @@ def _launch_with_automatic_detection(**kwargs: dict | None) -> "Modeler": " Trying to start the Geometry service locally." ) + try: + LOG.info("Starting Geometry Core service locally.") + return launch_modeler_with_core_service(**kwargs) + except Exception: + wrn_msg = "The Geometry Core service could not be started locally." + wrn_msg += " Trying to start the Geometry DMS service locally." if os.name == "nt" else "" + LOG.warning(wrn_msg) + # If we are on a Windows machine, we can try to start the Geometry service locally, # through various methods: Geometry service, SpaceClaim, Discovery. if os.name == "nt": try: - LOG.info("Starting Geometry service locally.") + LOG.info("Starting Geometry DMS service locally.") return launch_modeler_with_geometry_service(**kwargs) except Exception: LOG.warning( - "The Geometry service could not be started locally." + "The Geometry DMS service could not be started locally." " Trying to start Ansys SpaceClaim locally." ) @@ -962,3 +976,128 @@ def _launch_pim_instance( logging_level=client_log_level, logging_file=client_log_file, ) + + +def launch_modeler_with_core_service( + product_version: int = None, + host: str = "localhost", + port: int = None, + enable_trace: bool = False, + timeout: int = 60, + server_log_level: int = 2, + client_log_level: int = logging.INFO, + server_logs_folder: str = None, + client_log_file: str = None, + **kwargs: dict | None, +) -> "Modeler": + """Start the Geometry Core service locally using the ``ProductInstance`` class. + + When calling this method, a standalone Geometry Core service is started. + By default, if an endpoint is specified (by defining `host` and `port` parameters) + but the endpoint is not available, the startup will fail. Otherwise, it will try to + launch its own service. + + Parameters + ---------- + product_version: int, optional + The product version to be started. Goes from v25.2 to + the latest. Default is ``None``. + If a specific product version is requested but not installed locally, + a SystemError will be raised. + + **Ansys products versions and their corresponding int values:** + + * ``252`` : Ansys 25R2 + * ``261`` : Ansys 26R1 + host: str, optional + IP address at which the service will be deployed. By default, + its value will be ``localhost``. + port : int, optional + Port at which the service will be deployed. By default, its + value will be ``None``. + enable_trace : bool, optional + Boolean enabling the logs trace on the service console window. + By default its value is ``False``. + timeout : int, optional + Timeout for starting the backend startup process. The default is 60. + server_log_level : int, optional + Backend's log level from 0 to 3: + 0: Chatterbox + 1: Debug + 2: Warning + 3: Error + + The default is ``2`` (Warning). + client_log_level : int, optional + Logging level to apply to the client. By default, INFO level is used. + Use the logging module's levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. + server_logs_folder : str, optional + Sets the backend's logs folder path. If nothing is defined, + the backend will use its default path. + client_log_file : str, optional + Sets the client's log file path. If nothing is defined, + the client will log to the console. + **kwargs : dict, default: None + Placeholder to prevent errors when passing additional arguments that + are not compatible with this method. + + Returns + ------- + Modeler + Instance of the Geometry Core service. + + Raises + ------ + ConnectionError + If the specified endpoint is already in use, a connection + error will be raised. + SystemError + If there is not an Ansys product 25.2 version or later installed + a SystemError will be raised. + + Examples + -------- + Starting a geometry core service with the default parameters and getting back a ``Modeler`` + object: + + >>> from ansys.geometry.core import launch_modeler_with_core_service + >>> modeler = launch_modeler_with_core_service() + + Starting a geometry service, on address ``10.171.22.44``, port ``5001``, with chatty + logs, traces enabled and a ``300`` seconds timeout: + + >>> from ansys.geometry.core import launch_modeler_with_core_service + >>> modeler = launch_modeler_with_core_service(host="10.171.22.44", + port=5001, + enable_trace= True, + timeout=300, + server_log_level=0) + """ + # if api_version is passed, throw a warning saying that it is not used + if "api_version" in kwargs: + LOG.warning( + "The 'api_version' parameter is not used in 'launch_modeler_with_core_service'. " + "Please remove it from the arguments." + ) + + # If we are in a Windows environment, we are going to write down the server + # logs in the %PUBLIC%/Documents/Ansys/GeometryService folder. + if os.name == "nt" and server_logs_folder is None: + # Writing to the "Public" folder by default - no write permissions specifically required. + server_logs_folder = Path(os.getenv("PUBLIC"), "Documents", "Ansys", "GeometryService") + LOG.info(f"Writing server logs to the default folder at {server_logs_folder}.") + + return prepare_and_start_backend( + BackendType.LINUX_SERVICE, + product_version=product_version, + host=host, + port=port, + enable_trace=enable_trace, + api_version=ApiVersions.LATEST, + timeout=timeout, + server_log_level=server_log_level, + client_log_level=client_log_level, + server_logs_folder=server_logs_folder, + client_log_file=client_log_file, + specific_minimum_version=252, + ) diff --git a/src/ansys/geometry/core/connection/product_instance.py b/src/ansys/geometry/core/connection/product_instance.py index 2f18545ff8..3e5ac35410 100644 --- a/src/ansys/geometry/core/connection/product_instance.py +++ b/src/ansys/geometry/core/connection/product_instance.py @@ -41,7 +41,10 @@ WINDOWS_GEOMETRY_SERVICE_FOLDER = "GeometryService" -"""Default Geometry Service's folder name into the unified installer.""" +"""Default Geometry Service's folder name into the unified installer (DMS).""" + +CORE_GEOMETRY_SERVICE_FOLDER = "CoreGeometryService" +"""Default Geometry Service's folder name into the unified installer (Core Service).""" DISCOVERY_FOLDER = "Discovery" """Default Discovery's folder name into the unified installer.""" @@ -62,7 +65,10 @@ """ GEOMETRY_SERVICE_EXE = "Presentation.ApiServerDMS.exe" -"""The Windows Geometry Service's filename.""" +"""The Windows Geometry Service's filename (DMS).""" + +CORE_GEOMETRY_SERVICE_EXE = "Presentation.ApiServerLinux.exe" +"""The Windows Geometry Service's filename (Core Service).""" DISCOVERY_EXE = "Discovery.exe" """The Ansys Discovery's filename.""" @@ -179,6 +185,7 @@ def prepare_and_start_backend( client_log_level: int = logging.INFO, server_logs_folder: str = None, client_log_file: str = None, + specific_minimum_version: int = None, log_level: int = None, # DEPRECATED logs_folder: str = None, # DEPRECATED ) -> "Modeler": @@ -232,6 +239,9 @@ def prepare_and_start_backend( client_log_file : str, optional Sets the client's log file path. If nothing is defined, the client will log to the console. + specific_minimum_version : int, optional + Sets a specific minimum version to be checked. If this is not defined, + the minimum version will be set to 24.1.0. log_level : int, optional DEPRECATED. Use ``server_log_level`` instead. logs_folder : str, optional @@ -253,8 +263,11 @@ def prepare_and_start_backend( """ from ansys.geometry.core.modeler import Modeler - if os.name != "nt": # pragma: no cover - raise RuntimeError("Method 'prepare_and_start_backend' is only available on Windows.") + if os.name != "nt" and backend_type != BackendType.LINUX_SERVICE: # pragma: no cover + raise RuntimeError( + "Method 'prepare_and_start_backend' is only available on Windows." + "A Linux version is only available for the Core Geometry Service." + ) # Deprecation behavior... To be removed in release 0.7 if log_level is not None: # pragma: no cover @@ -285,7 +298,7 @@ def prepare_and_start_backend( product_version = get_latest_ansys_installation()[0] # Verify that the minimum version is installed. - _check_minimal_versions(product_version) + _check_minimal_versions(product_version, specific_minimum_version) if server_logs_folder is not None: # Verify that the user has write permissions to the folder and that it exists. @@ -348,6 +361,78 @@ def prepare_and_start_backend( GEOMETRY_SERVICE_EXE, ) ) + # This should be modified to Windows Core Service in the future + elif backend_type == BackendType.LINUX_SERVICE: + # Define several Ansys Geometry Core Service folders needed + root_service_folder = Path(installations[product_version], CORE_GEOMETRY_SERVICE_FOLDER) + native_folder = root_service_folder / "Native" + cad_integration_folder = root_service_folder / "CADIntegration" + schema_folder = root_service_folder / "Schema" + + # Set the environment variables for the Ansys Geometry Core Service launch + # ANS_DSCO_REMOTE_IP should be variable "host" directly, but not working... + env_copy["ANS_DSCO_REMOTE_IP"] = "127.0.0.1" if host == "localhost" else host + env_copy["ANS_DSCO_REMOTE_PORT"] = str(port) + env_copy["ANS_DSCO_REMOTE_LOGS_CONFIG"] = "linux" + env_copy["P_SCHEMA"] = schema_folder.as_posix() + env_copy["ANSYS_CI_INSTALL"] = cad_integration_folder.as_posix() + env_copy[f"ANSYSCL{product_version}_DIR"] = ( + root_service_folder / "licensingclient" + ).as_posix() + + if os.name == "nt": + # Modify the PATH variable to include the path to the Ansys Geometry Core Service + env_copy["PATH"] = ( + f"{env_copy['PATH']}" + + f";{root_service_folder.as_posix()}" + + f";{native_folder.as_posix()}" + + f";{cad_integration_folder.as_posix()}" + ) + + # For Windows, we need to use the exe file to launch the Core Geometry Service + args.append( + Path( + installations[product_version], + CORE_GEOMETRY_SERVICE_FOLDER, + CORE_GEOMETRY_SERVICE_EXE, + ) + ) + else: + # Verify dotnet is installed + import shutil + + if not shutil.which("dotnet"): + raise RuntimeError( + "Cannot find 'dotnet' command. " + "Please install 'dotnet' to use the Ansys Geometry Core Service. " + "At least dotnet 8.0 is required." + ) + + # At least dotnet 8.0 is required + # private method and controlled input by library - excluding bandit check. + if subprocess.check_output(["dotnet", "--version"]).decode("utf-8").split(".")[0] < "8": # nosec B607, B603 + raise RuntimeError( + "Ansys Geometry Core Service requires at least dotnet 8.0. " + "Please install a compatible version." + ) + + # Modify the LD_LIBRARY_PATH variable to include the Ansys Geometry Core Service + env_copy["LD_LIBRARY_PATH"] = ( + env_copy.get("LD_LIBRARY_PATH", "") + + f":{root_service_folder.as_posix()}" + + f":{native_folder.as_posix()}" + + f":{cad_integration_folder.as_posix()}" + ) + + # For Linux, we need to use the dotnet command to launch the Core Geometry Service + args.append("dotnet") + args.append( + Path( + installations[product_version], + CORE_GEOMETRY_SERVICE_FOLDER, + CORE_GEOMETRY_SERVICE_EXE.replace(".exe", ".dll"), + ) + ) else: raise RuntimeError( f"Cannot connect to backend {backend_type.name} using ``prepare_and_start_backend()``" @@ -491,15 +576,21 @@ def __start_program(args: list[str], local_env: dict[str, str]) -> subprocess.Po ) -def _check_minimal_versions(latest_installed_version: int) -> None: +def _check_minimal_versions( + latest_installed_version: int, specific_minimum_version: int | None +) -> None: """Check client is compatible with Ansys Products. - Check that at least V241 is installed. + Check that at least V241 is installed. Or, if a specific version is requested, + check that it is installed. """ - if abs(latest_installed_version) < 241: + min_ver = specific_minimum_version or 241 + if abs(latest_installed_version) < min_ver: + # Split the version into its components. + major, minor = divmod(min_ver, 10) msg = ( - "PyAnsys Geometry is compatible with Ansys Products from version 24.1.0. " - + "Please install Ansys products 24.1.0 or later." + f"PyAnsys Geometry is compatible with Ansys Products from version {major}.{minor}.0. " + + f"Please install Ansys products {major}.{minor}.0 or later." ) raise SystemError(msg)