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.
Files changed (169) hide show
  1. pulp_certguard/app/__init__.py +1 -1
  2. pulp_certguard/app/models.py +7 -26
  3. pulp_certguard/app/serializers.py +0 -2
  4. pulp_certguard/rhsm/__init__.py +4 -0
  5. pulp_certguard/rhsm/rhsm_check_path.py +198 -0
  6. pulp_certguard/tests/unit/certdata.py +249 -0
  7. pulp_certguard/tests/unit/test_rhsm_check_path.py +213 -0
  8. pulp_file/app/__init__.py +1 -1
  9. pulp_file/app/migrations/0001_initial_squashed_0016_add_domain.py +0 -20
  10. pulp_file/app/migrations/0017_alter_filealternatecontentsource_alternatecontentsource_ptr_and_more.py +1 -1
  11. pulpcore/app/apps.py +2 -12
  12. pulpcore/app/entrypoint.py +22 -22
  13. pulpcore/app/migrations/0001_squashed_0090_char_to_text_field.py +0 -95
  14. pulpcore/app/migrations/0091_systemid.py +1 -1
  15. pulpcore/app/migrations/0134_task_insert_trigger.py +81 -0
  16. pulpcore/app/migrations/0135_task_pulp_task_resources_index.py +25 -0
  17. pulp_file/app/migrations/0006_delete_filefilesystemexporter.py → pulpcore/app/migrations/0136_delete_basedistribution.py +3 -3
  18. pulpcore/app/migrations/0137_appstatus.py +33 -0
  19. pulpcore/app/migrations/0138_vulnerabilityreport.py +33 -0
  20. pulpcore/app/models/__init__.py +4 -1
  21. pulpcore/app/models/publication.py +0 -41
  22. pulpcore/app/models/status.py +145 -0
  23. pulpcore/app/models/task.py +8 -0
  24. pulpcore/app/models/vulnerability_report.py +34 -0
  25. pulpcore/app/serializers/__init__.py +1 -0
  26. pulpcore/app/serializers/content.py +13 -1
  27. pulpcore/app/serializers/repository.py +8 -1
  28. pulpcore/app/serializers/vulnerability_report.py +27 -0
  29. pulpcore/app/settings.py +13 -38
  30. pulpcore/app/tasks/__init__.py +2 -0
  31. pulpcore/app/tasks/purge.py +8 -5
  32. pulpcore/app/tasks/vulnerability_report.py +159 -0
  33. pulpcore/app/viewsets/__init__.py +1 -0
  34. pulpcore/app/viewsets/vulnerability_report.py +20 -0
  35. pulpcore/constants.py +8 -0
  36. pulpcore/content/__init__.py +23 -22
  37. pulpcore/content/handler.py +5 -2
  38. pulpcore/migrations.py +38 -11
  39. pulpcore/openapi/__init__.py +8 -0
  40. pulpcore/plugin/models/__init__.py +2 -0
  41. pulpcore/plugin/serializers/__init__.py +2 -0
  42. pulpcore/plugin/tasking.py +2 -0
  43. pulpcore/plugin/viewsets/__init__.py +2 -0
  44. pulpcore/pytest_plugin.py +21 -21
  45. pulpcore/tasking/entrypoint.py +12 -2
  46. pulpcore/tasking/tasks.py +5 -30
  47. pulpcore/tasking/worker.py +115 -74
  48. pulpcore/tests/functional/api/test_auth.py +18 -3
  49. pulpcore/tests/functional/api/test_login.py +62 -32
  50. pulpcore/tests/functional/api/test_openapi_schema.py +32 -15
  51. pulpcore/tests/functional/api/using_plugin/test_checkpoint.py +23 -1
  52. pulpcore/tests/functional/api/using_plugin/test_proxy.py +1 -1
  53. pulpcore/tests/unit/content/test_heartbeat.py +11 -8
  54. pulpcore/tests/unit/test_vulnerability_report.py +74 -0
  55. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/METADATA +13 -18
  56. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/RECORD +60 -156
  57. pulp_certguard/app/utils.py +0 -28
  58. pulp_certguard/tests/unit/test_models.py +0 -9
  59. pulp_file/app/migrations/0001_initial.py +0 -59
  60. pulp_file/app/migrations/0002_file_related_names.py +0 -55
  61. pulp_file/app/migrations/0003_auto_20191014_1721.py +0 -18
  62. pulp_file/app/migrations/0004_filefilesystemexporter.py +0 -21
  63. pulp_file/app/migrations/0005_filerepository.py +0 -24
  64. pulp_file/app/migrations/0007_filefilesystemexporter.py +0 -25
  65. pulp_file/app/migrations/0008_add_manifest_field.py +0 -19
  66. pulp_file/app/migrations/0009_move_data_to_new_master_distribution_model.py +0 -77
  67. pulp_file/app/migrations/0010_auto_publish.py +0 -23
  68. pulp_file/app/migrations/0011_fix_auto_publish.py +0 -36
  69. pulp_file/app/migrations/0012_delete_filefilesystemexporter.py +0 -28
  70. pulp_file/app/migrations/0013_file_acs.py +0 -24
  71. pulp_file/app/migrations/0014_new_rbac_permissions.py +0 -33
  72. pulp_file/app/migrations/0015_allow_null_manifest.py +0 -23
  73. pulp_file/app/migrations/0016_add_domain.py +0 -25
  74. pulpcore/app/migrations/0001_initial.py +0 -451
  75. pulpcore/app/migrations/0002_increase_artifact_size_field.py +0 -18
  76. pulpcore/app/migrations/0003_remove_upload_completed.py +0 -17
  77. pulpcore/app/migrations/0004_add_duplicated_reserved_resources.py +0 -45
  78. pulpcore/app/migrations/0005_progressreport_code.py +0 -19
  79. pulpcore/app/migrations/0006_repository_plugin_managed.py +0 -18
  80. pulpcore/app/migrations/0007_delete_progress_proxies.py +0 -19
  81. pulpcore/app/migrations/0008_published_metadata_as_content.py +0 -44
  82. pulpcore/app/migrations/0009_remove_task_non_fatal_errors.py +0 -17
  83. pulpcore/app/migrations/0010_pulp_fields.py +0 -570
  84. pulpcore/app/migrations/0011_relative_path.py +0 -28
  85. pulpcore/app/migrations/0012_auto_20191104_2000.py +0 -31
  86. pulpcore/app/migrations/0013_repository_pulp_type.py +0 -18
  87. pulpcore/app/migrations/0014_remove_repository_plugin_managed.py +0 -17
  88. pulpcore/app/migrations/0015_auto_20191112_1426.py +0 -33
  89. pulpcore/app/migrations/0016_charfield_to_textfield.py +0 -68
  90. pulpcore/app/migrations/0017_remove_task_parent.py +0 -17
  91. pulpcore/app/migrations/0018_auto_20191127_2350.py +0 -20
  92. pulpcore/app/migrations/0019_add_signing_service_model.py +0 -27
  93. pulpcore/app/migrations/0020_change_publishedartifact_constraints.py +0 -17
  94. pulpcore/app/migrations/0021_add_signing_service_foreign_key.py +0 -24
  95. pulpcore/app/migrations/0022_rename_last_version.py +0 -27
  96. pulpcore/app/migrations/0023_change_exporter_models.py +0 -82
  97. pulpcore/app/migrations/0024_use_local_storage_for_uploads.py +0 -19
  98. pulpcore/app/migrations/0025_task_parent_task.py +0 -19
  99. pulpcore/app/migrations/0026_task_group.py +0 -32
  100. pulpcore/app/migrations/0027_export_backend.py +0 -31
  101. pulpcore/app/migrations/0028_import_importer_pulpimporter_pulpimporterrepository.py +0 -85
  102. pulpcore/app/migrations/0029_export_delete.py +0 -19
  103. pulpcore/app/migrations/0030_taskgroup_all_tasks_dispatched.py +0 -24
  104. pulpcore/app/migrations/0031_import_export_validate_params.py +0 -19
  105. pulpcore/app/migrations/0032_export_to_chunks.py +0 -27
  106. pulpcore/app/migrations/0033_increase_remote_artifact_size_field.py +0 -18
  107. pulpcore/app/migrations/0034_groupprogressreport.py +0 -32
  108. pulpcore/app/migrations/0035_content_upstream_id.py +0 -18
  109. pulpcore/app/migrations/0036_unprotect_last_export.py +0 -19
  110. pulpcore/app/migrations/0037_pulptemporaryfile.py +0 -28
  111. pulpcore/app/migrations/0038_repository_remote.py +0 -19
  112. pulpcore/app/migrations/0039_change_download_concurrency.py +0 -25
  113. pulpcore/app/migrations/0040_set_admin_is_staff.py +0 -28
  114. pulpcore/app/migrations/0041_accesspolicy.py +0 -29
  115. pulpcore/app/migrations/0042_rbac_for_tasks.py +0 -56
  116. pulpcore/app/migrations/0043_toc_attribute.py +0 -19
  117. pulpcore/app/migrations/0044_temp_file_artifact_field.py +0 -20
  118. pulpcore/app/migrations/0045_accesspolicy_permissions_allow_null.py +0 -19
  119. pulpcore/app/migrations/0046_task__resource_job_id.py +0 -35
  120. pulpcore/app/migrations/0047_improve_orphan_cleanup.py +0 -59
  121. pulpcore/app/migrations/0048_fips_checksums.py +0 -38
  122. pulpcore/app/migrations/0049_add_file_field_to_uploadchunk.py +0 -24
  123. pulpcore/app/migrations/0050_namespace_access_policies.py +0 -28
  124. pulpcore/app/migrations/0051_timeoutfields.py +0 -34
  125. pulpcore/app/migrations/0052_tasking_logging_cid.py +0 -18
  126. pulpcore/app/migrations/0053_remote_headers.py +0 -19
  127. pulpcore/app/migrations/0054_add_public_key.py +0 -104
  128. pulpcore/app/migrations/0055_label.py +0 -31
  129. pulpcore/app/migrations/0056_remote_rate_limit.py +0 -18
  130. pulpcore/app/migrations/0057_add_label_indexes.py +0 -23
  131. pulpcore/app/migrations/0058_accesspolicy_customized.py +0 -18
  132. pulpcore/app/migrations/0059_proxy_creds.py +0 -23
  133. pulpcore/app/migrations/0060_data_migration_proxy_creds.py +0 -45
  134. pulpcore/app/migrations/0061_call_handle_artifact_checksums_command.py +0 -87
  135. pulpcore/app/migrations/0062_add_new_distribution_mastermodel.py +0 -36
  136. pulpcore/app/migrations/0063_repository_retained_versions.py +0 -18
  137. pulpcore/app/migrations/0064_add_new_style_task_columns.py +0 -109
  138. pulpcore/app/migrations/0064_repository_user_hidden.py +0 -18
  139. pulpcore/app/migrations/0065_merge_20210615_1211.py +0 -14
  140. pulpcore/app/migrations/0066_download_concurrency_and_retry_changes.py +0 -24
  141. pulpcore/app/migrations/0067_add_protect_to_task_reservation.py +0 -19
  142. pulpcore/app/migrations/0068_add_timestamp_of_interest.py +0 -23
  143. pulpcore/app/migrations/0069_update_json_fields.py +0 -63
  144. pulpcore/app/migrations/0070_rename_retained_versions.py +0 -18
  145. pulpcore/app/migrations/0071_filesystemexport_filesystemexporter.py +0 -35
  146. pulpcore/app/migrations/0072_add_method_to_filesystem_exporter.py +0 -18
  147. pulpcore/app/migrations/0073_encrypt_remote_fields.py +0 -139
  148. pulpcore/app/migrations/0074_acs.py +0 -47
  149. pulpcore/app/migrations/0075_rbaccontentguard.py +0 -25
  150. pulpcore/app/migrations/0076_remove_reserved_resource.py +0 -39
  151. pulpcore/app/migrations/0077_move_remote_url_credentials.py +0 -41
  152. pulpcore/app/migrations/0078_grouprole_role_userrole.py +0 -70
  153. pulpcore/app/migrations/0079_rename_permissions_assignment_accesspolicy_creation_hooks.py +0 -18
  154. pulpcore/app/migrations/0080_proxy_group_model.py +0 -37
  155. pulpcore/app/migrations/0081_reapplabel_group_permissions.py +0 -59
  156. pulpcore/app/migrations/0082_add_manage_roles_permissions.py +0 -17
  157. pulpcore/app/migrations/0083_alter_group_options.py +0 -17
  158. pulpcore/app/migrations/0084_alter_rbaccontentguard_options.py +0 -17
  159. pulpcore/app/migrations/0085_contentredirectcontentguard.py +0 -26
  160. pulpcore/app/migrations/0086_task_json_fields.py +0 -77
  161. pulpcore/app/migrations/0087_taskschedule.py +0 -34
  162. pulpcore/app/migrations/0088_accesspolicy_queryset_scoping.py +0 -18
  163. pulpcore/app/migrations/0089_alter_contentredirectcontentguard_options.py +0 -17
  164. pulpcore/app/migrations/0090_char_to_text_field.py +0 -79
  165. pulpcore/tests/unit/migration/test_0077_move_remote_url_credentials.py +0 -35
  166. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/WHEEL +0 -0
  167. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/entry_points.txt +0 -0
  168. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/licenses/LICENSE +0 -0
  169. {pulpcore-3.83.2.dist-info → pulpcore-3.85.0.dist-info}/top_level.txt +0 -0
@@ -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, get_domain
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.wakeup = False
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
- self.worker = self.handle_worker_heartbeat()
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
- self.wakeup = True
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
- Create or update worker heartbeat records.
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=worker.last_heartbeat, name=self.name
165
+ timestamp=self.app_status.last_heartbeat, name=self.name
158
166
  )
159
- _logger.debug(msg)
160
-
161
- return worker
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.worker.delete()
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
- if qs:
175
- for app_worker in qs:
176
- _logger.info(_("Clean missing %s worker %s."), cls_name, app_worker.name)
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.worker.last_heartbeat < timezone.now() - self.heartbeat_period:
181
- self.worker = self.handle_worker_heartbeat()
182
- self.worker_cleanup_countdown -= 1
183
- if self.worker_cleanup_countdown <= 0:
184
- self.worker_cleanup_countdown = WORKER_CLEANUP_INTERVAL
185
- self.worker_cleanup()
186
- with contextlib.suppress(AdvisoryLockError), PGAdvisoryLock(TASK_SCHEDULING_LOCK):
187
- dispatch_scheduled_tasks()
188
- # This "reporting code" must not me moved inside a task, because it is supposed
189
- # to be able to report on a congested tasking system to produce reliable results.
190
- self.record_unblocked_waiting_tasks_metric()
191
-
192
- def notify_workers(self):
193
- self.cursor.execute("NOTIFY pulp_worker_wakeup")
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 = get_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 = get_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
- Returns `True` if at least one task was unblocked. `False` otherwise.
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
- changed = False
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
- changed = True
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
- changed = True
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 changed
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.wakeup:
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
- self.wakeup = False
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 = get_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.wakeup:
428
- with contextlib.suppress(AdvisoryLockError), PGAdvisoryLock(
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 handle_available_tasks(self):
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
- try:
487
- with PGAdvisoryLock(TASK_UNBLOCKING_LOCK):
488
- keep_looping = self.unblock_tasks()
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.handle_available_tasks()
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.handle_available_tasks()
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
- and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
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
- and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
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
- and "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
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, anonymous_user):
24
- old_cookie = pulpcore_bindings.client.cookie
25
- user = gen_user()
26
- with user:
27
- response = pulpcore_bindings.LoginApi.login_with_http_info()
28
- if isinstance(response, tuple):
29
- # old bindings
30
- _, _, headers = response
31
- else:
32
- # new bindings
33
- headers = response.headers
34
- cookie_jar = http.cookies.SimpleCookie(headers["Set-Cookie"])
35
- # Use anonymous_user to remove the basic auth header from the api client.
36
- with anonymous_user:
37
- pulpcore_bindings.client.cookie = "; ".join(
38
- (f"{k}={v.value}" for k, v in cookie_jar.items())
39
- )
40
- # Weird: You need to pass the CSRFToken as a header not a cookie...
41
- pulpcore_bindings.client.set_default_header("X-CSRFToken", cookie_jar["csrftoken"].value)
42
- yield user
43
- pulpcore_bindings.client.cookie = old_cookie
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, anonymous_user, session_user):
87
- result = pulpcore_bindings.LoginApi.login_read()
88
- assert result.username == session_user.username
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
- response = pulpcore_bindings.LoginApi.logout_with_http_info()
93
- if isinstance(response, tuple):
94
- # old bindings
95
- _, status_code, headers = response
96
- else:
97
- # new bindings
98
- status_code = response.status_code
99
- headers = response.headers
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 os
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
- with open(JSON_SCHEMA_SPEC_PATH) as fh:
21
- openapi3_schema_spec = json.load(fh)
22
-
23
- return openapi3_schema_spec
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
- openapi3_schema_spec_copy = copy.deepcopy(openapi3_schema_spec) # Don't modify the original
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
- properties = openapi3_schema_spec_copy["definitions"]["Paths"]["patternProperties"]
31
- properties["^\\/|{"] = properties["^\\/"]
32
- del properties["^\\/"]
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 openapi3_schema_spec_copy
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 test_no_trailing_slash_is_redirected(self, setup, http_get, distribution_base_url):
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