Skip to content

Commit 1fe6cdb

Browse files
fix: container launch issues (#4163)
There were issues running Fluent containers through PyFluent introduced at some point since version 0.30.5 (after which the `simba-plugin-fluent` tests started failing). The mount_source/mount_target/working_dir logic was not working properly, see first commit below for test that was showing the failure: 270495d Notes: - Main change: revised mount_source/mount_target logic, see code comments in `fluent_container.py` for more details - Took the opportunity to do some refactoring: - New `dict_to_str` utility function to provide "pretty printed" string for dicts, also aware of hide log secrets env var for CI runs - Revised docstring for mount_source/mount_target - New test, and test fixes related to this. In summary: we should not assume that the full paths that Fluent sees inside the container match exactly the full paths that PyFluent sees on the host system --------- Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent ec44600 commit 1fe6cdb

File tree

7 files changed

+183
-97
lines changed

7 files changed

+183
-97
lines changed

doc/changelog.d/4163.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
container launch issues

src/ansys/fluent/core/launcher/container_launcher.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from ansys.fluent.core.fluent_connection import FluentConnection
4545
from ansys.fluent.core.launcher.fluent_container import (
4646
configure_container_dict,
47+
dict_to_str,
4748
start_fluent_container,
4849
)
4950
from ansys.fluent.core.launcher.launch_options import (
@@ -199,23 +200,20 @@ def __init__(
199200
self._args.append(" -meshing")
200201

201202
def __call__(self):
203+
202204
if self.argvals["dry_run"]:
203205
config_dict, *_ = configure_container_dict(
204206
self._args, **self.argvals["container_dict"]
205207
)
206-
from pprint import pprint
207-
208+
dict_str = dict_to_str(config_dict)
208209
print("\nDocker container run configuration:\n")
209210
print("config_dict = ")
210-
if os.getenv("PYFLUENT_HIDE_LOG_SECRETS") != "1":
211-
pprint(config_dict)
212-
else:
213-
config_dict_h = config_dict.copy()
214-
config_dict_h.pop("environment")
215-
pprint(config_dict_h)
216-
del config_dict_h
211+
print(dict_str)
217212
return config_dict
218213

214+
logger.debug(f"Fluent container launcher args: {self._args}")
215+
logger.debug(f"Fluent container launcher argvals:\n{dict_to_str(self.argvals)}")
216+
219217
if is_compose():
220218
port, config_dict, container = start_fluent_container(
221219
self._args, self.argvals["container_dict"]

src/ansys/fluent/core/launcher/fluent_container.py

Lines changed: 106 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import logging
7575
import os
7676
from pathlib import Path, PurePosixPath
77+
from pprint import pformat
7778
import tempfile
7879
from typing import Any, List
7980

@@ -118,6 +119,20 @@ def __init__(self):
118119
)
119120

120121

122+
def dict_to_str(dict: dict) -> str:
123+
"""Converts the dict to string while hiding the 'environment' argument from the dictionary,
124+
if the environment variable 'PYFLUENT_HIDE_LOG_SECRETS' is '1'.
125+
This is useful for logging purposes, to avoid printing sensitive information such as license server details.
126+
"""
127+
128+
if "environment" in dict and os.getenv("PYFLUENT_HIDE_LOG_SECRETS") == "1":
129+
modified_dict = dict.copy()
130+
modified_dict.pop("environment")
131+
return pformat(modified_dict)
132+
else:
133+
return pformat(dict)
134+
135+
121136
@all_deprecators(
122137
deprecate_arg_mappings=[
123138
{
@@ -158,9 +173,12 @@ def configure_container_dict(
158173
args : List[str]
159174
List of Fluent launch arguments.
160175
mount_source : str | Path, optional
161-
Existing path in the host operating system that will be mounted to ``mount_target``.
176+
Path on the host system to mount into the container. This directory will serve as the working directory
177+
for the Fluent process inside the container. If not specified, PyFluent's current working directory will
178+
be used.
162179
mount_target : str | Path, optional
163-
Path inside the container where ``mount_source`` will be mounted to.
180+
Path inside the container where ``mount_source`` will be mounted. This will be the working directory path
181+
visible to the Fluent process running inside the container.
164182
timeout : int, optional
165183
Time limit for the Fluent container to start, in seconds. By default, 30 seconds.
166184
port : int, optional
@@ -213,71 +231,85 @@ def configure_container_dict(
213231
See also :func:`start_fluent_container`.
214232
"""
215233

216-
if (
217-
container_dict
218-
and "environment" in container_dict
219-
and os.getenv("PYFLUENT_HIDE_LOG_SECRETS") == "1"
220-
):
221-
container_dict_h = container_dict.copy()
222-
container_dict_h.pop("environment")
223-
logger.debug(f"container_dict before processing: {container_dict_h}")
224-
del container_dict_h
225-
else:
226-
logger.debug(f"container_dict before processing: {container_dict}")
234+
logger.debug(f"container_dict before processing:\n{dict_to_str(container_dict)}")
235+
236+
# Starting with 'mount_source' because it is not tied to the 'working_dir'.
237+
# The intended 'mount_source' logic is as follows, if it is not directly specified:
238+
# 1. If 'file_transfer_service' is provided, use its 'mount_source'.
239+
# 2. Try to use the environment variable 'PYFLUENT_CONTAINER_MOUNT_SOURCE', if it is set.
240+
# 3. Use the value from 'pyfluent.CONTAINER_MOUNT_SOURCE', if it is set.
241+
# 4. If 'volumes' is specified in 'container_dict', try to infer the value from it.
242+
# 5. Finally, use the current working directory, which is always available.
227243

228244
if not mount_source:
229245
if file_transfer_service:
230246
mount_source = file_transfer_service.mount_source
231247
else:
232248
mount_source = os.getenv(
233249
"PYFLUENT_CONTAINER_MOUNT_SOURCE",
234-
pyfluent.CONTAINER_MOUNT_SOURCE or os.getcwd(),
250+
pyfluent.CONTAINER_MOUNT_SOURCE,
235251
)
236252

237-
elif "volumes" in container_dict:
238-
logger.warning(
239-
"'volumes' keyword specified in 'container_dict', but "
240-
"it is going to be overwritten by specified 'mount_source'."
241-
)
242-
container_dict.pop("volumes")
253+
if "volumes" in container_dict:
254+
if len(container_dict["volumes"]) != 1:
255+
logger.warning(
256+
"Multiple volumes being mounted in the Docker container, "
257+
"Assuming the first mount is the working directory for Fluent."
258+
)
259+
volumes_string = container_dict["volumes"][0]
260+
if mount_source:
261+
logger.warning(
262+
"'volumes' keyword specified in 'container_dict', but "
263+
"it is going to be overwritten by specified 'mount_source'."
264+
)
265+
else:
266+
mount_source = volumes_string.split(":")[0]
267+
logger.debug(f"mount_source: {mount_source}")
268+
inferred_mount_target = volumes_string.split(":")[1]
269+
logger.debug(f"inferred_mount_target: {inferred_mount_target}")
270+
271+
if not mount_source:
272+
logger.debug("No container 'mount_source' specified, using default value.")
273+
mount_source = os.getcwd()
243274

244-
if not os.path.exists(mount_source):
245-
os.makedirs(mount_source)
275+
# The intended 'mount_target' logic is as follows, if it is not directly specified:
276+
# 1. If 'working_dir' is specified in 'container_dict', use it as 'mount_target'.
277+
# 2. Use the environment variable 'PYFLUENT_CONTAINER_MOUNT_TARGET', if it is set.
278+
# 3. Try to infer the value from the 'volumes' keyword in 'container_dict', if available.
279+
# 4. Finally, use the value from 'pyfluent.CONTAINER_MOUNT_TARGET', which is always set.
246280

247281
if not mount_target:
248-
mount_target = os.getenv(
249-
"PYFLUENT_CONTAINER_MOUNT_TARGET", pyfluent.CONTAINER_MOUNT_TARGET
250-
)
251-
elif "volumes" in container_dict:
252-
logger.warning(
253-
"'volumes' keyword specified in 'container_dict', but "
254-
"it is going to be overwritten by specified 'mount_target'."
255-
)
256-
container_dict.pop("volumes")
282+
if "working_dir" in container_dict:
283+
mount_target = container_dict["working_dir"]
284+
else:
285+
mount_target = os.getenv("PYFLUENT_CONTAINER_MOUNT_TARGET")
286+
287+
if "working_dir" in container_dict and mount_target:
288+
# working_dir will be set later to the final value of mount_target
289+
container_dict.pop("working_dir")
290+
291+
if not mount_target and "volumes" in container_dict:
292+
mount_target = inferred_mount_target
293+
294+
if not mount_target:
295+
logger.debug("No container 'mount_target' specified, using default value.")
296+
mount_target = pyfluent.CONTAINER_MOUNT_TARGET
257297

258298
if "volumes" not in container_dict:
259299
container_dict.update(volumes=[f"{mount_source}:{mount_target}"])
260300
else:
261-
logger.debug(f"container_dict['volumes']: {container_dict['volumes']}")
262-
if len(container_dict["volumes"]) != 1:
263-
logger.warning(
264-
"Multiple volumes being mounted in the Docker container, "
265-
"using the first mount as the working directory for Fluent."
266-
)
267-
volumes_string = container_dict["volumes"][0]
268-
mount_target = ""
269-
for c in reversed(volumes_string):
270-
if c == ":":
271-
break
272-
else:
273-
mount_target += c
274-
mount_target = mount_target[::-1]
275-
mount_source = volumes_string.replace(":" + mount_target, "")
276-
logger.debug(f"mount_source: {mount_source}")
277-
logger.debug(f"mount_target: {mount_target}")
301+
container_dict["volumes"][0] = f"{mount_source}:{mount_target}"
302+
278303
logger.warning(
279-
f"Starting Fluent container mounted to {mount_source}, with this path available as {mount_target} for the Fluent session running inside the container."
304+
f"Configuring Fluent container to mount to {mount_source}, "
305+
f"with this path available as {mount_target} for the Fluent session running inside the container."
280306
)
307+
308+
if "working_dir" not in container_dict:
309+
container_dict.update(
310+
working_dir=mount_target,
311+
)
312+
281313
port_mapping = {port: port} if port else {}
282314
if not port_mapping and "ports" in container_dict:
283315
# take the specified 'port', OR the first port value from the specified 'ports', for Fluent to use
@@ -315,11 +347,7 @@ def configure_container_dict(
315347
labels={"test_name": test_name},
316348
)
317349

318-
if "working_dir" not in container_dict:
319-
container_dict.update(
320-
working_dir=mount_target,
321-
)
322-
350+
# Find the server info file name from the command line arguments
323351
if "command" in container_dict:
324352
for v in container_dict["command"]:
325353
if v.startswith("-sifile="):
@@ -343,6 +371,20 @@ def configure_container_dict(
343371
os.close(fd)
344372
container_server_info_file = PurePosixPath(mount_target) / Path(sifile).name
345373

374+
logger.debug(
375+
f"Using server info file '{container_server_info_file}' for Fluent container."
376+
)
377+
378+
# If the 'command' had already been specified in the 'container_dict',
379+
# maintain other 'command' arguments but update the '-sifile' argument,
380+
# as the 'mount_target' or 'working_dir' may have changed.
381+
if "command" in container_dict:
382+
for i, item in enumerate(container_dict["command"]):
383+
if item.startswith("-sifile="):
384+
container_dict["command"][i] = f"-sifile={container_server_info_file}"
385+
else:
386+
container_dict["command"] = args + [f"-sifile={container_server_info_file}"]
387+
346388
if not fluent_image:
347389
if not image_tag:
348390
image_tag = os.getenv(
@@ -384,16 +426,13 @@ def configure_container_dict(
384426
container_dict["environment"] = {}
385427
container_dict["environment"]["FLUENT_LAUNCHED_FROM_PYFLUENT"] = "1"
386428

387-
fluent_commands = [f"-sifile={container_server_info_file}"] + args
388-
389-
container_dict_default = {}
390-
container_dict_default.update(
391-
command=fluent_commands,
429+
container_dict_base = {}
430+
container_dict_base.update(
392431
detach=True,
393432
auto_remove=True,
394433
)
395434

396-
for k, v in container_dict_default.items():
435+
for k, v in container_dict_base.items():
397436
if k not in container_dict:
398437
container_dict[k] = v
399438

@@ -404,6 +443,13 @@ def configure_container_dict(
404443
container_dict["mount_source"] = mount_source
405444
container_dict["mount_target"] = mount_target
406445

446+
logger.debug(
447+
f"Fluent container timeout: {timeout}, container_grpc_port: {container_grpc_port}, "
448+
f"host_server_info_file: '{host_server_info_file}', "
449+
f"remove_server_info_file: {remove_server_info_file}"
450+
)
451+
logger.debug(f"container_dict after processing:\n{dict_to_str(container_dict)}")
452+
407453
return (
408454
container_dict,
409455
timeout,
@@ -458,21 +504,6 @@ def start_fluent_container(
458504
remove_server_info_file,
459505
) = container_vars
460506

461-
if os.getenv("PYFLUENT_HIDE_LOG_SECRETS") != "1":
462-
logger.debug(f"container_vars: {container_vars}")
463-
else:
464-
config_dict_h = config_dict.copy()
465-
config_dict_h.pop("environment")
466-
container_vars_tmp = (
467-
config_dict_h,
468-
timeout,
469-
port,
470-
host_server_info_file,
471-
remove_server_info_file,
472-
)
473-
logger.debug(f"container_vars: {container_vars_tmp}")
474-
del container_vars_tmp
475-
476507
try:
477508
if is_compose():
478509
config_dict["fluent_port"] = port

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ def run_before_each_test(
177177
monkeypatch.setenv("PYFLUENT_TEST_NAME", request.node.name)
178178
monkeypatch.setenv("PYFLUENT_CODEGEN_SKIP_BUILTIN_SETTINGS", "1")
179179
pyfluent.CONTAINER_MOUNT_SOURCE = pyfluent.EXAMPLES_PATH
180-
pyfluent.CONTAINER_MOUNT_TARGET = pyfluent.EXAMPLES_PATH
181180
original_cwd = os.getcwd()
182181
monkeypatch.chdir(tmp_path)
183182
yield

tests/test_builtin_settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,8 @@ def test_builtin_settings(mixing_elbow_case_data_session):
547547
else:
548548
with pytest.raises(RuntimeError):
549549
CustomVectors(settings_source=solver)
550-
tmp_save_path = tempfile.mkdtemp(dir=pyfluent.EXAMPLES_PATH)
551-
project_file = Path(tmp_save_path) / "mixing_elbow_param.flprj"
550+
tmp_save_path = Path(tempfile.mkdtemp(dir=pyfluent.EXAMPLES_PATH))
551+
project_file = Path(tmp_save_path.parts[-1]) / "mixing_elbow_param.flprj"
552552
solver.settings.parametric_studies.initialize(project_filename=str(project_file))
553553
assert ParametricStudies(settings_source=solver) == solver.parametric_studies
554554
assert (

0 commit comments

Comments
 (0)