From cfcdd6acde65798d5770c3b91f913f695a45bf2a Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 4 Nov 2025 10:57:59 +0100 Subject: [PATCH 01/14] New class for exception without traceback added --- pulpcore/exceptions/__init__.py | 2 ++ pulpcore/exceptions/base.py | 41 +++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index 476cd60d3da..cd8a8b09c7c 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -1,9 +1,11 @@ from .base import ( PulpException, + PulpExceptionNoTrace, ResourceImmutableError, TimeoutException, exception_to_dict, DomainProtectedError, + DnsDomainNameException, ) from .validation import ( DigestValidationError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index ecddb2f084f..b0792048be9 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -26,7 +26,9 @@ def __str__(self): expected to implement it's own __str__() method. The return value is used by Pulp when recording the exception in the database. """ - raise NotImplementedError("Subclasses of PulpException must implement a __str__() method") + raise NotImplementedError( + "Subclasses of PulpException must implement a __str__() method" + ) def exception_to_dict(exc, traceback=None): @@ -44,6 +46,14 @@ def exception_to_dict(exc, traceback=None): return {"description": str(exc), "traceback": traceback} +class PulpExceptionNoTrace(PulpException): + """ + Base class for PulpExceptions where the traceback should not be logged or recorded. + """ + + pass + + class ResourceImmutableError(PulpException): """ Exceptions that are raised due to trying to update an immutable resource @@ -58,9 +68,9 @@ def __init__(self, model): self.model = model def __str__(self): - msg = _("Cannot update immutable resource {model_pk} of type {model_type}").format( - resource=str(self.model.pk), type=type(self.model).__name__ - ) + msg = _( + "Cannot update immutable resource {model_pk} of type {model_type}" + ).format(resource=str(self.model.pk), type=type(self.model).__name__) return msg @@ -93,4 +103,25 @@ def __init__(self): super().__init__("PLP0007") def __str__(self): - return _("You cannot delete a domain that still contains repositories with content.") + return _( + "You cannot delete a domain that still contains repositories with content." + ) + + +class DnsDomainNameException(PulpExceptionNoTrace): + """ + Exception to signal that dns could not resolve the domain name for specified url. + """ + + def __init__(self, url): + """ + :param url: the url that dns could not resolve + :type url: str + """ + super().__init__("PLP0008") + self.url = url + + def __str__(self): + return _( + "Domain name was not found for {}. Check if specified url is valid." + ).format(self.url) From 29fe731444e2f7134c1845d64485501ef77cda66 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 4 Nov 2025 10:59:25 +0100 Subject: [PATCH 02/14] Exception traceback removed from failed tasks --- CHANGES/+no-traceback-task.removal | 1 + pulpcore/app/models/task.py | 10 +++++++--- pulpcore/download/http.py | 17 +++++++++++------ pulpcore/exceptions/base.py | 20 ++++++++------------ pulpcore/tasking/tasks.py | 17 ++++++++++++++--- 5 files changed, 41 insertions(+), 24 deletions(-) create mode 100644 CHANGES/+no-traceback-task.removal diff --git a/CHANGES/+no-traceback-task.removal b/CHANGES/+no-traceback-task.removal new file mode 100644 index 00000000000..df8c4f78454 --- /dev/null +++ b/CHANGES/+no-traceback-task.removal @@ -0,0 +1 @@ +Failed tasks no longer store traceback. diff --git a/pulpcore/app/models/task.py b/pulpcore/app/models/task.py index 80d6ef7650f..02fca1124f1 100644 --- a/pulpcore/app/models/task.py +++ b/pulpcore/app/models/task.py @@ -245,7 +245,7 @@ def set_completed(self, result=None): ) self._cleanup_progress_reports(TASK_STATES.COMPLETED) - def set_failed(self, exc, tb): + def set_failed(self, exc, tb=None): """ Set this Task to the failed state and save it. @@ -257,8 +257,12 @@ def set_failed(self, exc, tb): tb (traceback): Traceback instance for the current exception. """ finished_at = timezone.now() - tb_str = "".join(traceback.format_tb(tb)) - error = exception_to_dict(exc, tb_str) + error = {} + if tb: + tb_str = "".join(traceback.format_tb(tb)) + error = exception_to_dict(exc, tb_str) + else: + error = exception_to_dict(exc) rows = Task.objects.filter( pk=self.pk, state=TASK_STATES.RUNNING, diff --git a/pulpcore/download/http.py b/pulpcore/download/http.py index 2d3f46a9374..d7167325327 100644 --- a/pulpcore/download/http.py +++ b/pulpcore/download/http.py @@ -9,6 +9,7 @@ DigestValidationError, SizeValidationError, TimeoutException, + DnsDomainNameException, ) @@ -236,6 +237,7 @@ async def run(self, extra_data=None): aiohttp.ClientPayloadError, aiohttp.ClientResponseError, aiohttp.ServerDisconnectedError, + DnsDomainNameException, TimeoutError, TimeoutException, DigestValidationError, @@ -289,12 +291,15 @@ async def _run(self, extra_data=None): """ if self.download_throttler: await self.download_throttler.acquire() - async with self.session.get( - self.url, proxy=self.proxy, proxy_auth=self.proxy_auth, auth=self.auth - ) as response: - self.raise_for_status(response) - to_return = await self._handle_response(response) - await response.release() + try: + async with self.session.get( + self.url, proxy=self.proxy, proxy_auth=self.proxy_auth, auth=self.auth + ) as response: + self.raise_for_status(response) + to_return = await self._handle_response(response) + await response.release() + except aiohttp.ClientConnectorDNSError: + raise DnsDomainNameException(self.url) if self._close_session_on_finalize: await self.session.close() return to_return diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index b0792048be9..673bc4fbe4b 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -26,9 +26,7 @@ def __str__(self): expected to implement it's own __str__() method. The return value is used by Pulp when recording the exception in the database. """ - raise NotImplementedError( - "Subclasses of PulpException must implement a __str__() method" - ) + raise NotImplementedError("Subclasses of PulpException must implement a __str__() method") def exception_to_dict(exc, traceback=None): @@ -68,9 +66,9 @@ def __init__(self, model): self.model = model def __str__(self): - msg = _( - "Cannot update immutable resource {model_pk} of type {model_type}" - ).format(resource=str(self.model.pk), type=type(self.model).__name__) + msg = _("Cannot update immutable resource {model_pk} of type {model_type}").format( + resource=str(self.model.pk), type=type(self.model).__name__ + ) return msg @@ -103,9 +101,7 @@ def __init__(self): super().__init__("PLP0007") def __str__(self): - return _( - "You cannot delete a domain that still contains repositories with content." - ) + return _("You cannot delete a domain that still contains repositories with content.") class DnsDomainNameException(PulpExceptionNoTrace): @@ -122,6 +118,6 @@ def __init__(self, url): self.url = url def __str__(self): - return _( - "Domain name was not found for {}. Check if specified url is valid." - ).format(self.url) + return _("Domain name was not found for {}. Check if specified url is valid.").format( + self.url + ) diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index d2f47eba7a6..7fa336d8a94 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -32,6 +32,8 @@ from pulpcore.middleware import x_task_diagnostics_var from pulpcore.tasking.kafka import send_task_notification +from pulpcore.exceptions import PulpExceptionNoTrace + _logger = logging.getLogger(__name__) @@ -75,9 +77,13 @@ def _execute_task(task): log_task_start(task, domain) task_function = get_task_function(task) result = task_function() + except PulpExceptionNoTrace: + exc_type, exc, _ = sys.exc_info() + task.set_failed(exc) + send_task_notification(task) except Exception: exc_type, exc, tb = sys.exc_info() - task.set_failed(exc, tb) + task.set_failed(exc) log_task_failed(task, exc_type, exc, tb, domain) send_task_notification(task) else: @@ -96,9 +102,13 @@ async def _aexecute_task(task): try: coroutine = get_task_function(task, ensure_coroutine=True) result = await coroutine + except PulpExceptionNoTrace: + exc_type, exc, _ = sys.exc_info() + await sync_to_async(task.set_failed)(exc) + send_task_notification(task) except Exception: exc_type, exc, tb = sys.exc_info() - await sync_to_async(task.set_failed)(exc, tb) + await sync_to_async(task.set_failed)(exc) log_task_failed(task, exc_type, exc, tb, domain) send_task_notification(task) else: @@ -145,7 +155,8 @@ def log_task_failed(task, exc_type, exc, tb, domain): domain=domain.name, ) ) - _logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb)))) + if tb: + _logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb)))) def get_task_function(task, ensure_coroutine=False): From bce2ec00d7129a604c14665691599702cc524374 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Thu, 6 Nov 2025 14:17:10 +0100 Subject: [PATCH 03/14] NoTraceException class removed. Exceptions now return generic eror message to user --- pulpcore/exceptions/__init__.py | 1 - pulpcore/exceptions/base.py | 10 +--------- pulpcore/tasking/tasks.py | 14 +++++++++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index cd8a8b09c7c..a6c259cddf3 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -1,6 +1,5 @@ from .base import ( PulpException, - PulpExceptionNoTrace, ResourceImmutableError, TimeoutException, exception_to_dict, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index 673bc4fbe4b..2ebeb499a3f 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -44,14 +44,6 @@ def exception_to_dict(exc, traceback=None): return {"description": str(exc), "traceback": traceback} -class PulpExceptionNoTrace(PulpException): - """ - Base class for PulpExceptions where the traceback should not be logged or recorded. - """ - - pass - - class ResourceImmutableError(PulpException): """ Exceptions that are raised due to trying to update an immutable resource @@ -104,7 +96,7 @@ def __str__(self): return _("You cannot delete a domain that still contains repositories with content.") -class DnsDomainNameException(PulpExceptionNoTrace): +class DnsDomainNameException(PulpException): """ Exception to signal that dns could not resolve the domain name for specified url. """ diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index 7fa336d8a94..0d61c064998 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -32,7 +32,7 @@ from pulpcore.middleware import x_task_diagnostics_var from pulpcore.tasking.kafka import send_task_notification -from pulpcore.exceptions import PulpExceptionNoTrace +from pulpcore.exceptions import PulpException _logger = logging.getLogger(__name__) @@ -77,14 +77,16 @@ def _execute_task(task): log_task_start(task, domain) task_function = get_task_function(task) result = task_function() - except PulpExceptionNoTrace: + except PulpException: exc_type, exc, _ = sys.exc_info() task.set_failed(exc) send_task_notification(task) except Exception: exc_type, exc, tb = sys.exc_info() - task.set_failed(exc) log_task_failed(task, exc_type, exc, tb, domain) + # Generic exception for user + safe_exc = Exception("An internal error occured.") + task.set_failed(safe_exc) send_task_notification(task) else: task.set_completed(result) @@ -102,14 +104,16 @@ async def _aexecute_task(task): try: coroutine = get_task_function(task, ensure_coroutine=True) result = await coroutine - except PulpExceptionNoTrace: + except PulpException: exc_type, exc, _ = sys.exc_info() await sync_to_async(task.set_failed)(exc) send_task_notification(task) except Exception: exc_type, exc, tb = sys.exc_info() - await sync_to_async(task.set_failed)(exc) log_task_failed(task, exc_type, exc, tb, domain) + # Generic exception for user + safe_exc = Exception("An internal error occured.") + await sync_to_async(task.set_failed)(safe_exc) send_task_notification(task) else: await sync_to_async(task.set_completed)(result) From c711a2e8391a04761a031829c550cd1214a9ae37 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau <94230089+aKlimau@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:25:52 +0100 Subject: [PATCH 04/14] Update change text message --- CHANGES/+no-traceback-task.removal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES/+no-traceback-task.removal b/CHANGES/+no-traceback-task.removal index df8c4f78454..40c4f9ee82d 100644 --- a/CHANGES/+no-traceback-task.removal +++ b/CHANGES/+no-traceback-task.removal @@ -1 +1 @@ -Failed tasks no longer store traceback. +Stopped leaking sensitive information of failures in the task API. From 184d9daf81022c1af94b93c210d57348a1e7f778 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Mon, 10 Nov 2025 10:14:43 +0100 Subject: [PATCH 05/14] Replace generic errors with specific PulpExceptions --- pulpcore/exceptions/__init__.py | 2 ++ pulpcore/exceptions/base.py | 42 +++++++++++++++++++++++++++++++++ pulpcore/tasking/tasks.py | 10 +++++--- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index a6c259cddf3..62ee39757db 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -5,6 +5,8 @@ exception_to_dict, DomainProtectedError, DnsDomainNameException, + ImmediateTaskTimeoutError, + NonAsyncImmediateTaskError, ) from .validation import ( DigestValidationError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index 2ebeb499a3f..647d330a2af 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -113,3 +113,45 @@ def __str__(self): return _("Domain name was not found for {}. Check if specified url is valid.").format( self.url ) + + +class ImmediateTaskTimeoutError(PulpException): + """ + Exception to signal that an immediate task timed out. + """ + + def __init__(self, task_pk, timeout_seconds): + """ + :param task_pk: The PK of the task that timed out. + :type task_pk: str + :param timeout_seconds: The timeout duration. + :type timeout_seconds: int or str + """ + super().__init__("PLP0009") + self.task_pk = task_pk + self.timeout_seconds = timeout_seconds + + def __str__(self): + return _("Immediate task {task_pk} timed out after {timeout_seconds} seconds.").format( + task_pk=self.task_pk, timeout_seconds=self.timeout_seconds + ) + + +class NonAsyncImmediateTaskError(PulpException): + """ + Exception raised when a task is marked as 'immediate' but is not + an async coroutine function. + """ + + def __init__(self, task_name): + """ + :param task_name: The name of the task that caused the error. + :type task_name: str + """ + super().__init__("PLP0010") + self.task_name = task_name + + def __str__(self): + return _("Immediate task '{task_name}' must be an async function.").format( + task_name=self.task_name + ) diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index 0d61c064998..47d2faf285f 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -29,10 +29,14 @@ TASK_WAKEUP_HANDLE, TASK_WAKEUP_UNBLOCK, ) +from pulpcore.exceptions.base import ( + PulpException, + ImmediateTaskTimeoutError, + NonAsyncImmediateTaskError, +) from pulpcore.middleware import x_task_diagnostics_var from pulpcore.tasking.kafka import send_task_notification -from pulpcore.exceptions import PulpException _logger = logging.getLogger(__name__) @@ -173,7 +177,7 @@ def get_task_function(task, ensure_coroutine=False): is_coroutine_fn = asyncio.iscoroutinefunction(func) if immediate and not is_coroutine_fn: - raise ValueError("Immediate tasks must be async functions.") + raise NonAsyncImmediateTaskError(task_name=task.name) if ensure_coroutine: if not is_coroutine_fn: @@ -196,7 +200,7 @@ async def task_wrapper(): # asyncio.wait_for + async_to_sync requires wrapping msg_template = "Immediate task %s timed out after %s seconds." error_msg = msg_template % (task.pk, IMMEDIATE_TIMEOUT) _logger.info(error_msg) - raise RuntimeError(error_msg) + raise ImmediateTaskTimeoutError(task_pk=task.pk, timeout_seconds=IMMEDIATE_TIMEOUT) return async_to_sync(task_wrapper) From 43966ea4adb1fe1b7024a8503565eb073c08fda0 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 11 Nov 2025 16:02:39 +0100 Subject: [PATCH 06/14] PulpExceptions now logged without traceback --- pulpcore/tasking/tasks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index 47d2faf285f..d0e1a4e3268 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -83,6 +83,7 @@ def _execute_task(task): result = task_function() except PulpException: exc_type, exc, _ = sys.exc_info() + log_task_failed(task, exc_type, exc, None, domain) # Leave no traceback in logs task.set_failed(exc) send_task_notification(task) except Exception: @@ -110,6 +111,7 @@ async def _aexecute_task(task): result = await coroutine except PulpException: exc_type, exc, _ = sys.exc_info() + log_task_failed(task, exc_type, exc, None, domain) # Leave no traceback in logs await sync_to_async(task.set_failed)(exc) send_task_notification(task) except Exception: From 0a5587f8219f355706a27ff92ea6d4030f4f9ed1 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Wed, 19 Nov 2025 14:43:19 +0100 Subject: [PATCH 07/14] Added PulpException for not supproted URL schema --- pulpcore/download/factory.py | 3 ++- pulpcore/exceptions/__init__.py | 1 + pulpcore/exceptions/base.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/pulpcore/download/factory.py b/pulpcore/download/factory.py index 343d45be3f6..756d960ef33 100644 --- a/pulpcore/download/factory.py +++ b/pulpcore/download/factory.py @@ -15,6 +15,7 @@ from pulpcore.app.apps import PulpAppConfig from .http import HttpDownloader from .file import FileDownloader +from pulpcore.exceptions import UrlSchemeNotSupportedError PROTOCOL_MAP = { @@ -177,7 +178,7 @@ def build(self, url, **kwargs): builder = self._handler_map[scheme] download_class = self._download_class_map[scheme] except KeyError: - raise ValueError(_("URL: {u} not supported.".format(u=url))) + raise UrlSchemeNotSupportedError(url) else: return builder(download_class, url, **kwargs) diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index 62ee39757db..c2868fc2d47 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -7,6 +7,7 @@ DnsDomainNameException, ImmediateTaskTimeoutError, NonAsyncImmediateTaskError, + UrlSchemeNotSupportedError, ) from .validation import ( DigestValidationError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index 647d330a2af..d163592c109 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -155,3 +155,21 @@ def __str__(self): return _("Immediate task '{task_name}' must be an async function.").format( task_name=self.task_name ) + + +class UrlSchemeNotSupportedError(PulpException): + """ + Exception raised when a URL scheme (e.g. 'ftp://') is provided that + Pulp does not have a registered handler for. + """ + + def __init__(self, url): + """ + :param url: The full URL that failed validation. + :type url: str + """ + super().__init__("PLP0011") + self.url = url + + def __str__(self): + return _("URL: {u} not supported.").format(u=self.url) From 01431deb312fb9e0ee29dc55b1364cf3e1bc7001 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 25 Nov 2025 13:00:07 +0100 Subject: [PATCH 08/14] NonAsyncImmediateTaskError message fixed --- pulpcore/exceptions/base.py | 11 ++--------- pulpcore/tasking/tasks.py | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index d163592c109..360995ec36e 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -143,18 +143,11 @@ class NonAsyncImmediateTaskError(PulpException): an async coroutine function. """ - def __init__(self, task_name): - """ - :param task_name: The name of the task that caused the error. - :type task_name: str - """ + def __init__(self): super().__init__("PLP0010") - self.task_name = task_name def __str__(self): - return _("Immediate task '{task_name}' must be an async function.").format( - task_name=self.task_name - ) + return _("Immediate tasks must be async functions.") class UrlSchemeNotSupportedError(PulpException): diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index d0e1a4e3268..3ff8a379157 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -179,7 +179,7 @@ def get_task_function(task, ensure_coroutine=False): is_coroutine_fn = asyncio.iscoroutinefunction(func) if immediate and not is_coroutine_fn: - raise NonAsyncImmediateTaskError(task_name=task.name) + raise NonAsyncImmediateTaskError() if ensure_coroutine: if not is_coroutine_fn: From 970fcd690c41f38e16258d46f9605f3ed807ee98 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 2 Dec 2025 13:09:02 +0100 Subject: [PATCH 09/14] Add exception for proxy authentication error --- pulpcore/download/http.py | 3 +++ pulpcore/exceptions/__init__.py | 1 + pulpcore/exceptions/base.py | 11 +++++++++++ .../tests/functional/api/using_plugin/test_proxy.py | 2 +- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pulpcore/download/http.py b/pulpcore/download/http.py index d7167325327..7b301f37c2e 100644 --- a/pulpcore/download/http.py +++ b/pulpcore/download/http.py @@ -10,6 +10,7 @@ SizeValidationError, TimeoutException, DnsDomainNameException, + ProxyAuthenticationRequiredError, ) @@ -300,6 +301,8 @@ async def _run(self, extra_data=None): await response.release() except aiohttp.ClientConnectorDNSError: raise DnsDomainNameException(self.url) + except aiohttp.ClientHttpProxyError: + raise ProxyAuthenticationRequiredError(self.proxy) if self._close_session_on_finalize: await self.session.close() return to_return diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index c2868fc2d47..7ea59f81f38 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -8,6 +8,7 @@ ImmediateTaskTimeoutError, NonAsyncImmediateTaskError, UrlSchemeNotSupportedError, + ProxyAuthenticationRequiredError, ) from .validation import ( DigestValidationError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index 360995ec36e..248ed6ea5de 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -166,3 +166,14 @@ def __init__(self, url): def __str__(self): return _("URL: {u} not supported.").format(u=self.url) + + +class ProxyAuthenticationRequiredError(PulpException): + def __init__(self, proxy_url): + super().__init__("PLP0012") + self.proxy_url = proxy_url + + def __str__(self): + return _( + "Proxy authentication failed for {proxy_url}. Please check your proxy credentials." + ).format(proxy_url=self.proxy_url) diff --git a/pulpcore/tests/functional/api/using_plugin/test_proxy.py b/pulpcore/tests/functional/api/using_plugin/test_proxy.py index 29f132ba92a..3c19007b6e0 100644 --- a/pulpcore/tests/functional/api/using_plugin/test_proxy.py +++ b/pulpcore/tests/functional/api/using_plugin/test_proxy.py @@ -101,7 +101,7 @@ def test_sync_https_through_http_proxy_with_auth_but_auth_not_configured( try: _run_basic_sync_and_assert(file_bindings, monitor_task, remote_on_demand, file_repo) except PulpTaskError as exc: - assert "407, message='Proxy Authentication Required'" in exc.task.error["description"] + assert "Proxy authentication failed for" in exc.task.error["description"] @pytest.mark.parallel From 643adc958226f21e77d4c460ab181e484585ebf5 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 2 Dec 2025 13:44:08 +0100 Subject: [PATCH 10/14] Add exception for repository version delete --- pulpcore/app/tasks/repository.py | 9 ++------- pulpcore/exceptions/__init__.py | 1 + pulpcore/exceptions/base.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pulpcore/app/tasks/repository.py b/pulpcore/app/tasks/repository.py index b20bf3c7dbd..86976e19353 100644 --- a/pulpcore/app/tasks/repository.py +++ b/pulpcore/app/tasks/repository.py @@ -7,7 +7,7 @@ from asgiref.sync import sync_to_async from django.db import transaction -from rest_framework.serializers import ValidationError +from exceptions.base import RepositoryVersionDeleteError from pulpcore.app import models from pulpcore.app.models import ProgressReport @@ -44,12 +44,7 @@ def delete_version(pk): return if version.repository.versions.complete().count() <= 1: - raise ValidationError( - _( - "Cannot delete repository version. Repositories must have at least one " - "repository version." - ) - ) + raise RepositoryVersionDeleteError log.info( "Deleting and squashing version {num} of repository '{repo}'".format( diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index 7ea59f81f38..7a5e08da644 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -9,6 +9,7 @@ NonAsyncImmediateTaskError, UrlSchemeNotSupportedError, ProxyAuthenticationRequiredError, + RepositoryVersionDeleteError, ) from .validation import ( DigestValidationError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index 248ed6ea5de..db6864b3295 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -169,7 +169,16 @@ def __str__(self): class ProxyAuthenticationRequiredError(PulpException): + """ + Exception to signal that the proxy server requires authentication + but it was not provided or is invalid (HTTP 407). + """ + def __init__(self, proxy_url): + """ + :param proxy_url: The URL of the proxy server. + :type proxy_url: str + """ super().__init__("PLP0012") self.proxy_url = proxy_url @@ -177,3 +186,17 @@ def __str__(self): return _( "Proxy authentication failed for {proxy_url}. Please check your proxy credentials." ).format(proxy_url=self.proxy_url) + + +class RepositoryVersionDeleteError(PulpException): + """ + Raised when attempting to delete a repository version that cannot be deleted + """ + + def __init__(self): + super().__init__("PLP0013") + + def __str__(self): + return _( + "Cannot delete repository version. Repositories must have at least one repository version." + ) From 0d3fa509852aa3b71ea732ce646c005ad6929275 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Tue, 2 Dec 2025 14:01:56 +0100 Subject: [PATCH 11/14] Fix linting, removed unused exceptions --- pulpcore/app/tasks/repository.py | 2 +- pulpcore/download/factory.py | 1 - pulpcore/exceptions/__init__.py | 2 -- pulpcore/exceptions/base.py | 44 ++++---------------------------- pulpcore/tasking/tasks.py | 2 -- 5 files changed, 6 insertions(+), 45 deletions(-) diff --git a/pulpcore/app/tasks/repository.py b/pulpcore/app/tasks/repository.py index 86976e19353..efa701b94bb 100644 --- a/pulpcore/app/tasks/repository.py +++ b/pulpcore/app/tasks/repository.py @@ -7,7 +7,7 @@ from asgiref.sync import sync_to_async from django.db import transaction -from exceptions.base import RepositoryVersionDeleteError +from pulpcore.exceptions import RepositoryVersionDeleteError from pulpcore.app import models from pulpcore.app.models import ProgressReport diff --git a/pulpcore/download/factory.py b/pulpcore/download/factory.py index 756d960ef33..504dae34452 100644 --- a/pulpcore/download/factory.py +++ b/pulpcore/download/factory.py @@ -2,7 +2,6 @@ import asyncio import atexit import copy -from gettext import gettext as _ from multidict import MultiDict import platform import ssl diff --git a/pulpcore/exceptions/__init__.py b/pulpcore/exceptions/__init__.py index 7a5e08da644..9563733af9c 100644 --- a/pulpcore/exceptions/__init__.py +++ b/pulpcore/exceptions/__init__.py @@ -5,8 +5,6 @@ exception_to_dict, DomainProtectedError, DnsDomainNameException, - ImmediateTaskTimeoutError, - NonAsyncImmediateTaskError, UrlSchemeNotSupportedError, ProxyAuthenticationRequiredError, RepositoryVersionDeleteError, diff --git a/pulpcore/exceptions/base.py b/pulpcore/exceptions/base.py index db6864b3295..49979b7d0ef 100644 --- a/pulpcore/exceptions/base.py +++ b/pulpcore/exceptions/base.py @@ -115,41 +115,6 @@ def __str__(self): ) -class ImmediateTaskTimeoutError(PulpException): - """ - Exception to signal that an immediate task timed out. - """ - - def __init__(self, task_pk, timeout_seconds): - """ - :param task_pk: The PK of the task that timed out. - :type task_pk: str - :param timeout_seconds: The timeout duration. - :type timeout_seconds: int or str - """ - super().__init__("PLP0009") - self.task_pk = task_pk - self.timeout_seconds = timeout_seconds - - def __str__(self): - return _("Immediate task {task_pk} timed out after {timeout_seconds} seconds.").format( - task_pk=self.task_pk, timeout_seconds=self.timeout_seconds - ) - - -class NonAsyncImmediateTaskError(PulpException): - """ - Exception raised when a task is marked as 'immediate' but is not - an async coroutine function. - """ - - def __init__(self): - super().__init__("PLP0010") - - def __str__(self): - return _("Immediate tasks must be async functions.") - - class UrlSchemeNotSupportedError(PulpException): """ Exception raised when a URL scheme (e.g. 'ftp://') is provided that @@ -161,7 +126,7 @@ def __init__(self, url): :param url: The full URL that failed validation. :type url: str """ - super().__init__("PLP0011") + super().__init__("PLP0009") self.url = url def __str__(self): @@ -179,7 +144,7 @@ def __init__(self, proxy_url): :param proxy_url: The URL of the proxy server. :type proxy_url: str """ - super().__init__("PLP0012") + super().__init__("PLP0010") self.proxy_url = proxy_url def __str__(self): @@ -194,9 +159,10 @@ class RepositoryVersionDeleteError(PulpException): """ def __init__(self): - super().__init__("PLP0013") + super().__init__("PLP0011") def __str__(self): return _( - "Cannot delete repository version. Repositories must have at least one repository version." + "Cannot delete repository version. Repositories must have at least one " + "repository version." ) diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index 7aeb81c5c44..2eb81f47470 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -32,8 +32,6 @@ ) from pulpcore.exceptions.base import ( PulpException, - ImmediateTaskTimeoutError, - NonAsyncImmediateTaskError, ) from pulpcore.middleware import x_task_diagnostics_var from pulpcore.tasking.kafka import send_task_notification From ba22c6125c2e0c9569a3cb17a0dffcc4df1e30ae Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Wed, 10 Dec 2025 12:47:28 +0100 Subject: [PATCH 12/14] Add pulp-glue exceptions handling --- pulpcore/tasking/tasks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pulpcore/tasking/tasks.py b/pulpcore/tasking/tasks.py index 2eb81f47470..f1a72880913 100644 --- a/pulpcore/tasking/tasks.py +++ b/pulpcore/tasking/tasks.py @@ -30,9 +30,9 @@ TASK_WAKEUP_HANDLE, TASK_WAKEUP_UNBLOCK, ) -from pulpcore.exceptions.base import ( - PulpException, -) +from pulpcore.exceptions.base import PulpException +from pulp_glue.common.exceptions import PulpException as PulpGlueException + from pulpcore.middleware import x_task_diagnostics_var from pulpcore.tasking.kafka import send_task_notification @@ -74,7 +74,7 @@ def _execute_task(task): log_task_start(task, domain) task_function = get_task_function(task) result = task_function() - except PulpException: + except (PulpException, PulpGlueException): exc_type, exc, _ = sys.exc_info() log_task_failed(task, exc_type, exc, None, domain) # Leave no traceback in logs task.set_failed(exc) @@ -106,7 +106,7 @@ async def _aexecute_task(task): try: task_coroutine_fn = await aget_task_function(task) result = await task_coroutine_fn() - except PulpException: + except (PulpException, PulpGlueException): exc_type, exc, _ = sys.exc_info() log_task_failed(task, exc_type, exc, None, domain) # Leave no traceback in logs await sync_to_async(task.set_failed)(exc) From cb4fe926f3206f17debba494537036c495e870a5 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Wed, 10 Dec 2025 15:51:07 +0100 Subject: [PATCH 13/14] Update tasking tests to expect generic internal error messages --- pulpcore/tests/functional/api/test_tasking.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pulpcore/tests/functional/api/test_tasking.py b/pulpcore/tests/functional/api/test_tasking.py index 183ec2f00b6..86c597df1ee 100644 --- a/pulpcore/tests/functional/api/test_tasking.py +++ b/pulpcore/tests/functional/api/test_tasking.py @@ -468,7 +468,9 @@ def test_executes_on_api_worker_when_no_async( "pulpcore.app.tasks.test.sleep", args=(LT_TIMEOUT,), immediate=True ) monitor_task(task_href) - assert "Immediate tasks must be async functions" in ctx.value.task.error["description"] + # Assert masked internal error + # Underlying cause is ValueError("Immediate tasks must be async functions") + assert "An internal error occured." in ctx.value.task.error["description"] @pytest.mark.parallel def test_timeouts_on_api_worker(self, pulpcore_bindings, dispatch_task): @@ -484,7 +486,8 @@ def test_timeouts_on_api_worker(self, pulpcore_bindings, dispatch_task): ) task = pulpcore_bindings.TasksApi.read(task_href) assert task.state == "failed" - assert "timed out after" in task.error["description"] + # Assert masked internal error; underlying cause is asyncio.TimeoutError + assert "An internal error occured." in task.error["description"] @pytest.fixture @@ -576,4 +579,5 @@ def test_times_out_on_task_worker( exclusive_resources=[COMMON_RESOURCE], ) monitor_task(task_href) - assert "timed out after" in ctx.value.task.error["description"] + # Assert masked internal error; underlying cause is asyncio.TimeoutError + assert "An internal error occured." in ctx.value.task.error["description"] From f10b3baac93fc57584fa63bf324c7dbd0d21d247 Mon Sep 17 00:00:00 2001 From: Aliaksei Klimau Date: Thu, 11 Dec 2025 12:59:51 +0100 Subject: [PATCH 14/14] Bump pulp-glue dependency to >=0.30.0 and add pycares constraint - Raise pulp-glue lower bound due to new imports requiring features from 0.30.0 - Add explicit pycares dependency to fix aiodns compatibility issue --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b775a4975c0..be5cfa671ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,9 +52,10 @@ dependencies = [ "opentelemetry-sdk>=1.27.0,<1.39", "opentelemetry-exporter-otlp-proto-http>=1.27.0,<1.39", "protobuf>=4.21.1,<7.0", - "pulp-glue>=0.28.0,<0.37", + "pulp-glue>=0.30.0,<0.37", "pygtrie>=2.5,<=2.5.0", "psycopg[binary]>=3.1.8,<3.3", # SemVer, not explicitely stated, but mentioned on multiple changes. + "pycares>=4.0.0,<5.0", # Explicit dependency to ensure compatibility with aiodns. "pyparsing>=3.1.0,<3.3", # Looks like only bugfixes in z-Stream. "python-gnupg>=0.5.0,<0.6", # Looks like only bugfixes in z-Stream [changelog only in git] "PyYAML>=5.1.1,<6.1", # Looks like only bugfixes in z-Stream.