From 5a743aa6e86206fc07f16427a735646586a358ae Mon Sep 17 00:00:00 2001 From: Benny Zlotnik Date: Sun, 9 Nov 2025 14:18:25 +0200 Subject: [PATCH 1/2] add info if package is missing Signed-off-by: Benny Zlotnik --- .../jumpstarter/jumpstarter/client/lease.py | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/jumpstarter/jumpstarter/client/lease.py b/packages/jumpstarter/jumpstarter/client/lease.py index 32c81fc6d..32167a5d1 100644 --- a/packages/jumpstarter/jumpstarter/client/lease.py +++ b/packages/jumpstarter/jumpstarter/client/lease.py @@ -241,6 +241,33 @@ async def _wait_for_ready_connection(self, path: str): else: logger.warning("Waiting for ready connection to %s: %s", path, e) await sleep(5) + except ModuleNotFoundError as e: + module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1] + + if module_name.startswith("jumpstarter_"): + logger.error( + "Missing Jumpstarter driver module '%s' while connecting to exporter. " + "This usually indicates a version mismatch between your client and the exporter.", + module_name, + ) + raise ConnectionError( + f"Missing Jumpstarter driver module '{module_name}'.\n\n" + "This usually indicates a version mismatch between your client and the exporter.\n" + "Please try to update your client to the latest version\n" + ) from e + else: + logger.error( + "Missing Python module '%s' while connecting to exporter. " + "This module needs to be installed in your environment.", + module_name, + ) + raise ConnectionError( + f"Missing Python module '{module_name}'.\n\n" + "Please install the missing module:\n" + f" pip install {module_name}\n\n" + "or if using uv:\n" + f" uv pip install {module_name}" + ) from e except Exception as e: logger.error("Unexpected error while waiting for ready connection to %s: %s", path, e) raise ConnectionError("Unexpected error while waiting for ready connection to %s" % path) from e @@ -252,7 +279,7 @@ async def _monitor(): while True: lease = await self.get() if lease.effective_begin_time and lease.effective_duration: - if lease.effective_end_time: # already ended + if lease.effective_end_time: # already ended end_time = lease.effective_end_time else: end_time = lease.effective_begin_time + lease.duration @@ -263,8 +290,11 @@ async def _monitor(): break # Log once when entering the threshold window if threshold - timedelta(seconds=check_interval) <= remain < threshold: - logger.info("Lease {} ending in {} minutes at {}".format( - self.name, int((remain.total_seconds() + 30) // 60), end_time)) + logger.info( + "Lease {} ending in {} minutes at {}".format( + self.name, int((remain.total_seconds() + 30) // 60), end_time + ) + ) await sleep(min(remain.total_seconds(), check_interval)) else: await sleep(1) @@ -317,19 +347,17 @@ def _is_non_interactive(self) -> bool: def _is_terminal_available(self) -> bool: """Check if we're running in a terminal/TTY.""" return ( - hasattr(sys.stdout, 'isatty') and - sys.stdout.isatty() and - hasattr(sys.stderr, 'isatty') and - sys.stderr.isatty() + hasattr(sys.stdout, "isatty") + and sys.stdout.isatty() + and hasattr(sys.stderr, "isatty") + and sys.stderr.isatty() ) def __enter__(self): self.start_time = datetime.now() if self._should_show_spinner: self.spinner = self.console.status( - f"Acquiring lease {self.lease_name or '...'}...", - spinner="dots", - spinner_style="blue" + f"Acquiring lease {self.lease_name or '...'}...", spinner="dots", spinner_style="blue" ) self.spinner.start() return self @@ -343,18 +371,18 @@ def update_status(self, message: str): if self.spinner and self._should_show_spinner: self._current_message = f"[blue]{message}[/blue]" elapsed = datetime.now() - self.start_time - elapsed_str = str(elapsed).split('.')[0] # Remove microseconds + elapsed_str = str(elapsed).split(".")[0] # Remove microseconds self.spinner.update(f"{self._current_message} [dim]({elapsed_str})[/dim]") else: # Log info message when no console is available elapsed = datetime.now() - self.start_time - elapsed_str = str(elapsed).split('.')[0] # Remove microseconds + elapsed_str = str(elapsed).split(".")[0] # Remove microseconds logger.info(f"{message} ({elapsed_str})") def tick(self): """Update the spinner with current elapsed time without changing the message.""" if self.spinner and self._should_show_spinner and self._current_message: elapsed = datetime.now() - self.start_time - elapsed_str = str(elapsed).split('.')[0] # Remove microseconds + elapsed_str = str(elapsed).split(".")[0] # Remove microseconds # Use the stored current message and update with new elapsed time self.spinner.update(f"{self._current_message} [dim]({elapsed_str})[/dim]") From ee45dc5bca716afea2290a66d5a00e9718dc284e Mon Sep 17 00:00:00 2001 From: Benny Zlotnik Date: Mon, 10 Nov 2025 12:31:39 +0200 Subject: [PATCH 2/2] move missing package handling to importlib Signed-off-by: Benny Zlotnik --- .../jumpstarter/jumpstarter/client/lease.py | 29 ++------------- .../jumpstarter/common/importlib.py | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/jumpstarter/jumpstarter/client/lease.py b/packages/jumpstarter/jumpstarter/client/lease.py index 32167a5d1..5bf087ade 100644 --- a/packages/jumpstarter/jumpstarter/client/lease.py +++ b/packages/jumpstarter/jumpstarter/client/lease.py @@ -241,33 +241,8 @@ async def _wait_for_ready_connection(self, path: str): else: logger.warning("Waiting for ready connection to %s: %s", path, e) await sleep(5) - except ModuleNotFoundError as e: - module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1] - - if module_name.startswith("jumpstarter_"): - logger.error( - "Missing Jumpstarter driver module '%s' while connecting to exporter. " - "This usually indicates a version mismatch between your client and the exporter.", - module_name, - ) - raise ConnectionError( - f"Missing Jumpstarter driver module '{module_name}'.\n\n" - "This usually indicates a version mismatch between your client and the exporter.\n" - "Please try to update your client to the latest version\n" - ) from e - else: - logger.error( - "Missing Python module '%s' while connecting to exporter. " - "This module needs to be installed in your environment.", - module_name, - ) - raise ConnectionError( - f"Missing Python module '{module_name}'.\n\n" - "Please install the missing module:\n" - f" pip install {module_name}\n\n" - "or if using uv:\n" - f" uv pip install {module_name}" - ) from e + except ConnectionError: + raise except Exception as e: logger.error("Unexpected error while waiting for ready connection to %s: %s", path, e) raise ConnectionError("Unexpected error while waiting for ready connection to %s" % path) from e diff --git a/packages/jumpstarter/jumpstarter/common/importlib.py b/packages/jumpstarter/jumpstarter/common/importlib.py index 04c8c8160..cc03c35e2 100644 --- a/packages/jumpstarter/jumpstarter/common/importlib.py +++ b/packages/jumpstarter/jumpstarter/common/importlib.py @@ -1,9 +1,12 @@ # Reference: https://docs.djangoproject.com/en/5.0/_modules/django/utils/module_loading/#import_string +import logging import sys from fnmatch import fnmatchcase from importlib import import_module +logger = logging.getLogger(__name__) + def cached_import(module_path, class_name): # Check whether module is loaded and fully initialized. @@ -36,5 +39,37 @@ def import_class(class_path: str, allow: list[str], unsafe: bool): raise ImportError(f"{class_path} doesn't look like a class path") from e try: return cached_import(module_path, class_name) + except ModuleNotFoundError as e: + module_name = str(e).split("'")[1] if "'" in str(e) else str(e).split()[-1] + + is_jumpstarter_driver = unsafe or any(fnmatchcase(class_path, pattern) for pattern in allow) + + if is_jumpstarter_driver: + logger.error( + "Missing Jumpstarter driver module '%s' for class '%s'. " + "This usually indicates a version mismatch between your client and the exporter.", + module_name, + class_path, + ) + raise ConnectionError( + f"Missing Jumpstarter driver module '{module_name}'.\n\n" + "This usually indicates a version mismatch between your client and the exporter.\n" + "Please try to update your client to the latest version and ensure the exporter " + "has the correct version installed.\n" + ) from e + else: + logger.error( + "Missing Python module '%s' while importing '%s'. " + "This module needs to be installed in your environment.", + module_name, + class_path, + ) + raise ConnectionError( + f"Missing Python module '{module_name}'.\n\n" + "Please install the missing module:\n" + f" pip install {module_name}\n\n" + "or if using uv:\n" + f" uv pip install {module_name}" + ) from e except AttributeError as e: raise ImportError(f"{module_path} doesn't have specified class {class_name}") from e