From 34e4f40dc152af65a0408219aa6e0c331f7921cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Thu, 12 May 2022 14:02:04 +0200 Subject: [PATCH 1/6] PyPIM integration --- ansys/dpf/core/core.py | 3 +- ansys/dpf/core/misc.py | 13 +++++++++ ansys/dpf/core/server.py | 46 ++++++++++++++++++++++++------- requirements_test.txt | 1 + tests/test_launcher.py | 59 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 12 deletions(-) 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..527be028f16 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..cae800402e9 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,22 @@ 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 + else: + ip = ip or LOCALHOST + port = port or DPF_DEFAULT_PORT + 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,9 +377,8 @@ def __init__( # TODO: add to PIDs ... - # store port and ip for later reference - self._input_ip = ip - self._input_port = port + # store the address for later reference + self._address = address self.live = True self.ansys_path = str(ansys_path) self._own_process = launch_server @@ -465,6 +474,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 +678,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 +703,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 d3da4307ede..3a73d6f314c 100755 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,3 +4,4 @@ pytest-rerunfailures matplotlib==3.2 vtk<9.1.0 pyvista>=0.24.0 +ansys-platform-instancemanagement~=1.0 diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 0f7684a30ad..7e9ba2ec9e4 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -1,7 +1,13 @@ +from unittest.mock import create_autospec +import grpc import pytest +import sys +import ansys.platform.instancemanagement as pypim from ansys.dpf import core +from ansys.dpf.core import server from ansys.dpf.core.misc import is_ubuntu +from ansys.dpf.core.server import DpfServer, __ansys_version__ ansys_path = core.misc.find_ansys() @@ -29,11 +35,64 @@ def test_start_local(): # ensure global channel didn't change assert starting_server == id(core.SERVER) +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, ansys_path=core.SERVER.ansys_path) + 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 arguments + server = DpfServer() + + # 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 def test_start_local_failed(): with pytest.raises(NotADirectoryError): core.start_local_server(ansys_path="", use_docker_by_default=False) +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) def test_server_ip(): assert core.SERVER.ip != None From d9e6330a2201331d8fc4b5787c3e3507e96d83bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Fri, 13 May 2022 10:16:38 +0200 Subject: [PATCH 2/6] flake8 --- ansys/dpf/core/misc.py | 4 ++-- tests/test_launcher.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ansys/dpf/core/misc.py b/ansys/dpf/core/misc.py index 527be028f16..03ff2b34aee 100644 --- a/ansys/dpf/core/misc.py +++ b/ansys/dpf/core/misc.py @@ -168,10 +168,10 @@ def find_ansys(): 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 diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 7e9ba2ec9e4..752f6e1cbf5 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -37,7 +37,7 @@ def test_start_local(): 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, ansys_path=core.SERVER.ansys_path) server_address = local_server._address @@ -52,7 +52,7 @@ def test_start_remote(monkeypatch): 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")) @@ -64,7 +64,7 @@ def test_start_remote(monkeypatch): 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 arguments server = DpfServer() From 88ba411527ad9a6e2f51c949735f8ecb267bbb88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Fri, 13 May 2022 15:53:19 +0200 Subject: [PATCH 3/6] restore input_ip and input_port to limit the impact --- ansys/dpf/core/server.py | 7 +++++-- tests/test_launcher.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ansys/dpf/core/server.py b/ansys/dpf/core/server.py index cae800402e9..7a5df65546d 100644 --- a/ansys/dpf/core/server.py +++ b/ansys/dpf/core/server.py @@ -362,9 +362,10 @@ def __init__( 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 that may not be the same + ip=None + port=None else: - ip = ip or LOCALHOST - port = port or DPF_DEFAULT_PORT self._server_id = launch_dpf(str(ansys_path), ip, port, docker_name=docker_name, timeout=timeout) @@ -379,6 +380,8 @@ def __init__( # store the address for later reference self._address = address + self._ip = ip + self._port = port self.live = True self.ansys_path = str(ansys_path) self._own_process = launch_server diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 752f6e1cbf5..36b2defc002 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -65,8 +65,8 @@ def test_start_remote(monkeypatch): monkeypatch.setattr(pypim, "connect", mock_connect) monkeypatch.setenv("ANSYS_PLATFORM_INSTANCEMANAGEMENT_CONFIG", "/fake/config.json") - # Call the generic startup sequence with no arguments - server = DpfServer() + # 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 @@ -82,6 +82,12 @@ def test_start_remote(monkeypatch): # 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_local_failed(): with pytest.raises(NotADirectoryError): core.start_local_server(ansys_path="", use_docker_by_default=False) From 1ef4cab68106ff8d9d0122f0dd98ddc8687e2f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Fri, 13 May 2022 15:56:41 +0200 Subject: [PATCH 4/6] flake8 --- ansys/dpf/core/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansys/dpf/core/server.py b/ansys/dpf/core/server.py index 7a5df65546d..934abf64ecb 100644 --- a/ansys/dpf/core/server.py +++ b/ansys/dpf/core/server.py @@ -362,9 +362,9 @@ def __init__( 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 that may not be the same - ip=None - port=None + # 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, From c478407be855902a1eac8bd90a747d20fbf38742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Fri, 13 May 2022 16:07:59 +0200 Subject: [PATCH 5/6] restore the variables names as before --- ansys/dpf/core/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansys/dpf/core/server.py b/ansys/dpf/core/server.py index 934abf64ecb..22082dd13c9 100644 --- a/ansys/dpf/core/server.py +++ b/ansys/dpf/core/server.py @@ -380,8 +380,8 @@ def __init__( # store the address for later reference self._address = address - self._ip = ip - self._port = port + self._input_ip = ip + self._input_port = port self.live = True self.ansys_path = str(ansys_path) self._own_process = launch_server From 6433bcf2b95f22abeec480c4ddb18c3aa588ff30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20Lul=C3=A9?= Date: Wed, 18 May 2022 14:49:45 +0200 Subject: [PATCH 6/6] Move the PIM tests in their own module --- tests/test_launcher.py | 65 ------------------------------- tests/test_launcher_remote.py | 72 +++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 65 deletions(-) create mode 100644 tests/test_launcher_remote.py diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 36b2defc002..0f7684a30ad 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -1,13 +1,7 @@ -from unittest.mock import create_autospec -import grpc import pytest -import sys -import ansys.platform.instancemanagement as pypim from ansys.dpf import core -from ansys.dpf.core import server from ansys.dpf.core.misc import is_ubuntu -from ansys.dpf.core.server import DpfServer, __ansys_version__ ansys_path = core.misc.find_ansys() @@ -35,70 +29,11 @@ def test_start_local(): # ensure global channel didn't change assert starting_server == id(core.SERVER) -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, ansys_path=core.SERVER.ansys_path) - 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_local_failed(): with pytest.raises(NotADirectoryError): core.start_local_server(ansys_path="", use_docker_by_default=False) -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) def test_server_ip(): assert core.SERVER.ip != None 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)