Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions ansys/dpf/core/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions ansys/dpf/core/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 37 additions & 8 deletions ansys/dpf/core/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -351,23 +354,32 @@ 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:
dpf.core.SERVER = self

# 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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
1 change: 1 addition & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ pytest-rerunfailures
matplotlib==3.2
vtk<9.1.0
pyvista>=0.24.0
ansys-platform-instancemanagement~=1.0
coverage
72 changes: 72 additions & 0 deletions tests/test_launcher_remote.py
Original file line number Diff line number Diff line change
@@ -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)