pulpcore 3.83.2__py3-none-any.whl → 3.85.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pulp_certguard/app/__init__.py +1 -1
- pulp_certguard/app/models.py +7 -26
- pulp_certguard/app/serializers.py +0 -2
- pulp_certguard/rhsm/__init__.py +4 -0
- pulp_certguard/rhsm/rhsm_check_path.py +198 -0
- pulp_certguard/tests/unit/certdata.py +249 -0
- pulp_certguard/tests/unit/test_rhsm_check_path.py +213 -0
- pulp_file/app/__init__.py +1 -1
- pulp_file/app/migrations/0001_initial_squashed_0016_add_domain.py +0 -20
- pulp_file/app/migrations/0017_alter_filealternatecontentsource_alternatecontentsource_ptr_and_more.py +1 -1
- pulpcore/app/apps.py +2 -12
- pulpcore/app/entrypoint.py +22 -22
- pulpcore/app/migrations/0001_squashed_0090_char_to_text_field.py +0 -95
- pulpcore/app/migrations/0091_systemid.py +1 -1
- pulpcore/app/migrations/0134_task_insert_trigger.py +81 -0
- pulpcore/app/migrations/0135_task_pulp_task_resources_index.py +25 -0
- pulp_file/app/migrations/0006_delete_filefilesystemexporter.py → pulpcore/app/migrations/0136_delete_basedistribution.py +3 -3
- pulpcore/app/migrations/0137_appstatus.py +33 -0
- pulpcore/app/migrations/0138_vulnerabilityreport.py +33 -0
- pulpcore/app/models/__init__.py +4 -1
- pulpcore/app/models/publication.py +0 -41
- pulpcore/app/models/status.py +145 -0
- pulpcore/app/models/task.py +8 -0
- pulpcore/app/models/vulnerability_report.py +34 -0
- pulpcore/app/serializers/__init__.py +1 -0
- pulpcore/app/serializers/content.py +13 -1
- pulpcore/app/serializers/repository.py +8 -1
- pulpcore/app/serializers/vulnerability_report.py +27 -0
- pulpcore/app/settings.py +13 -38
- pulpcore/app/tasks/__init__.py +2 -0
- pulpcore/app/tasks/purge.py +8 -5
- pulpcore/app/tasks/vulnerability_report.py +159 -0
- pulpcore/app/viewsets/__init__.py +1 -0
- pulpcore/app/viewsets/vulnerability_report.py +20 -0
- pulpcore/constants.py +8 -0
- pulpcore/content/__init__.py +23 -22
- pulpcore/content/handler.py +5 -2
- pulpcore/migrations.py +38 -11
- pulpcore/openapi/__init__.py +8 -0
- pulpcore/plugin/models/__init__.py +2 -0
- pulpcore/plugin/serializers/__init__.py +2 -0
- pulpcore/plugin/tasking.py +2 -0
- pulpcore/plugin/viewsets/__init__.py +2 -0
- pulpcore/pytest_plugin.py +21 -21
- pulpcore/tasking/entrypoint.py +12 -2
- pulpcore/tasking/tasks.py +5 -30
- pulpcore/tasking/worker.py +115 -74
- pulpcore/tests/functional/api/test_auth.py +18 -3
- pulpcore/tests/functional/api/test_login.py +62 -32
- pulpcore/tests/functional/api/test_openapi_schema.py +32 -15
- pulpcore/tests/functional/api/using_plugin/test_checkpoint.py +23 -1
- pulpcore/tests/functional/api/using_plugin/test_proxy.py +1 -1
- pulpcore/tests/unit/content/test_heartbeat.py +11 -8
- pulpcore/tests/unit/test_vulnerability_report.py +74 -0
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/METADATA +13 -18
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/RECORD +60 -156
- pulp_certguard/app/utils.py +0 -28
- pulp_certguard/tests/unit/test_models.py +0 -9
- pulp_file/app/migrations/0001_initial.py +0 -59
- pulp_file/app/migrations/0002_file_related_names.py +0 -55
- pulp_file/app/migrations/0003_auto_20191014_1721.py +0 -18
- pulp_file/app/migrations/0004_filefilesystemexporter.py +0 -21
- pulp_file/app/migrations/0005_filerepository.py +0 -24
- pulp_file/app/migrations/0007_filefilesystemexporter.py +0 -25
- pulp_file/app/migrations/0008_add_manifest_field.py +0 -19
- pulp_file/app/migrations/0009_move_data_to_new_master_distribution_model.py +0 -77
- pulp_file/app/migrations/0010_auto_publish.py +0 -23
- pulp_file/app/migrations/0011_fix_auto_publish.py +0 -36
- pulp_file/app/migrations/0012_delete_filefilesystemexporter.py +0 -28
- pulp_file/app/migrations/0013_file_acs.py +0 -24
- pulp_file/app/migrations/0014_new_rbac_permissions.py +0 -33
- pulp_file/app/migrations/0015_allow_null_manifest.py +0 -23
- pulp_file/app/migrations/0016_add_domain.py +0 -25
- pulpcore/app/migrations/0001_initial.py +0 -451
- pulpcore/app/migrations/0002_increase_artifact_size_field.py +0 -18
- pulpcore/app/migrations/0003_remove_upload_completed.py +0 -17
- pulpcore/app/migrations/0004_add_duplicated_reserved_resources.py +0 -45
- pulpcore/app/migrations/0005_progressreport_code.py +0 -19
- pulpcore/app/migrations/0006_repository_plugin_managed.py +0 -18
- pulpcore/app/migrations/0007_delete_progress_proxies.py +0 -19
- pulpcore/app/migrations/0008_published_metadata_as_content.py +0 -44
- pulpcore/app/migrations/0009_remove_task_non_fatal_errors.py +0 -17
- pulpcore/app/migrations/0010_pulp_fields.py +0 -570
- pulpcore/app/migrations/0011_relative_path.py +0 -28
- pulpcore/app/migrations/0012_auto_20191104_2000.py +0 -31
- pulpcore/app/migrations/0013_repository_pulp_type.py +0 -18
- pulpcore/app/migrations/0014_remove_repository_plugin_managed.py +0 -17
- pulpcore/app/migrations/0015_auto_20191112_1426.py +0 -33
- pulpcore/app/migrations/0016_charfield_to_textfield.py +0 -68
- pulpcore/app/migrations/0017_remove_task_parent.py +0 -17
- pulpcore/app/migrations/0018_auto_20191127_2350.py +0 -20
- pulpcore/app/migrations/0019_add_signing_service_model.py +0 -27
- pulpcore/app/migrations/0020_change_publishedartifact_constraints.py +0 -17
- pulpcore/app/migrations/0021_add_signing_service_foreign_key.py +0 -24
- pulpcore/app/migrations/0022_rename_last_version.py +0 -27
- pulpcore/app/migrations/0023_change_exporter_models.py +0 -82
- pulpcore/app/migrations/0024_use_local_storage_for_uploads.py +0 -19
- pulpcore/app/migrations/0025_task_parent_task.py +0 -19
- pulpcore/app/migrations/0026_task_group.py +0 -32
- pulpcore/app/migrations/0027_export_backend.py +0 -31
- pulpcore/app/migrations/0028_import_importer_pulpimporter_pulpimporterrepository.py +0 -85
- pulpcore/app/migrations/0029_export_delete.py +0 -19
- pulpcore/app/migrations/0030_taskgroup_all_tasks_dispatched.py +0 -24
- pulpcore/app/migrations/0031_import_export_validate_params.py +0 -19
- pulpcore/app/migrations/0032_export_to_chunks.py +0 -27
- pulpcore/app/migrations/0033_increase_remote_artifact_size_field.py +0 -18
- pulpcore/app/migrations/0034_groupprogressreport.py +0 -32
- pulpcore/app/migrations/0035_content_upstream_id.py +0 -18
- pulpcore/app/migrations/0036_unprotect_last_export.py +0 -19
- pulpcore/app/migrations/0037_pulptemporaryfile.py +0 -28
- pulpcore/app/migrations/0038_repository_remote.py +0 -19
- pulpcore/app/migrations/0039_change_download_concurrency.py +0 -25
- pulpcore/app/migrations/0040_set_admin_is_staff.py +0 -28
- pulpcore/app/migrations/0041_accesspolicy.py +0 -29
- pulpcore/app/migrations/0042_rbac_for_tasks.py +0 -56
- pulpcore/app/migrations/0043_toc_attribute.py +0 -19
- pulpcore/app/migrations/0044_temp_file_artifact_field.py +0 -20
- pulpcore/app/migrations/0045_accesspolicy_permissions_allow_null.py +0 -19
- pulpcore/app/migrations/0046_task__resource_job_id.py +0 -35
- pulpcore/app/migrations/0047_improve_orphan_cleanup.py +0 -59
- pulpcore/app/migrations/0048_fips_checksums.py +0 -38
- pulpcore/app/migrations/0049_add_file_field_to_uploadchunk.py +0 -24
- pulpcore/app/migrations/0050_namespace_access_policies.py +0 -28
- pulpcore/app/migrations/0051_timeoutfields.py +0 -34
- pulpcore/app/migrations/0052_tasking_logging_cid.py +0 -18
- pulpcore/app/migrations/0053_remote_headers.py +0 -19
- pulpcore/app/migrations/0054_add_public_key.py +0 -104
- pulpcore/app/migrations/0055_label.py +0 -31
- pulpcore/app/migrations/0056_remote_rate_limit.py +0 -18
- pulpcore/app/migrations/0057_add_label_indexes.py +0 -23
- pulpcore/app/migrations/0058_accesspolicy_customized.py +0 -18
- pulpcore/app/migrations/0059_proxy_creds.py +0 -23
- pulpcore/app/migrations/0060_data_migration_proxy_creds.py +0 -45
- pulpcore/app/migrations/0061_call_handle_artifact_checksums_command.py +0 -87
- pulpcore/app/migrations/0062_add_new_distribution_mastermodel.py +0 -36
- pulpcore/app/migrations/0063_repository_retained_versions.py +0 -18
- pulpcore/app/migrations/0064_add_new_style_task_columns.py +0 -109
- pulpcore/app/migrations/0064_repository_user_hidden.py +0 -18
- pulpcore/app/migrations/0065_merge_20210615_1211.py +0 -14
- pulpcore/app/migrations/0066_download_concurrency_and_retry_changes.py +0 -24
- pulpcore/app/migrations/0067_add_protect_to_task_reservation.py +0 -19
- pulpcore/app/migrations/0068_add_timestamp_of_interest.py +0 -23
- pulpcore/app/migrations/0069_update_json_fields.py +0 -63
- pulpcore/app/migrations/0070_rename_retained_versions.py +0 -18
- pulpcore/app/migrations/0071_filesystemexport_filesystemexporter.py +0 -35
- pulpcore/app/migrations/0072_add_method_to_filesystem_exporter.py +0 -18
- pulpcore/app/migrations/0073_encrypt_remote_fields.py +0 -139
- pulpcore/app/migrations/0074_acs.py +0 -47
- pulpcore/app/migrations/0075_rbaccontentguard.py +0 -25
- pulpcore/app/migrations/0076_remove_reserved_resource.py +0 -39
- pulpcore/app/migrations/0077_move_remote_url_credentials.py +0 -41
- pulpcore/app/migrations/0078_grouprole_role_userrole.py +0 -70
- pulpcore/app/migrations/0079_rename_permissions_assignment_accesspolicy_creation_hooks.py +0 -18
- pulpcore/app/migrations/0080_proxy_group_model.py +0 -37
- pulpcore/app/migrations/0081_reapplabel_group_permissions.py +0 -59
- pulpcore/app/migrations/0082_add_manage_roles_permissions.py +0 -17
- pulpcore/app/migrations/0083_alter_group_options.py +0 -17
- pulpcore/app/migrations/0084_alter_rbaccontentguard_options.py +0 -17
- pulpcore/app/migrations/0085_contentredirectcontentguard.py +0 -26
- pulpcore/app/migrations/0086_task_json_fields.py +0 -77
- pulpcore/app/migrations/0087_taskschedule.py +0 -34
- pulpcore/app/migrations/0088_accesspolicy_queryset_scoping.py +0 -18
- pulpcore/app/migrations/0089_alter_contentredirectcontentguard_options.py +0 -17
- pulpcore/app/migrations/0090_char_to_text_field.py +0 -79
- pulpcore/tests/unit/migration/test_0077_move_remote_url_credentials.py +0 -35
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/WHEEL +0 -0
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/entry_points.txt +0 -0
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/licenses/LICENSE +0 -0
- {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/top_level.txt +0 -0
pulpcore/tasking/worker.py
CHANGED
|
@@ -13,8 +13,9 @@ from tempfile import TemporaryDirectory
|
|
|
13
13
|
from packaging.version import parse as parse_version
|
|
14
14
|
|
|
15
15
|
from django.conf import settings
|
|
16
|
-
from django.db import connection
|
|
16
|
+
from django.db import connection, DatabaseError, IntegrityError
|
|
17
17
|
from django.db.models import Case, Count, F, Max, Value, When
|
|
18
|
+
from django.db.models.functions import Random
|
|
18
19
|
from django.utils import timezone
|
|
19
20
|
|
|
20
21
|
from pulpcore.constants import (
|
|
@@ -23,11 +24,13 @@ from pulpcore.constants import (
|
|
|
23
24
|
TASK_SCHEDULING_LOCK,
|
|
24
25
|
TASK_UNBLOCKING_LOCK,
|
|
25
26
|
TASK_METRICS_HEARTBEAT_LOCK,
|
|
27
|
+
TASK_WAKEUP_UNBLOCK,
|
|
28
|
+
TASK_WAKEUP_HANDLE,
|
|
26
29
|
)
|
|
27
30
|
from pulpcore.metrics import init_otel_meter
|
|
28
31
|
from pulpcore.app.apps import pulp_plugin_configs
|
|
29
|
-
from pulpcore.app.models import Worker, Task, ApiAppStatus, ContentAppStatus
|
|
30
|
-
from pulpcore.app.util import PGAdvisoryLock
|
|
32
|
+
from pulpcore.app.models import Worker, Task, AppStatus, ApiAppStatus, ContentAppStatus
|
|
33
|
+
from pulpcore.app.util import PGAdvisoryLock
|
|
31
34
|
from pulpcore.exceptions import AdvisoryLockError
|
|
32
35
|
|
|
33
36
|
from pulpcore.tasking.storage import WorkerDirectory
|
|
@@ -56,19 +59,28 @@ THRESHOLD_UNBLOCKED_WAITING_TIME = 5
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
class PulpcoreWorker:
|
|
59
|
-
def __init__(self):
|
|
62
|
+
def __init__(self, auxiliary=False):
|
|
60
63
|
# Notification states from several signal handlers
|
|
61
64
|
self.shutdown_requested = False
|
|
62
|
-
self.
|
|
65
|
+
self.wakeup_unblock = False
|
|
66
|
+
self.wakeup_handle = False
|
|
63
67
|
self.cancel_task = False
|
|
64
68
|
|
|
69
|
+
self.auxiliary = auxiliary
|
|
65
70
|
self.task = None
|
|
66
71
|
self.name = f"{os.getpid()}@{socket.getfqdn()}"
|
|
67
72
|
self.heartbeat_period = timedelta(seconds=settings.WORKER_TTL / 3)
|
|
68
73
|
self.last_metric_heartbeat = timezone.now()
|
|
69
74
|
self.versions = {app.label: app.version for app in pulp_plugin_configs()}
|
|
70
75
|
self.cursor = connection.cursor()
|
|
71
|
-
|
|
76
|
+
try:
|
|
77
|
+
self.app_status = AppStatus.objects.create(
|
|
78
|
+
name=self.name, app_type="worker", versions=self.versions
|
|
79
|
+
)
|
|
80
|
+
self.worker = self.app_status._old_status
|
|
81
|
+
except IntegrityError:
|
|
82
|
+
_logger.error(f"A worker with name {self.name} already exists in the database.")
|
|
83
|
+
exit(1)
|
|
72
84
|
# This defaults to immediate task cancellation.
|
|
73
85
|
# It will be set into the future on moderately graceful worker shutdown,
|
|
74
86
|
# and set to None for fully graceful shutdown.
|
|
@@ -124,7 +136,17 @@ class PulpcoreWorker:
|
|
|
124
136
|
|
|
125
137
|
def _pg_notify_handler(self, notification):
|
|
126
138
|
if notification.channel == "pulp_worker_wakeup":
|
|
127
|
-
|
|
139
|
+
if notification.payload == TASK_WAKEUP_UNBLOCK:
|
|
140
|
+
# Auxiliary workers don't do this.
|
|
141
|
+
self.wakeup_unblock = not self.auxiliary
|
|
142
|
+
elif notification.payload == TASK_WAKEUP_HANDLE:
|
|
143
|
+
self.wakeup_handle = True
|
|
144
|
+
else:
|
|
145
|
+
_logger.warn("Unknown wakeup call recieved. Reason: '%s'", notification.payload)
|
|
146
|
+
# We cannot be sure so assume everything happened.
|
|
147
|
+
self.wakeup_unblock = not self.auxiliary
|
|
148
|
+
self.wakeup_handle = True
|
|
149
|
+
|
|
128
150
|
elif notification.channel == "pulp_worker_metrics_heartbeat":
|
|
129
151
|
self.last_metric_heartbeat = datetime.fromisoformat(notification.payload)
|
|
130
152
|
elif self.task and notification.channel == "pulp_worker_cancel":
|
|
@@ -133,64 +155,61 @@ class PulpcoreWorker:
|
|
|
133
155
|
|
|
134
156
|
def handle_worker_heartbeat(self):
|
|
135
157
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Existing Worker objects are searched for one to update. If an existing one is found, it is
|
|
139
|
-
updated. Otherwise a new Worker entry is created. Logging at the info level is also done.
|
|
158
|
+
Update worker heartbeat records.
|
|
140
159
|
|
|
160
|
+
If the update fails (the record was deleted, the database is unreachable, ...) the worker
|
|
161
|
+
is shut down.
|
|
141
162
|
"""
|
|
142
|
-
worker, created = Worker.objects.get_or_create(
|
|
143
|
-
name=self.name, defaults={"versions": self.versions}
|
|
144
|
-
)
|
|
145
|
-
if not created and worker.versions != self.versions:
|
|
146
|
-
worker.versions = self.versions
|
|
147
|
-
worker.save(update_fields=["versions"])
|
|
148
|
-
|
|
149
|
-
if created:
|
|
150
|
-
_logger.info(_("New worker '{name}' discovered").format(name=self.name))
|
|
151
|
-
elif worker.online is False:
|
|
152
|
-
_logger.info(_("Worker '{name}' is back online.").format(name=self.name))
|
|
153
|
-
|
|
154
|
-
worker.save_heartbeat()
|
|
155
163
|
|
|
156
164
|
msg = "Worker heartbeat from '{name}' at time {timestamp}".format(
|
|
157
|
-
timestamp=
|
|
165
|
+
timestamp=self.app_status.last_heartbeat, name=self.name
|
|
158
166
|
)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
try:
|
|
168
|
+
self.app_status.save_heartbeat()
|
|
169
|
+
_logger.debug(msg)
|
|
170
|
+
except (IntegrityError, DatabaseError):
|
|
171
|
+
# WARNING: Do not attempt to recycle the connection here.
|
|
172
|
+
# The advisory locks are bound to the connection and we must not loose them.
|
|
173
|
+
_logger.error(f"Updating the heartbeat of worker {self.name} failed.")
|
|
174
|
+
# TODO if shutdown_requested, we may need to be more aggressive.
|
|
175
|
+
self.shutdown_requested = True
|
|
176
|
+
self.cancel_task = True
|
|
162
177
|
|
|
163
178
|
def shutdown(self):
|
|
164
|
-
self.
|
|
179
|
+
self.app_status.delete()
|
|
165
180
|
_logger.info(_("Worker %s was shut down."), self.name)
|
|
166
181
|
|
|
167
182
|
def worker_cleanup(self):
|
|
183
|
+
qs = AppStatus.objects.older_than(age=timedelta(days=7))
|
|
184
|
+
for app_worker in qs:
|
|
185
|
+
_logger.info(_("Clean missing %s worker %s."), app_worker.app_type, app_worker.name)
|
|
186
|
+
qs.delete()
|
|
168
187
|
for cls, cls_name in (
|
|
169
188
|
(Worker, "pulp"),
|
|
170
189
|
(ApiAppStatus, "api"),
|
|
171
190
|
(ContentAppStatus, "content"),
|
|
172
191
|
):
|
|
173
192
|
qs = cls.objects.missing(age=timedelta(days=7))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
qs.delete()
|
|
193
|
+
for app_worker in qs:
|
|
194
|
+
_logger.info(_("Clean missing %s worker %s."), cls_name, app_worker.name)
|
|
195
|
+
qs.delete()
|
|
178
196
|
|
|
179
197
|
def beat(self):
|
|
180
|
-
if self.
|
|
181
|
-
self.
|
|
182
|
-
self.
|
|
183
|
-
|
|
184
|
-
self.worker_cleanup_countdown
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
if self.app_status.last_heartbeat < timezone.now() - self.heartbeat_period:
|
|
199
|
+
self.handle_worker_heartbeat()
|
|
200
|
+
if not self.auxiliary:
|
|
201
|
+
self.worker_cleanup_countdown -= 1
|
|
202
|
+
if self.worker_cleanup_countdown <= 0:
|
|
203
|
+
self.worker_cleanup_countdown = WORKER_CLEANUP_INTERVAL
|
|
204
|
+
self.worker_cleanup()
|
|
205
|
+
with contextlib.suppress(AdvisoryLockError), PGAdvisoryLock(TASK_SCHEDULING_LOCK):
|
|
206
|
+
dispatch_scheduled_tasks()
|
|
207
|
+
# This "reporting code" must not me moved inside a task, because it is supposed
|
|
208
|
+
# to be able to report on a congested tasking system to produce reliable results.
|
|
209
|
+
self.record_unblocked_waiting_tasks_metric()
|
|
210
|
+
|
|
211
|
+
def notify_workers(self, reason="unknown"):
|
|
212
|
+
self.cursor.execute("SELECT pg_notify('pulp_worker_wakeup', %s)", (reason,))
|
|
194
213
|
|
|
195
214
|
def cancel_abandoned_task(self, task, final_state, reason=None):
|
|
196
215
|
"""Cancel and clean up an abandoned task.
|
|
@@ -201,7 +220,7 @@ class PulpcoreWorker:
|
|
|
201
220
|
Return ``True`` if the task was actually canceled, ``False`` otherwise.
|
|
202
221
|
"""
|
|
203
222
|
# A task is considered abandoned when in running state, but no worker holds its lock
|
|
204
|
-
domain =
|
|
223
|
+
domain = task.pulp_domain
|
|
205
224
|
try:
|
|
206
225
|
task.set_canceling()
|
|
207
226
|
except RuntimeError:
|
|
@@ -224,11 +243,11 @@ class PulpcoreWorker:
|
|
|
224
243
|
delete_incomplete_resources(task)
|
|
225
244
|
task.set_canceled(final_state=final_state, reason=reason)
|
|
226
245
|
if task.reserved_resources_record:
|
|
227
|
-
self.notify_workers()
|
|
246
|
+
self.notify_workers(TASK_WAKEUP_UNBLOCK)
|
|
228
247
|
return True
|
|
229
248
|
|
|
230
249
|
def is_compatible(self, task):
|
|
231
|
-
domain =
|
|
250
|
+
domain = task.pulp_domain
|
|
232
251
|
unmatched_versions = [
|
|
233
252
|
f"task: {label}>={version} worker: {self.versions.get(label)}"
|
|
234
253
|
for label, version in task.versions.items()
|
|
@@ -249,10 +268,31 @@ class PulpcoreWorker:
|
|
|
249
268
|
def unblock_tasks(self):
|
|
250
269
|
"""Iterate over waiting tasks and mark them unblocked accordingly.
|
|
251
270
|
|
|
252
|
-
|
|
271
|
+
This function also handles the communication around it.
|
|
272
|
+
In order to prevent multiple workers to attempt unblocking tasks at the same time it tries
|
|
273
|
+
to acquire a lock and just returns on failure to do so.
|
|
274
|
+
Also it clears the notification about tasks to be unblocked and sends the notification that
|
|
275
|
+
new unblocked tasks are made available.
|
|
276
|
+
|
|
277
|
+
Returns the number of new unblocked tasks.
|
|
253
278
|
"""
|
|
254
279
|
|
|
255
|
-
|
|
280
|
+
assert not self.auxiliary
|
|
281
|
+
|
|
282
|
+
count = 0
|
|
283
|
+
self.wakeup_unblock_tasks = False
|
|
284
|
+
with contextlib.suppress(AdvisoryLockError), PGAdvisoryLock(TASK_UNBLOCKING_LOCK):
|
|
285
|
+
if count := self._unblock_tasks():
|
|
286
|
+
self.notify_workers(TASK_WAKEUP_HANDLE)
|
|
287
|
+
return count
|
|
288
|
+
|
|
289
|
+
def _unblock_tasks(self):
|
|
290
|
+
"""Iterate over waiting tasks and mark them unblocked accordingly.
|
|
291
|
+
|
|
292
|
+
Returns the number of new unblocked tasks.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
count = 0
|
|
256
296
|
taken_exclusive_resources = set()
|
|
257
297
|
taken_shared_resources = set()
|
|
258
298
|
# When batching this query, be sure to use "pulp_created" as a cursor
|
|
@@ -280,7 +320,7 @@ class PulpcoreWorker:
|
|
|
280
320
|
task.pulp_domain.name,
|
|
281
321
|
)
|
|
282
322
|
task.unblock()
|
|
283
|
-
|
|
323
|
+
count += 1
|
|
284
324
|
# Don't consider this task's resources as held.
|
|
285
325
|
continue
|
|
286
326
|
|
|
@@ -301,7 +341,7 @@ class PulpcoreWorker:
|
|
|
301
341
|
task.pulp_domain.name,
|
|
302
342
|
)
|
|
303
343
|
task.unblock()
|
|
304
|
-
|
|
344
|
+
count += 1
|
|
305
345
|
elif task.state == TASK_STATES.RUNNING and task.unblocked_at is None:
|
|
306
346
|
# This should not happen in normal operation.
|
|
307
347
|
# And it is only an issue if the worker running that task died, because it will
|
|
@@ -318,7 +358,7 @@ class PulpcoreWorker:
|
|
|
318
358
|
taken_exclusive_resources.update(exclusive_resources)
|
|
319
359
|
taken_shared_resources.update(shared_resources)
|
|
320
360
|
|
|
321
|
-
return
|
|
361
|
+
return count
|
|
322
362
|
|
|
323
363
|
def iter_tasks(self):
|
|
324
364
|
"""Iterate over ready tasks and yield each task while holding the lock."""
|
|
@@ -327,7 +367,7 @@ class PulpcoreWorker:
|
|
|
327
367
|
for task in Task.objects.filter(
|
|
328
368
|
state__in=TASK_INCOMPLETE_STATES,
|
|
329
369
|
unblocked_at__isnull=False,
|
|
330
|
-
).order_by("-immediate", "pulp_created"):
|
|
370
|
+
).order_by("-immediate", F("pulp_created") + Value(timedelta(seconds=8)) * Random()):
|
|
331
371
|
# This code will only be called if we acquired the lock successfully
|
|
332
372
|
# The lock will be automatically be released at the end of the block
|
|
333
373
|
with contextlib.suppress(AdvisoryLockError), task:
|
|
@@ -366,16 +406,18 @@ class PulpcoreWorker:
|
|
|
366
406
|
"""Wait for signals on the wakeup channel while heart beating."""
|
|
367
407
|
|
|
368
408
|
_logger.debug(_("Worker %s entering sleep state."), self.name)
|
|
369
|
-
while not self.shutdown_requested and not self.
|
|
409
|
+
while not self.shutdown_requested and not self.wakeup_handle:
|
|
370
410
|
r, w, x = select.select(
|
|
371
411
|
[self.sentinel, connection.connection], [], [], self.heartbeat_period.seconds
|
|
372
412
|
)
|
|
373
413
|
self.beat()
|
|
374
414
|
if connection.connection in r:
|
|
375
415
|
connection.connection.execute("SELECT 1")
|
|
416
|
+
if self.wakeup_unblock:
|
|
417
|
+
self.unblock_tasks()
|
|
376
418
|
if self.sentinel in r:
|
|
377
419
|
os.read(self.sentinel, 256)
|
|
378
|
-
|
|
420
|
+
_logger.debug(_("Worker %s leaving sleep state."), self.name)
|
|
379
421
|
|
|
380
422
|
def supervise_task(self, task):
|
|
381
423
|
"""Call and supervise the task process while heart beating.
|
|
@@ -388,7 +430,7 @@ class PulpcoreWorker:
|
|
|
388
430
|
task.save(update_fields=["worker"])
|
|
389
431
|
cancel_state = None
|
|
390
432
|
cancel_reason = None
|
|
391
|
-
domain =
|
|
433
|
+
domain = task.pulp_domain
|
|
392
434
|
with TemporaryDirectory(dir=".") as task_working_dir_rel_path:
|
|
393
435
|
task_process = Process(target=perform_task, args=(task.pk, task_working_dir_rel_path))
|
|
394
436
|
task_process.start()
|
|
@@ -424,12 +466,8 @@ class PulpcoreWorker:
|
|
|
424
466
|
)
|
|
425
467
|
cancel_state = TASK_STATES.CANCELED
|
|
426
468
|
self.cancel_task = False
|
|
427
|
-
if self.
|
|
428
|
-
|
|
429
|
-
TASK_UNBLOCKING_LOCK
|
|
430
|
-
):
|
|
431
|
-
self.unblock_tasks()
|
|
432
|
-
self.wakeup = False
|
|
469
|
+
if self.wakeup_unblock:
|
|
470
|
+
self.unblock_tasks()
|
|
433
471
|
if task_process.sentinel in r:
|
|
434
472
|
if not task_process.is_alive():
|
|
435
473
|
break
|
|
@@ -471,10 +509,10 @@ class PulpcoreWorker:
|
|
|
471
509
|
if cancel_state:
|
|
472
510
|
self.cancel_abandoned_task(task, cancel_state, cancel_reason)
|
|
473
511
|
if task.reserved_resources_record:
|
|
474
|
-
self.notify_workers()
|
|
512
|
+
self.notify_workers(TASK_WAKEUP_UNBLOCK)
|
|
475
513
|
self.task = None
|
|
476
514
|
|
|
477
|
-
def
|
|
515
|
+
def handle_unblocked_tasks(self):
|
|
478
516
|
"""Pick and supervise tasks until there are no more available tasks.
|
|
479
517
|
|
|
480
518
|
Failing to detect new available tasks can lead to a stuck state, as the workers
|
|
@@ -483,11 +521,9 @@ class PulpcoreWorker:
|
|
|
483
521
|
"""
|
|
484
522
|
keep_looping = True
|
|
485
523
|
while keep_looping and not self.shutdown_requested:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
except AdvisoryLockError:
|
|
490
|
-
keep_looping = True
|
|
524
|
+
# Clear pending wakeups. We are about to handle them anyway.
|
|
525
|
+
self.wakeup_handle = False
|
|
526
|
+
keep_looping = False
|
|
491
527
|
for task in self.iter_tasks():
|
|
492
528
|
keep_looping = True
|
|
493
529
|
self.supervise_task(task)
|
|
@@ -538,14 +574,19 @@ class PulpcoreWorker:
|
|
|
538
574
|
self.cursor.execute("LISTEN pulp_worker_cancel")
|
|
539
575
|
self.cursor.execute("LISTEN pulp_worker_metrics_heartbeat")
|
|
540
576
|
if burst:
|
|
541
|
-
self.
|
|
577
|
+
if not self.auxiliary:
|
|
578
|
+
# Attempt to flush the task queue completely.
|
|
579
|
+
# Stop iteration if no new tasks were found to unblock.
|
|
580
|
+
while self.unblock_tasks():
|
|
581
|
+
self.handle_unblocked_tasks()
|
|
582
|
+
self.handle_unblocked_tasks()
|
|
542
583
|
else:
|
|
543
584
|
self.cursor.execute("LISTEN pulp_worker_wakeup")
|
|
544
585
|
while not self.shutdown_requested:
|
|
545
586
|
# do work
|
|
546
587
|
if self.shutdown_requested:
|
|
547
588
|
break
|
|
548
|
-
self.
|
|
589
|
+
self.handle_unblocked_tasks()
|
|
549
590
|
if self.shutdown_requested:
|
|
550
591
|
break
|
|
551
592
|
# rest until notified to wakeup
|
|
@@ -13,6 +13,11 @@ from pulpcore.app import settings
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@pytest.mark.parallel
|
|
16
|
+
@pytest.mark.skipif(
|
|
17
|
+
"rest_framework.authentication.BasicAuthentication"
|
|
18
|
+
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
19
|
+
reason="Test can't run unless BasicAuthentication is enabled",
|
|
20
|
+
)
|
|
16
21
|
def test_base_auth_success(pulpcore_bindings, pulp_admin_user):
|
|
17
22
|
"""Perform HTTP basic authentication with valid credentials.
|
|
18
23
|
|
|
@@ -33,6 +38,11 @@ def test_base_auth_success(pulpcore_bindings, pulp_admin_user):
|
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
@pytest.mark.parallel
|
|
41
|
+
@pytest.mark.skipif(
|
|
42
|
+
"rest_framework.authentication.BasicAuthentication"
|
|
43
|
+
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
44
|
+
reason="Test can't run unless BasicAuthentication is enabled",
|
|
45
|
+
)
|
|
36
46
|
def test_base_auth_failure(pulpcore_bindings, invalid_user):
|
|
37
47
|
"""Perform HTTP basic authentication with invalid credentials.
|
|
38
48
|
|
|
@@ -50,6 +60,11 @@ def test_base_auth_failure(pulpcore_bindings, invalid_user):
|
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
@pytest.mark.parallel
|
|
63
|
+
@pytest.mark.skipif(
|
|
64
|
+
"rest_framework.authentication.BasicAuthentication"
|
|
65
|
+
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
66
|
+
reason="Test can't run unless BasicAuthentication is enabled",
|
|
67
|
+
)
|
|
53
68
|
def test_base_auth_required(pulpcore_bindings, anonymous_user):
|
|
54
69
|
"""Perform HTTP basic authentication with no credentials.
|
|
55
70
|
|
|
@@ -69,7 +84,7 @@ def test_base_auth_required(pulpcore_bindings, anonymous_user):
|
|
|
69
84
|
@pytest.mark.parallel
|
|
70
85
|
@pytest.mark.skipif(
|
|
71
86
|
"django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS
|
|
72
|
-
|
|
87
|
+
or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
|
|
73
88
|
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
74
89
|
reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled",
|
|
75
90
|
)
|
|
@@ -98,7 +113,7 @@ def test_jq_header_remote_auth(pulpcore_bindings, anonymous_user):
|
|
|
98
113
|
@pytest.mark.parallel
|
|
99
114
|
@pytest.mark.skipif(
|
|
100
115
|
"django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS
|
|
101
|
-
|
|
116
|
+
or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
|
|
102
117
|
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
103
118
|
reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled",
|
|
104
119
|
)
|
|
@@ -127,7 +142,7 @@ def test_jq_header_remote_auth_denied_by_wrong_header(pulpcore_bindings, anonymo
|
|
|
127
142
|
@pytest.mark.parallel
|
|
128
143
|
@pytest.mark.skipif(
|
|
129
144
|
"django.contrib.auth.backends.RemoteUserBackend" not in settings.AUTHENTICATION_BACKENDS
|
|
130
|
-
|
|
145
|
+
or "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
|
|
131
146
|
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
132
147
|
reason="Test can't run unless RemoteUserBackend and JSONHeaderRemoteAuthentication are enabled",
|
|
133
148
|
)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import http
|
|
2
2
|
import pytest
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from urllib.parse import urlparse
|
|
3
6
|
|
|
4
7
|
pytestmark = [pytest.mark.parallel]
|
|
5
8
|
|
|
@@ -20,27 +23,43 @@ def _fix_response_headers(monkeypatch, pulpcore_bindings):
|
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
@pytest.fixture
|
|
23
|
-
def session_user(pulpcore_bindings, gen_user,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
headers
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
def session_user(pulpcore_bindings, gen_user, bindings_cfg):
|
|
27
|
+
class SessionUser(gen_user):
|
|
28
|
+
def __enter__(self):
|
|
29
|
+
"""
|
|
30
|
+
Mimic the behavior of a session user (aka what browsers do).
|
|
31
|
+
|
|
32
|
+
- Set auth to None so client will use the session cookie
|
|
33
|
+
- Set X-CSRFToken header since we are posting JSON instead of form data
|
|
34
|
+
(Django creates a hidden input field for the CSRF token when using forms)
|
|
35
|
+
- Set Origin and Host headers so Django CSRF middleware will allow the request
|
|
36
|
+
(Browsers send these headers and it is needed when using HTTPS)
|
|
37
|
+
"""
|
|
38
|
+
self.old_cookie = pulpcore_bindings.client.cookie
|
|
39
|
+
super().__enter__()
|
|
40
|
+
response = pulpcore_bindings.LoginApi.login_with_http_info()
|
|
41
|
+
if isinstance(response, tuple):
|
|
42
|
+
# old bindings
|
|
43
|
+
_, _, headers = response
|
|
44
|
+
else:
|
|
45
|
+
# new bindings
|
|
46
|
+
headers = response.headers
|
|
47
|
+
cookie_jar = http.cookies.SimpleCookie(headers["Set-Cookie"])
|
|
48
|
+
self.cookie = "; ".join((f"{k}={v.value}" for k, v in cookie_jar.items()))
|
|
49
|
+
self.csrf_token = cookie_jar["csrftoken"].value
|
|
50
|
+
self.session_id = cookie_jar["sessionid"].value
|
|
51
|
+
bindings_cfg.username, bindings_cfg.password = None, None
|
|
52
|
+
pulpcore_bindings.client.cookie = self.cookie
|
|
53
|
+
pulpcore_bindings.client.set_default_header("X-CSRFToken", self.csrf_token)
|
|
54
|
+
pulpcore_bindings.client.set_default_header("Origin", bindings_cfg.host)
|
|
55
|
+
pulpcore_bindings.client.set_default_header("Host", urlparse(bindings_cfg.host).netloc)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
59
|
+
super().__exit__(exc_type, exc_value, traceback)
|
|
60
|
+
pulpcore_bindings.client.cookie = self.old_cookie
|
|
61
|
+
|
|
62
|
+
return SessionUser
|
|
44
63
|
|
|
45
64
|
|
|
46
65
|
def test_login_read_denies_anonymous(pulpcore_bindings, anonymous_user):
|
|
@@ -83,20 +102,31 @@ def test_login_sets_session_cookie(pulpcore_bindings, gen_user):
|
|
|
83
102
|
assert cookie_jar["csrftoken"].value != ""
|
|
84
103
|
|
|
85
104
|
|
|
86
|
-
def test_session_cookie_is_authorization(pulpcore_bindings,
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
def test_session_cookie_is_authorization(pulpcore_bindings, session_user):
|
|
106
|
+
with session_user() as user:
|
|
107
|
+
result = pulpcore_bindings.LoginApi.login_read()
|
|
108
|
+
assert result.username == user.username
|
|
109
|
+
assert pulpcore_bindings.client.configuration.username is None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_session_cookie_object_create(pulpcore_bindings, session_user, gen_object_with_cleanup):
|
|
113
|
+
with session_user(model_roles=["core.rbaccontentguard_creator"]):
|
|
114
|
+
assert pulpcore_bindings.client.configuration.username is None
|
|
115
|
+
gen_object_with_cleanup(pulpcore_bindings.ContentguardsRbacApi, {"name": str(uuid.uuid4())})
|
|
89
116
|
|
|
90
117
|
|
|
91
118
|
def test_logout_removes_sessionid(pulpcore_bindings, session_user):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
119
|
+
with session_user() as user:
|
|
120
|
+
assert user.session_id != ""
|
|
121
|
+
assert user.session_id in pulpcore_bindings.client.cookie
|
|
122
|
+
response = pulpcore_bindings.LoginApi.logout_with_http_info()
|
|
123
|
+
if isinstance(response, tuple):
|
|
124
|
+
# old bindings
|
|
125
|
+
_, status_code, headers = response
|
|
126
|
+
else:
|
|
127
|
+
# new bindings
|
|
128
|
+
status_code = response.status_code
|
|
129
|
+
headers = response.headers
|
|
100
130
|
assert status_code == 204
|
|
101
131
|
cookie_jar = http.cookies.SimpleCookie(headers["Set-Cookie"])
|
|
102
132
|
assert cookie_jar["sessionid"].value == ""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import json
|
|
5
|
-
import
|
|
5
|
+
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
import jsonschema
|
|
@@ -10,28 +10,31 @@ import jsonschema
|
|
|
10
10
|
from drf_spectacular import validation
|
|
11
11
|
from collections import defaultdict
|
|
12
12
|
|
|
13
|
-
JSON_SCHEMA_SPEC_PATH = os.path.join(
|
|
14
|
-
os.path.dirname(validation.__file__), "openapi_3_0_schema.json"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
13
|
|
|
18
14
|
@pytest.fixture(scope="session")
|
|
19
|
-
def openapi3_schema_spec():
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
def openapi3_schema_spec(pulp_openapi_schema):
|
|
16
|
+
schema_version = pulp_openapi_schema["openapi"]
|
|
17
|
+
if schema_version.startswith("3.0"):
|
|
18
|
+
spec_path = Path(validation.__file__).parent / "openapi_3_0_schema.json"
|
|
19
|
+
elif schema_version.startswith("3.1"):
|
|
20
|
+
spec_path = Path(validation.__file__).parent / "openapi_3_1_schema.json"
|
|
21
|
+
else:
|
|
22
|
+
pytest.fail(f"Unknown OpenAPI schema version [{schema_version}].")
|
|
23
|
+
return json.loads(spec_path.read_text())
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
@pytest.fixture(scope="session")
|
|
27
27
|
def openapi3_schema_with_modified_safe_chars(openapi3_schema_spec):
|
|
28
|
-
|
|
28
|
+
oas_copy = copy.deepcopy(openapi3_schema_spec) # Don't modify the original
|
|
29
29
|
# Making OpenAPI validation to accept paths starting with / and {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
if "3.1.x" in oas_copy["description"]:
|
|
31
|
+
pattern_properties = oas_copy["$defs"]["paths"]["patternProperties"]
|
|
32
|
+
pattern_properties["^/|{"] = pattern_properties.pop("^/")
|
|
33
|
+
else:
|
|
34
|
+
pattern_properties = oas_copy["definitions"]["Paths"]["patternProperties"]
|
|
35
|
+
pattern_properties["^\\/|{"] = pattern_properties.pop("^\\/")
|
|
33
36
|
|
|
34
|
-
return
|
|
37
|
+
return oas_copy
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
@pytest.mark.parallel
|
|
@@ -68,6 +71,20 @@ def test_no_dup_operation_ids(pulp_openapi_schema):
|
|
|
68
71
|
assert len(dup_ids) == 0, f"Duplicate operationIds found: {dup_ids}"
|
|
69
72
|
|
|
70
73
|
|
|
74
|
+
@pytest.mark.parallel
|
|
75
|
+
def test_remote_user_auth_security_scheme(pulp_settings, pulp_openapi_schema):
|
|
76
|
+
if (
|
|
77
|
+
"pulpcore.app.authentication.PulpRemoteUserAuthentication"
|
|
78
|
+
not in pulp_settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]
|
|
79
|
+
):
|
|
80
|
+
pytest.skip("Test can't run unless PulpRemoteUserAuthentication is enabled.")
|
|
81
|
+
|
|
82
|
+
expected_security_scheme = pulp_settings.REMOTE_USER_OPENAPI_SECURITY_SCHEME
|
|
83
|
+
security_schemes = pulp_openapi_schema["components"]["securitySchemes"]
|
|
84
|
+
|
|
85
|
+
assert security_schemes["remoteUserAuthentication"] == expected_security_scheme
|
|
86
|
+
|
|
87
|
+
|
|
71
88
|
@pytest.mark.parallel
|
|
72
89
|
def test_external_auth_on_security_scheme(pulp_settings, pulp_openapi_schema):
|
|
73
90
|
if (
|
|
@@ -87,11 +87,17 @@ class TestCheckpointDistribution:
|
|
|
87
87
|
assert Handler._format_checkpoint_timestamp(pubs[3].pulp_created) in checkpoints_ts
|
|
88
88
|
|
|
89
89
|
@pytest.mark.parallel
|
|
90
|
-
def
|
|
90
|
+
def test_distro_root_no_trailing_slash_is_redirected(
|
|
91
|
+
self,
|
|
92
|
+
setup,
|
|
93
|
+
http_get,
|
|
94
|
+
distribution_base_url,
|
|
95
|
+
):
|
|
91
96
|
"""Test checkpoint listing when path doesn't end with a slash."""
|
|
92
97
|
|
|
93
98
|
pubs, distribution = setup
|
|
94
99
|
|
|
100
|
+
# Test a checkpoint distro listing path
|
|
95
101
|
response = http_get(distribution_base_url(distribution.base_url[:-1])).decode("utf-8")
|
|
96
102
|
checkpoints_ts = set(re.findall(r"\d{8}T\d{6}Z", response))
|
|
97
103
|
|
|
@@ -99,6 +105,22 @@ class TestCheckpointDistribution:
|
|
|
99
105
|
assert Handler._format_checkpoint_timestamp(pubs[1].pulp_created) in checkpoints_ts
|
|
100
106
|
assert Handler._format_checkpoint_timestamp(pubs[3].pulp_created) in checkpoints_ts
|
|
101
107
|
|
|
108
|
+
@pytest.mark.parallel
|
|
109
|
+
def test_timestamped_checkpoint_no_trailing_slash_is_redirected(
|
|
110
|
+
self,
|
|
111
|
+
setup,
|
|
112
|
+
http_get,
|
|
113
|
+
checkpoint_url,
|
|
114
|
+
):
|
|
115
|
+
"""Test a timestamped checkpoint when path doesn't end with a slash."""
|
|
116
|
+
|
|
117
|
+
pubs, distribution = setup
|
|
118
|
+
|
|
119
|
+
pub_1_url = checkpoint_url(distribution, pubs[1].pulp_created)
|
|
120
|
+
response = http_get(pub_1_url[:-1]).decode("utf-8")
|
|
121
|
+
|
|
122
|
+
assert f"<h1>Index of {urlparse(pub_1_url).path}</h1>" in response
|
|
123
|
+
|
|
102
124
|
@pytest.mark.parallel
|
|
103
125
|
def test_exact_timestamp_is_served(self, setup, http_get, checkpoint_url):
|
|
104
126
|
pubs, distribution = setup
|