From a359bc25b95f77bf142b77d68ae3b4aa5c0eee42 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Thu, 26 Mar 2026 18:33:09 +0100 Subject: [PATCH 1/7] Add an option to disable selinux to make qlever commands work on fedora --- src/qlever/commands/add_text_index.py | 10 +++++-- src/qlever/commands/index.py | 8 +++++- src/qlever/commands/start.py | 4 ++- src/qlever/commands/system_info.py | 9 +++++- src/qlever/containerize.py | 28 +++++++++++++++++++ src/qlever/qleverfile.py | 10 +++++++ src/qlever/util.py | 2 ++ .../commands/test_index_other_methods.py | 7 ++++- test/qlever/commands/test_start_execute.py | 1 + .../commands/test_start_other_methods.py | 7 ++++- 10 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/qlever/commands/add_text_index.py b/src/qlever/commands/add_text_index.py index df250287..bcac8712 100644 --- a/src/qlever/commands/add_text_index.py +++ b/src/qlever/commands/add_text_index.py @@ -31,7 +31,12 @@ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: "text_words_file", "text_docs_file", ], - "runtime": ["system", "image", "index_container"], + "runtime": [ + "system", + "image", + "index_container", + "disable_selinux", + ], } def additional_arguments(self, subparser) -> None: @@ -54,7 +59,7 @@ def execute(self, args) -> bool: "from_text_records_and_literals", ]: add_text_index_cmd += ( - f" -w {args.text_words_file}" f" -d {args.text_docs_file}" + f" -w {args.text_words_file} -d {args.text_docs_file}" ) if args.text_index in [ "from_literals", @@ -73,6 +78,7 @@ def execute(self, args) -> bool: args.index_container, volumes=[("$(pwd)", "/index")], working_directory="/index", + disable_selinux=args.disable_selinux == "yes", ) # Show the command line. diff --git a/src/qlever/commands/index.py b/src/qlever/commands/index.py index b18bb02d..efe43db9 100644 --- a/src/qlever/commands/index.py +++ b/src/qlever/commands/index.py @@ -51,7 +51,12 @@ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: "stxxl_memory", "parser_buffer_size", ], - "runtime": ["system", "image", "index_container"], + "runtime": [ + "system", + "image", + "index_container", + "disable_selinux", + ], } def additional_arguments(self, subparser) -> None: @@ -265,6 +270,7 @@ def execute(self, args) -> bool: args.index_container, volumes=[("$(pwd)", "/index")], working_directory="/index", + disable_selinux=args.disable_selinux == "yes", ) # Command for writing the settings JSON to a file. diff --git a/src/qlever/commands/start.py b/src/qlever/commands/start.py index f55abe6d..a7e6225a 100644 --- a/src/qlever/commands/start.py +++ b/src/qlever/commands/start.py @@ -69,6 +69,7 @@ def wrap_command_in_container(args, start_cmd) -> str: volumes=[("$(pwd)", "/index")], ports=[(args.port, args.port)], working_directory="/index", + disable_selinux=args.disable_selinux == "yes", ) return start_cmd @@ -142,7 +143,8 @@ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: "use_text_index", "warmup_cmd", ], - "runtime": ["system", "image", "server_container"], + "runtime": ["system", "image", "server_container", + "disable_selinux"], } def additional_arguments(self, subparser) -> None: diff --git a/src/qlever/commands/system_info.py b/src/qlever/commands/system_info.py index e8b71a2b..c48e4dd1 100644 --- a/src/qlever/commands/system_info.py +++ b/src/qlever/commands/system_info.py @@ -49,7 +49,14 @@ def should_have_qleverfile(self) -> bool: return True def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: - return {"runtime": ["system", "image", "server_container"]} + return { + "runtime": [ + "system", + "image", + "server_container", + "disable_selinux", + ] + } def additional_arguments(self, subparser) -> None: pass diff --git a/src/qlever/containerize.py b/src/qlever/containerize.py index 11150ba7..760a9070 100644 --- a/src/qlever/containerize.py +++ b/src/qlever/containerize.py @@ -12,6 +12,15 @@ from qlever.util import get_random_string, run_command +def _selinux_enforcing() -> bool: + """Check if SELinux is in enforcing mode by reading the kernel interface.""" + try: + with open("/sys/fs/selinux/enforce") as f: + return f.read().strip() == "1" + except (FileNotFoundError, PermissionError): + return False + + class ContainerizeException(Exception): pass @@ -41,6 +50,7 @@ def containerize_command( ports: list[tuple[int, int]] = [], working_directory: Optional[str] = None, use_bash: bool = True, + disable_selinux: bool = False, ) -> str: """ Get the command to run `cmd` with the given `container_system` and the @@ -54,6 +64,15 @@ def containerize_command( f" (must be one of {Containerize.supported_systems()})" ) + # Warn if SELinux is enforcing but not disabled for the container. + if _selinux_enforcing() and not disable_selinux: + log.warning( + "SELinux is enforcing, which may cause permission " + "errors with bind-mounted files. If you experience " + "issues, set DISABLE_SELINUX = yes in your " + "Qleverfile or use --disable-selinux yes" + ) + # Set user and group ids. This is important so that the files created # by the containerized command are owned by the user running the # command. @@ -77,11 +96,18 @@ def containerize_command( f" -w {working_directory}" if working_directory is not None else "" ) + # If SELinux is disabled for the container, add the security option + # so that the container can access bind-mounted host files. + selinux_option = ( + " --security-opt label=disable" if disable_selinux else "" + ) + # Construct the command that runs `cmd` with the given container # system. containerized_cmd = ( f"{container_system} {run_subcommand}" f"{user_option}" + f"{selinux_option}" f" -v /etc/localtime:/etc/localtime:ro" f"{volume_options}" f"{port_options}" @@ -155,6 +181,7 @@ def run_in_container(cmd: str, args) -> Optional[str]: if args.system in Containerize.supported_systems(): if not args.server_container: args.server_container = get_random_string(20) + disable_selinux = getattr(args, "disable_selinux", "no") == "yes" run_cmd = Containerize().containerize_command( cmd, args.system, @@ -163,5 +190,6 @@ def run_in_container(cmd: str, args) -> Optional[str]: args.server_container, volumes=[("$(pwd)", "/index")], working_directory="/index", + disable_selinux=disable_selinux, ) return run_command(run_cmd, return_output=True) diff --git a/src/qlever/qleverfile.py b/src/qlever/qleverfile.py index 83ed8794..a6412e18 100644 --- a/src/qlever/qleverfile.py +++ b/src/qlever/qleverfile.py @@ -380,6 +380,16 @@ def arg(*args, **kwargs): type=str, help=f"The name of the container used by `{script_name} start`", ) + runtime_args["disable_selinux"] = arg( + "--disable-selinux", + choices=["yes", "no"], + default="no", + help=( + "Disable SELinux confinement for the container, " + "use this if you get permission errors on " + "bind-mounted files (e.g. on Fedora or RHEL)" + ), + ) ui_args["ui_port"] = arg( "--ui-port", diff --git a/src/qlever/util.py b/src/qlever/util.py index 60006e40..820e6d8b 100644 --- a/src/qlever/util.py +++ b/src/qlever/util.py @@ -323,6 +323,7 @@ def binary_exists(binary: str, cmd_arg: str, args) -> bool: is_containerized = args.system in Containerize.supported_systems() cmd = f"{binary} --help" + disable_selinux = getattr(args, "disable_selinux", "no") == "yes" if is_containerized: cmd = Containerize().containerize_command( cmd, @@ -332,6 +333,7 @@ def binary_exists(binary: str, cmd_arg: str, args) -> bool: "qlever.check-binary", volumes=[("$(pwd)", "/index")], working_directory="/index", + disable_selinux=disable_selinux, ) try: diff --git a/test/qlever/commands/test_index_other_methods.py b/test/qlever/commands/test_index_other_methods.py index 2a808ad4..e0d27ec4 100644 --- a/test/qlever/commands/test_index_other_methods.py +++ b/test/qlever/commands/test_index_other_methods.py @@ -48,7 +48,12 @@ def test_relevant_qleverfile_arguments(self): "stxxl_memory", "parser_buffer_size", ], - "runtime": ["system", "image", "index_container"], + "runtime": [ + "system", + "image", + "index_container", + "disable_selinux", + ], }, ) diff --git a/test/qlever/commands/test_start_execute.py b/test/qlever/commands/test_start_execute.py index c5e23505..197a5638 100644 --- a/test/qlever/commands/test_start_execute.py +++ b/test/qlever/commands/test_start_execute.py @@ -112,6 +112,7 @@ def test_wrap_command_in_container(mock_containerize_command): volumes=[("$(pwd)", "/index")], ports=[(args.port, args.port)], working_directory="/index", + disable_selinux=args.disable_selinux == "yes", ) # check start command was successfully returned start_command = "Test_Container_Command" diff --git a/test/qlever/commands/test_start_other_methods.py b/test/qlever/commands/test_start_other_methods.py index 2848dbb3..f08d8d1d 100644 --- a/test/qlever/commands/test_start_other_methods.py +++ b/test/qlever/commands/test_start_other_methods.py @@ -44,7 +44,12 @@ def test_relevant_qleverfile_arguments(self): "use_text_index", "warmup_cmd", ], - "runtime": ["system", "image", "server_container"], + "runtime": [ + "system", + "image", + "server_container", + "disable_selinux", + ], }, ) From 5e17d47e6089216dc1de13cc160b8bfd1de66bc0 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Thu, 2 Apr 2026 16:36:04 +0200 Subject: [PATCH 2/7] Add qlever index MemoryMonitor --- src/qlever/commands/index.py | 10 ++- src/qlever/memory_monitor.py | 164 +++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/qlever/memory_monitor.py diff --git a/src/qlever/commands/index.py b/src/qlever/commands/index.py index c01b2420..19f1ab04 100644 --- a/src/qlever/commands/index.py +++ b/src/qlever/commands/index.py @@ -8,6 +8,7 @@ from qlever.command import QleverCommand from qlever.containerize import Containerize from qlever.log import log +from qlever.memory_monitor import MemoryMonitor from qlever.util import ( binary_exists, get_existing_index_files, @@ -328,7 +329,14 @@ def execute(self, args) -> bool: # Run the index command. try: - run_command(index_cmd, show_output=True) + with MemoryMonitor( + engine="qlever", + dataset=args.name, + cmdline_regex=args.index_binary, + container=args.index_container, + system=args.system, + ): + run_command(index_cmd, show_output=True) except Exception as e: log.error(f"Building the index failed: {e}") return False diff --git a/src/qlever/memory_monitor.py b/src/qlever/memory_monitor.py new file mode 100644 index 00000000..d7709a7c --- /dev/null +++ b/src/qlever/memory_monitor.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +import json +import re +import threading +import time +from datetime import datetime +from pathlib import Path + +import psutil + +from qlever.containerize import Containerize +from qlever.log import log +from qlever.util import format_size, run_command + + +def parse_container_mem_usage(usage: str) -> int: + """ + Parse a memory usage string from `docker stats` / `podman stats` + like "4.2GiB", "150MiB", "512KiB" into bytes. + """ + usage = usage.strip() + units = { + "TIB": 1024**4, + "GIB": 1024**3, + "MIB": 1024**2, + "KIB": 1024, + "B": 1, + } + for suffix, multiplier in units.items(): + if usage.upper().endswith(suffix): + number = float(usage[: len(usage) - len(suffix)]) + return int(number * multiplier) + return 0 + + +class MemoryMonitor: + """ + Monitor memory usage of an index-building process. Works in both + native mode (via psutil) and container mode (via docker/podman stats). + + Usage as a context manager: + + with MemoryMonitor(engine="qlever", dataset="wikidata", + cmdline_regex=r"qlever-index"): + run_command(cmd, show_output=True) + + # For container mode: + with MemoryMonitor(engine="qlever", dataset="wikidata", + cmdline_regex=r"qlever-index", + container="qlever.index.wikidata", + system="docker"): + run_command(cmd, show_output=True) + """ + + def __init__( + self, + engine: str, + dataset: str, + cmdline_regex: str, + container: str | None = None, + system: str | None = None, + interval: float = 1.0, + output_dir: Path = Path.cwd(), + ): + self.engine = engine + self.dataset = dataset + self.cmdline_regex = cmdline_regex + self.container = container + self.system = system + self.interval = interval + self.output_dir = Path(output_dir) + self.peak_rss = 0 + self.samples: list[tuple[float, int]] = [] + self.stop_event = threading.Event() + self.thread: threading.Thread | None = None + self.start_time: float = 0 + + def sample_native(self) -> int: + """ + Find the index process among our children by matching its + command line, then sum RSS of that process and all its + descendants. + """ + me = psutil.Process() + for child in me.children(recursive=True): + try: + cmdline = " ".join(child.cmdline()) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + if re.search(self.cmdline_regex, cmdline): + rss = child.memory_info().rss + for grandchild in child.children(recursive=True): + try: + rss += grandchild.memory_info().rss + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass + return rss + return 0 + + def sample_container(self) -> int: + """ + Query the container runtime for the memory usage of the + index container. + """ + try: + output = run_command( + f"{self.system} stats --no-stream" + f" --format '{{{{.MemUsage}}}}' {self.container}", + return_output=True, + ) + usage = output.strip().split("/")[0].strip() + return parse_container_mem_usage(usage) + except Exception: + return 0 + + def run_loop(self): + sample = ( + self.sample_container + if self.system in Containerize.supported_systems() + else self.sample_native + ) + while not self.stop_event.is_set(): + rss = sample() + self.peak_rss = max(self.peak_rss, rss) + elapsed = time.monotonic() - self.start_time + self.samples.append((elapsed, rss)) + self.stop_event.wait(self.interval) + + def save(self): + path = ( + self.output_dir / f"{self.engine}.{self.dataset}.memory-log.json" + ) + data = { + "engine": self.engine, + "dataset": self.dataset, + "start_time": datetime.fromtimestamp( + time.time() - (time.monotonic() - self.start_time) + ).isoformat(timespec="seconds"), + "peak_rss_bytes": self.peak_rss, + "peak_rss_human": format_size(self.peak_rss), + "elapsed_s": ( + round(self.samples[-1][0], 1) if self.samples else 0 + ), + "samples": [ + {"elapsed_s": round(t, 1), "rss_bytes": r} + for t, r in self.samples + ], + } + with open(path, "w") as f: + json.dump(data, f, indent=2) + + def __enter__(self): + self.start_time = time.monotonic() + self.thread = threading.Thread(target=self.run_loop, daemon=True) + self.thread.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop_event.set() + self.thread.join() + self.save() + log.info(f"Peak memory usage: {format_size(self.peak_rss)}") + return False From c8d913953739e9c93f1e3cf8b352e17bda4a4f03 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Thu, 2 Apr 2026 16:36:28 +0200 Subject: [PATCH 3/7] Change default host to use 127.0.0.1 ipv4 address --- src/qlever/commands/start.py | 13 ++++++------- src/qlever/qleverfile.py | 16 ++++++---------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/qlever/commands/start.py b/src/qlever/commands/start.py index ea21960b..2bfb2132 100644 --- a/src/qlever/commands/start.py +++ b/src/qlever/commands/start.py @@ -1,6 +1,5 @@ from __future__ import annotations -import subprocess import time from pathlib import Path @@ -84,9 +83,9 @@ def wrap_command_in_container(args, start_cmd) -> str: # Set the index description. -def set_index_description(access_arg, port, desc) -> bool: +def set_index_description(access_arg, endpoint_url, desc) -> bool: curl_cmd = ( - f"curl -Gs http://localhost:{port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "index-description={desc}"' f" {access_arg} > /dev/null" ) @@ -100,9 +99,9 @@ def set_index_description(access_arg, port, desc) -> bool: # Set the text description. -def set_text_description(access_arg, port, text_desc) -> bool: +def set_text_description(access_arg, endpoint_url, text_desc) -> bool: curl_cmd = ( - f"curl -Gs http://localhost:{port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "text-description={text_desc}"' f" {access_arg} > /dev/null" ) @@ -306,13 +305,13 @@ def execute(self, args) -> bool: access_arg = f'--data-urlencode "access-token={args.access_token}"' if args.description: ret = set_index_description( - access_arg, args.port, args.description + access_arg, args.endpoint_url, args.description ) if not ret: return False if args.text_description: ret = set_text_description( - access_arg, args.port, args.text_description + access_arg, args.endpoint_url, args.text_description ) if not ret: return False diff --git a/src/qlever/qleverfile.py b/src/qlever/qleverfile.py index 70363e67..0d1820c5 100644 --- a/src/qlever/qleverfile.py +++ b/src/qlever/qleverfile.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -import socket import subprocess from configparser import ConfigParser, ExtendedInterpolation, RawConfigParser from importlib import import_module @@ -512,15 +511,12 @@ def read(qleverfile_path): if index.get("use_patterns", None) == "no": server["use_patterns"] = "no" - # Add other non-trivial default values. - try: - if config["server"].get("host_name") is None: - config["server"]["host_name"] = socket.gethostname() - except Exception: - log.warning( - "Could not get the hostname, using `localhost` as default" - ) - pass + # Add other non-trivial default values. We use 127.0.0.1 as the + # default host name because it avoids IPv6 resolution issues with + # container port forwarding (podman/docker rootless only forward on + # IPv4). Users who need remote access should set HOST_NAME explicitly. + if config["server"].get("host_name") is None: + config["server"]["host_name"] = "127.0.0.1" # Return the parsed Qleverfile with the added inherited values. return config From e0607fd614e300a8bce0a3348800ccd3b894ffd8 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 7 Apr 2026 16:16:05 +0200 Subject: [PATCH 4/7] Revert "Add qlever index MemoryMonitor" This reverts commit 5e17d47e6089216dc1de13cc160b8bfd1de66bc0. --- src/qlever/commands/index.py | 10 +-- src/qlever/memory_monitor.py | 164 ----------------------------------- 2 files changed, 1 insertion(+), 173 deletions(-) delete mode 100644 src/qlever/memory_monitor.py diff --git a/src/qlever/commands/index.py b/src/qlever/commands/index.py index 19f1ab04..c01b2420 100644 --- a/src/qlever/commands/index.py +++ b/src/qlever/commands/index.py @@ -8,7 +8,6 @@ from qlever.command import QleverCommand from qlever.containerize import Containerize from qlever.log import log -from qlever.memory_monitor import MemoryMonitor from qlever.util import ( binary_exists, get_existing_index_files, @@ -329,14 +328,7 @@ def execute(self, args) -> bool: # Run the index command. try: - with MemoryMonitor( - engine="qlever", - dataset=args.name, - cmdline_regex=args.index_binary, - container=args.index_container, - system=args.system, - ): - run_command(index_cmd, show_output=True) + run_command(index_cmd, show_output=True) except Exception as e: log.error(f"Building the index failed: {e}") return False diff --git a/src/qlever/memory_monitor.py b/src/qlever/memory_monitor.py deleted file mode 100644 index d7709a7c..00000000 --- a/src/qlever/memory_monitor.py +++ /dev/null @@ -1,164 +0,0 @@ -from __future__ import annotations - -import json -import re -import threading -import time -from datetime import datetime -from pathlib import Path - -import psutil - -from qlever.containerize import Containerize -from qlever.log import log -from qlever.util import format_size, run_command - - -def parse_container_mem_usage(usage: str) -> int: - """ - Parse a memory usage string from `docker stats` / `podman stats` - like "4.2GiB", "150MiB", "512KiB" into bytes. - """ - usage = usage.strip() - units = { - "TIB": 1024**4, - "GIB": 1024**3, - "MIB": 1024**2, - "KIB": 1024, - "B": 1, - } - for suffix, multiplier in units.items(): - if usage.upper().endswith(suffix): - number = float(usage[: len(usage) - len(suffix)]) - return int(number * multiplier) - return 0 - - -class MemoryMonitor: - """ - Monitor memory usage of an index-building process. Works in both - native mode (via psutil) and container mode (via docker/podman stats). - - Usage as a context manager: - - with MemoryMonitor(engine="qlever", dataset="wikidata", - cmdline_regex=r"qlever-index"): - run_command(cmd, show_output=True) - - # For container mode: - with MemoryMonitor(engine="qlever", dataset="wikidata", - cmdline_regex=r"qlever-index", - container="qlever.index.wikidata", - system="docker"): - run_command(cmd, show_output=True) - """ - - def __init__( - self, - engine: str, - dataset: str, - cmdline_regex: str, - container: str | None = None, - system: str | None = None, - interval: float = 1.0, - output_dir: Path = Path.cwd(), - ): - self.engine = engine - self.dataset = dataset - self.cmdline_regex = cmdline_regex - self.container = container - self.system = system - self.interval = interval - self.output_dir = Path(output_dir) - self.peak_rss = 0 - self.samples: list[tuple[float, int]] = [] - self.stop_event = threading.Event() - self.thread: threading.Thread | None = None - self.start_time: float = 0 - - def sample_native(self) -> int: - """ - Find the index process among our children by matching its - command line, then sum RSS of that process and all its - descendants. - """ - me = psutil.Process() - for child in me.children(recursive=True): - try: - cmdline = " ".join(child.cmdline()) - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - if re.search(self.cmdline_regex, cmdline): - rss = child.memory_info().rss - for grandchild in child.children(recursive=True): - try: - rss += grandchild.memory_info().rss - except (psutil.NoSuchProcess, psutil.AccessDenied): - pass - return rss - return 0 - - def sample_container(self) -> int: - """ - Query the container runtime for the memory usage of the - index container. - """ - try: - output = run_command( - f"{self.system} stats --no-stream" - f" --format '{{{{.MemUsage}}}}' {self.container}", - return_output=True, - ) - usage = output.strip().split("/")[0].strip() - return parse_container_mem_usage(usage) - except Exception: - return 0 - - def run_loop(self): - sample = ( - self.sample_container - if self.system in Containerize.supported_systems() - else self.sample_native - ) - while not self.stop_event.is_set(): - rss = sample() - self.peak_rss = max(self.peak_rss, rss) - elapsed = time.monotonic() - self.start_time - self.samples.append((elapsed, rss)) - self.stop_event.wait(self.interval) - - def save(self): - path = ( - self.output_dir / f"{self.engine}.{self.dataset}.memory-log.json" - ) - data = { - "engine": self.engine, - "dataset": self.dataset, - "start_time": datetime.fromtimestamp( - time.time() - (time.monotonic() - self.start_time) - ).isoformat(timespec="seconds"), - "peak_rss_bytes": self.peak_rss, - "peak_rss_human": format_size(self.peak_rss), - "elapsed_s": ( - round(self.samples[-1][0], 1) if self.samples else 0 - ), - "samples": [ - {"elapsed_s": round(t, 1), "rss_bytes": r} - for t, r in self.samples - ], - } - with open(path, "w") as f: - json.dump(data, f, indent=2) - - def __enter__(self): - self.start_time = time.monotonic() - self.thread = threading.Thread(target=self.run_loop, daemon=True) - self.thread.start() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.stop_event.set() - self.thread.join() - self.save() - log.info(f"Peak memory usage: {format_size(self.peak_rss)}") - return False From 163af87a3f9fd5a1d7fe864072146bb3d0d6da3b Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 7 Apr 2026 17:29:45 +0200 Subject: [PATCH 5/7] Add a warning if the hostname on user's system resolves to IPV6 first in containerized mode --- src/qlever/config.py | 23 +++++++++++++++++++++++ src/qlever/qleverfile.py | 16 ++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/qlever/config.py b/src/qlever/config.py index f877512a..ead7e28a 100644 --- a/src/qlever/config.py +++ b/src/qlever/config.py @@ -2,6 +2,7 @@ import argparse import os +import socket import traceback from importlib.metadata import version from pathlib import Path @@ -10,6 +11,7 @@ from termcolor import colored from qlever import command_objects, engine_name, script_name +from qlever.containerize import Containerize from qlever.log import log, log_levels from qlever.qleverfile import Qleverfile @@ -226,6 +228,27 @@ def add_qleverfile_option(parser): "arguments on the command line. This is possible, " "but not recommended.") + # Warn if the host name resolves to IPv6 first and the system is + # a container runtime. Container port forwarding (podman/docker in + # rootless mode) typically only forwards on IPv4, so curl will + # connect via IPv6 and fail. + host_name = getattr(args, "host_name", None) + system = getattr(args, "system", "native") + if host_name and system in Containerize.supported_systems(): + try: + addrinfo = socket.getaddrinfo(host_name, None) + if addrinfo and addrinfo[0][0] == socket.AF_INET6: + log.warning( + f"Your system resolves '{host_name}' to an " + "IPv6 address first, which may cause connection " + "failures with containerized servers. If you face " + "connection issues, consider setting HOST_NAME to an " + "IPv4 address in your Qleverfile or using " + "--host-name 127.0.0.1" + ) + except Exception: + pass + # Warn if the old binary names are still being used. if "IndexBuilderMain" in getattr(args, "index_binary", ""): log.warning("The index binary has been renamed from " diff --git a/src/qlever/qleverfile.py b/src/qlever/qleverfile.py index 0d1820c5..b7d0086e 100644 --- a/src/qlever/qleverfile.py +++ b/src/qlever/qleverfile.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +import socket import subprocess from configparser import ConfigParser, ExtendedInterpolation, RawConfigParser from importlib import import_module @@ -511,12 +512,15 @@ def read(qleverfile_path): if index.get("use_patterns", None) == "no": server["use_patterns"] = "no" - # Add other non-trivial default values. We use 127.0.0.1 as the - # default host name because it avoids IPv6 resolution issues with - # container port forwarding (podman/docker rootless only forward on - # IPv4). Users who need remote access should set HOST_NAME explicitly. - if config["server"].get("host_name") is None: - config["server"]["host_name"] = "127.0.0.1" + # Add other non-trivial default values. + try: + if config["server"].get("host_name") is None: + config["server"]["host_name"] = socket.gethostname() + except Exception: + log.warning( + "Could not get the hostname, using `localhost` as default" + ) + config["server"]["host_name"] = "localhost" # Return the parsed Qleverfile with the added inherited values. return config From 8ccd3dad16b86f4c07d371f8f2286750f538d19a Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Tue, 7 Apr 2026 17:36:14 +0200 Subject: [PATCH 6/7] Fix failing tests because of using args.host_name instead of hard-code localhost in start.py --- test/qlever/commands/test_start_execute.py | 30 ++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/test/qlever/commands/test_start_execute.py b/test/qlever/commands/test_start_execute.py index ee0686d3..c356af4e 100644 --- a/test/qlever/commands/test_start_execute.py +++ b/test/qlever/commands/test_start_execute.py @@ -180,17 +180,19 @@ def test_set_index_description_success(mock_log, mock_run_cmd): # Setup args args = MagicMock() args.access_token = True + args.host_name = "localhost" args.port = 1234 args.description = "TestDescription" access_arg = f'--data-urlencode "access-token={args.access_token}"' + endpoint_url = f"http://{args.host_name}:{args.port}" # Execute the function qlever.commands.start.set_index_description( - access_arg, args.port, args.description + access_arg, endpoint_url, args.description ) # Asserts curl_cmd = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "index-description={args.description}"' f" {access_arg} > /dev/null" ) @@ -208,21 +210,23 @@ def test_set_index_description_exception(mock_log, mock_run_cmd): # Setup args args = MagicMock() args.access_token = True + args.host_name = "localhost" args.port = 1234 args.description = "ErrorDescription" access_arg = f'--data-urlencode "access-token={args.access_token}"' + endpoint_url = f"http://{args.host_name}:{args.port}" # Simulate an exception when run_command is called mock_run_cmd.side_effect = Exception("Mocked command failure") # Execute the function qlever.commands.start.set_index_description( - access_arg, args.port, args.description + access_arg, endpoint_url, args.description ) # Asserts curl_cmd = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "index-description={args.description}"' f" {access_arg} > /dev/null" ) @@ -244,17 +248,19 @@ def test_set_text_description_success(mock_log, mock_run_cmd): # Setup args args = MagicMock() args.access_token = True + args.host_name = "localhost" args.port = 1234 args.description = "TestDescription" access_arg = f'--data-urlencode "access-token={args.access_token}"' + endpoint_url = f"http://{args.host_name}:{args.port}" # Execute the function qlever.commands.start.set_text_description( - access_arg, args.port, args.description + access_arg, endpoint_url, args.description ) # Asserts curl_cmd = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "text-description={args.description}"' f" {access_arg} > /dev/null" ) @@ -272,21 +278,23 @@ def test_set_text_description_exception(mock_log, mock_run_cmd): # Setup args args = MagicMock() args.access_token = True + args.host_name = "localhost" args.port = 1234 args.description = "ErrorDescription" access_arg = f'--data-urlencode "access-token={args.access_token}"' + endpoint_url = f"http://{args.host_name}:{args.port}" # Simulate an exception when run_command is called mock_run_cmd.side_effect = Exception("Mocked command failure") # Execute the function qlever.commands.start.set_text_description( - access_arg, args.port, args.description + access_arg, endpoint_url, args.description ) # Asserts curl_cmd = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "text-description={args.description}"' f" {access_arg} > /dev/null" ) @@ -620,6 +628,7 @@ def test_execute_containerize_and_description( # Setup args args = MagicMock() args.kill_existing_with_same_port = True + args.host_name = "localhost" args.port = 1234 args.server_binary = "/test/path/server_binary" args.name = "TestName" @@ -672,13 +681,14 @@ def test_execute_containerize_and_description( run_call_1 = f"{args.system} rm -f {args.server_container}" run_call_2 = "TestStart2" access_arg = f'--data-urlencode "access-token={args.access_token}"' + endpoint_url = f"http://{args.host_name}:{args.port}" run_call_3 = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "index-description={args.description}"' f" {access_arg} > /dev/null" ) run_call_4 = ( - f"curl -Gs http://localhost:{args.port}/api" + f"curl -Gs {endpoint_url}/api" f' --data-urlencode "text-description=' f'{args.text_description}"' f" {access_arg} > /dev/null" From e2f02377d079a6db4309cabbda0236757eea7aa5 Mon Sep 17 00:00:00 2001 From: tanmay-9 Date: Wed, 8 Apr 2026 16:00:17 +0200 Subject: [PATCH 7/7] Add disable_selinux and ipv6 warning to qlever ui as well --- src/qlever/commands/ui.py | 2 ++ src/qlever/config.py | 29 +++++++++++++++++++++++++---- src/qlever/containerize.py | 18 ------------------ src/qlever/util.py | 9 +++++++++ 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/qlever/commands/ui.py b/src/qlever/commands/ui.py index 5c12e1d1..f6a30b7a 100644 --- a/src/qlever/commands/ui.py +++ b/src/qlever/commands/ui.py @@ -56,6 +56,7 @@ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]: return { "data": ["name"], "server": ["host_name", "port"], + "runtime": ["disable_selinux"], "ui": [ "ui_port", "ui_config", @@ -130,6 +131,7 @@ def execute(self, args) -> bool: start_ui_cmd = ( f"{args.ui_system} run -d " f"--volume $(pwd):/app/db " + f"{'--security-opt label=disable ' if args.disable_selinux == 'yes' else ''}" f"--env QLEVERUI_DATABASE_URL=sqlite:////app/db/{ui_db_file} " f"--publish {args.ui_port}:7000 " f"--name {args.ui_container} " diff --git a/src/qlever/config.py b/src/qlever/config.py index ead7e28a..826d780c 100644 --- a/src/qlever/config.py +++ b/src/qlever/config.py @@ -14,6 +14,7 @@ from qlever.containerize import Containerize from qlever.log import log, log_levels from qlever.qleverfile import Qleverfile +from qlever.util import selinux_enforcing # Simple exception class for configuration errors (the class need not do @@ -233,7 +234,10 @@ def add_qleverfile_option(parser): # rootless mode) typically only forwards on IPv4, so curl will # connect via IPv6 and fail. host_name = getattr(args, "host_name", None) - system = getattr(args, "system", "native") + system = getattr(args, "ui_system", None) or getattr( + args, "system", "native" + ) + ipv6_warning = False if host_name and system in Containerize.supported_systems(): try: addrinfo = socket.getaddrinfo(host_name, None) @@ -242,13 +246,30 @@ def add_qleverfile_option(parser): f"Your system resolves '{host_name}' to an " "IPv6 address first, which may cause connection " "failures with containerized servers. If you face " - "connection issues, consider setting HOST_NAME to an " - "IPv4 address in your Qleverfile or using " - "--host-name 127.0.0.1" + "connection issues, consider using an explicit " + "IPv4 address like 127.0.0.1 (via HOST_NAME in " + "your Qleverfile or --host-name on the command line)" ) + ipv6_warning = True except Exception: pass + # Warn if SELinux is enforcing but not disabled for the container. + disable_selinux = getattr(args, "disable_selinux", None) + if ( + system in Containerize.supported_systems() + and disable_selinux == "no" + and selinux_enforcing() + ): + if ipv6_warning: + log.info("") + log.warning( + "SELinux is enforcing, which may cause permission " + "errors with bind-mounted files. If you experience " + "issues, set DISABLE_SELINUX = yes in your " + "Qleverfile or use --disable-selinux yes" + ) + # Warn if the old binary names are still being used. if "IndexBuilderMain" in getattr(args, "index_binary", ""): log.warning("The index binary has been renamed from " diff --git a/src/qlever/containerize.py b/src/qlever/containerize.py index 760a9070..82b350f0 100644 --- a/src/qlever/containerize.py +++ b/src/qlever/containerize.py @@ -12,15 +12,6 @@ from qlever.util import get_random_string, run_command -def _selinux_enforcing() -> bool: - """Check if SELinux is in enforcing mode by reading the kernel interface.""" - try: - with open("/sys/fs/selinux/enforce") as f: - return f.read().strip() == "1" - except (FileNotFoundError, PermissionError): - return False - - class ContainerizeException(Exception): pass @@ -64,15 +55,6 @@ def containerize_command( f" (must be one of {Containerize.supported_systems()})" ) - # Warn if SELinux is enforcing but not disabled for the container. - if _selinux_enforcing() and not disable_selinux: - log.warning( - "SELinux is enforcing, which may cause permission " - "errors with bind-mounted files. If you experience " - "issues, set DISABLE_SELINUX = yes in your " - "Qleverfile or use --disable-selinux yes" - ) - # Set user and group ids. This is important so that the files created # by the containerized command are owned by the user running the # command. diff --git a/src/qlever/util.py b/src/qlever/util.py index f346ea95..2b29ff03 100644 --- a/src/qlever/util.py +++ b/src/qlever/util.py @@ -402,6 +402,15 @@ def input_files_exist(input_files: str) -> bool: return True +def selinux_enforcing() -> bool: + """Check if SELinux is in enforcing mode by reading the kernel interface.""" + try: + with open("/sys/fs/selinux/enforce") as f: + return f.read().strip() == "1" + except (FileNotFoundError, PermissionError): + return False + + def build_image(build_cmd: str, system: str, image: str) -> bool: """ Build a container image using the build command, container system and