diff --git a/ansys/dpf/core/core.py b/ansys/dpf/core/core.py index a117f0ec9c2..9bce798f5eb 100644 --- a/ansys/dpf/core/core.py +++ b/ansys/dpf/core/core.py @@ -321,8 +321,7 @@ def _connect(self, timeout=5): if not state._matured: raise IOError( - f"Unable to connect to DPF instance at {self._server()._input_ip} " - f"{self._server()._input_port}" + f"Unable to connect to DPF instance at {self._server()._address}" ) return stub diff --git a/ansys/dpf/core/misc.py b/ansys/dpf/core/misc.py index db0cd575f7d..03ff2b34aee 100644 --- a/ansys/dpf/core/misc.py +++ b/ansys/dpf/core/misc.py @@ -165,3 +165,16 @@ def find_ansys(): versions[int(ver_str)] = path return versions[max(versions.keys())] + +def is_pypim_configured(): + """Check if the environment is configured for PyPIM, without using pypim. + + This method is equivalent to ansys.platform.instancemanagement.is_configured(). It's + reproduced here to avoid having hard dependencies. + + Returns + ------- + bool + ``True`` if the environment is setup to use the PIM API, ``False`` otherwise. + """ + return "ANSYS_PLATFORM_INSTANCEMANAGEMENT_CONFIG" in os.environ diff --git a/ansys/dpf/core/server.py b/ansys/dpf/core/server.py index 2a10e579d37..22082dd13c9 100644 --- a/ansys/dpf/core/server.py +++ b/ansys/dpf/core/server.py @@ -17,7 +17,7 @@ import copy from ansys import dpf -from ansys.dpf.core.misc import find_ansys, is_ubuntu +from ansys.dpf.core.misc import find_ansys, is_ubuntu, is_pypim_configured from ansys.dpf.core import errors from ansys.dpf.core._version import ( @@ -79,6 +79,9 @@ def _global_server(): ip = os.environ.get("DPF_IP", LOCALHOST) port = int(os.environ.get("DPF_PORT", DPF_DEFAULT_PORT)) connect_to_server(ip, port) + elif is_pypim_configured(): + # DpfServer constructor will start DPF through PyPIM + DpfServer(as_global=True, launch_server=True) else: start_local_server() @@ -351,15 +354,23 @@ def __init__( check_valid_ip(ip) if not isinstance(port, int): raise ValueError("Port must be an integer") + address = "%s:%d" % (ip, port) if os.name == "posix" and "ubuntu" in platform.platform().lower(): raise OSError("DPF does not support Ubuntu") elif launch_server: - self._server_id = launch_dpf(str(ansys_path), ip, port, - docker_name=docker_name, - timeout=timeout) + if is_pypim_configured() and not ansys_path and not docker_name: + self._remote_instance = launch_remote_dpf() + address = self._remote_instance.services["grpc"].uri + # Unset ip and port as it's created by address. + ip = None + port = None + else: + self._server_id = launch_dpf(str(ansys_path), ip, port, + docker_name=docker_name, + timeout=timeout) - self.channel = grpc.insecure_channel("%s:%d" % (ip, port)) + self.channel = grpc.insecure_channel(address) # assign to global channel when requested if as_global: @@ -367,7 +378,8 @@ def __init__( # TODO: add to PIDs ... - # store port and ip for later reference + # store the address for later reference + self._address = address self._input_ip = ip self._input_port = port self.live = True @@ -465,6 +477,8 @@ def shutdown(self): for line in io.TextIOWrapper(process.stdout, encoding="utf-8"): pass process = subprocess.Popen(run_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif hasattr(self, "_remote_instance") and self._remote_instance: + self._remote_instance.delete() else: p = psutil.Process(self._base_service.server_info["server_process_id"]) p.kill() @@ -667,6 +681,21 @@ def read_stderr(): if len(docker_id) > 0: return docker_id[0] +def launch_remote_dpf(version = None): + try: + import ansys.platform.instancemanagement as pypim + except ImportError as e: + raise ImportError("Launching a remote session of DPF requires the installation" + + " of ansys-platform-instancemanagement") from e + version = version or __ansys_version__ + pim = pypim.connect() + instance = pim.create_instance(product_name = "dpf", product_version = version) + instance.wait_for_ready() + grpc_service = instance.services["grpc"] + if grpc_service.headers: + LOG.error("Communicating with DPF in this remote environment requires metadata." + + "This is not supported, you will likely encounter errors or limitations.") + return instance def check_ansys_grpc_dpf_version(server, timeout=10.): state = grpc.channel_ready_future(server.channel) @@ -677,8 +706,8 @@ def check_ansys_grpc_dpf_version(server, timeout=10.): if not state._matured: raise TimeoutError( - f"Failed to connect to {server._input_ip}:" + - f"{server._input_port} in {int(timeout)} seconds" + f"Failed to connect to {server._address}" + + f" in {int(timeout)} seconds" ) LOG.debug("Established connection to DPF gRPC") diff --git a/requirements_test.txt b/requirements_test.txt index 33ed6c8703d..f0c0684bb56 100755 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,4 +4,5 @@ pytest-rerunfailures matplotlib==3.2 vtk<9.1.0 pyvista>=0.24.0 +ansys-platform-instancemanagement~=1.0 coverage diff --git a/tests/test_launcher_remote.py b/tests/test_launcher_remote.py new file mode 100644 index 00000000000..891adff7df5 --- /dev/null +++ b/tests/test_launcher_remote.py @@ -0,0 +1,72 @@ +import sys +from unittest.mock import create_autospec + +import ansys.platform.instancemanagement as pypim +import grpc +import pytest +from ansys.dpf import core +from ansys.dpf.core import server +from ansys.dpf.core.server import DpfServer, __ansys_version__ + + +def test_start_remote(monkeypatch): + # Test for the Product Instance Management API integration + + # Start a local DPF server and create a mock PyPIM pretending it is starting it + local_server = core.start_local_server(as_global=False) + server_address = local_server._address + mock_instance = pypim.Instance( + definition_name="definitions/fake-dpf", + name="instances/fake-dpf", + ready=True, + status_message=None, + services={"grpc": pypim.Service(uri=server_address, headers={})}, + ) + # Mock the wait_for_ready method so that it immediately returns + mock_instance.wait_for_ready = create_autospec(mock_instance.wait_for_ready) + # Mock the deletion method + mock_instance.delete = create_autospec(mock_instance.delete) + + # Mock the PyPIM client, so that on the "create_instance" call it returns the mock instance + # Note: the host and port here will not be used. + mock_client = pypim.Client(channel=grpc.insecure_channel("localhost:12345")) + mock_client.create_instance = create_autospec( + mock_client.create_instance, return_value=mock_instance + ) + + # Mock the general pypim connection and configuration check method to expose the mock client. + mock_connect = create_autospec(pypim.connect, return_value=mock_client) + monkeypatch.setattr(pypim, "connect", mock_connect) + monkeypatch.setenv("ANSYS_PLATFORM_INSTANCEMANAGEMENT_CONFIG", "/fake/config.json") + + # Call the generic startup sequence with no indication on how to launch it + server = DpfServer(as_global=False) + + # It detected the environment and connected to pypim + assert mock_connect.called + + # It created a remote instance through PyPIM + mock_client.create_instance.assert_called_with( + product_name="dpf", product_version=__ansys_version__ + ) + + # It waited for this instance to be ready + assert mock_instance.wait_for_ready.called + + # It connected using the address provided by PyPIM + assert server._address == server_address + + # Stop the server + server.shutdown() + + # The delete instance is called + assert mock_instance.delete.called + + +def test_start_remote_failed(monkeypatch): + # Verifies that the error includes the package name to install when using + # launch_remote_dpf() without the requirements installed. + monkeypatch.setitem(sys.modules, "ansys.platform.instancemanagement", None) + with pytest.raises(ImportError) as exc: + server.launch_remote_dpf() + assert "ansys-platform-instancemanagement" in str(exc)