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.

Potentially problematic release.


This version of pulpcore might be problematic. Click here for more details.

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
@@ -0,0 +1,159 @@
1
+ import aiohttp
2
+ import asyncio
3
+ import importlib
4
+
5
+ from asgiref.sync import sync_to_async
6
+ from django.conf import settings
7
+
8
+ from pulpcore.app.util import get_domain
9
+ from pulpcore.app.models.progress import ProgressReport
10
+ from pulpcore.app.models.vulnerability_report import VulnerabilityReport
11
+ from pulpcore.constants import OSV_QUERY_URL, TASK_STATES
12
+
13
+
14
+ class VulnerabilityReportScanner:
15
+ """
16
+ A scanner class to collect vulnerabilities from Pulp Contents using the OSV.dev API.
17
+
18
+ Attributes:
19
+ semaphore (asyncio.Semaphore): Controls concurrent API requests to osv.dev service.
20
+ generator (AsyncGenerator): The async generator with the content data to be scanned for
21
+ vulnerabilities.
22
+ session (aiohttp.ClientSession): HTTP session for making requests to the OSV.dev API.
23
+ Defaults to a new session if not provided.
24
+ created (ProgressReport): Tracks the number of new VulnerabilityReport records created
25
+ during the scanning process.
26
+ updated (ProgressReport): Tracks the number of existing VulnerabilityReport records
27
+ updated during the scanning process.
28
+ total_scanned (ProgressReport): Tracks the total number of content items that have
29
+ been processed for vulnerability scanning.
30
+ """
31
+
32
+ def __init__(self, semaphore, generator, session=None):
33
+ self.semaphore = semaphore
34
+ self.generator = generator
35
+ self.session = session or aiohttp.ClientSession()
36
+ self.created = ProgressReport(
37
+ message="Vulnerability Reports created", code="created", state=TASK_STATES.RUNNING
38
+ )
39
+ self.updated = ProgressReport(
40
+ message="Vulnerability Reports updated", code="updated", state=TASK_STATES.RUNNING
41
+ )
42
+ self.total_scanned = ProgressReport(
43
+ message="Content scanned", code="total_scanned", state=TASK_STATES.RUNNING
44
+ )
45
+
46
+ async def scan_packages(self):
47
+ """
48
+ Main entry point for scanning packages for vulnerabilities. Progress is tracked through
49
+ three ProgressReport instances (created, updated, total_scanned).
50
+ """
51
+ tasks = []
52
+ async for content in self.generator:
53
+ tasks.append(asyncio.create_task(self.scan_package(content)))
54
+ await asyncio.gather(*tasks)
55
+ await self.session.close()
56
+
57
+ if self.created.done > 0:
58
+ self.created.state = TASK_STATES.COMPLETED
59
+ await self.created.asave()
60
+ if self.total_scanned.done > 0:
61
+ self.total_scanned.state = TASK_STATES.COMPLETED
62
+ await self.total_scanned.asave()
63
+ if self.updated.done > 0:
64
+ self.updated.state = TASK_STATES.COMPLETED
65
+ await self.updated.asave()
66
+
67
+ async def scan_package(self, repo_content_osv_data):
68
+ """
69
+ Makes a request to the osv.dev API and store the results in VulnerabilityReport model.
70
+
71
+ Args:
72
+ repo_content_osv_data (Dict[str, Any]): A dictionary with osv.dev expected request
73
+ data format plus associated Pulp Content and RepoVersion.
74
+ """
75
+ vulns, content, repo_version = await self._process_reports(repo_content_osv_data)
76
+ await self._save_vulnerability_report(vulns, content, repo_version)
77
+
78
+ async def _process_reports(self, repo_content_osv_data):
79
+ """
80
+ Process vulnerability scanning data by querying the OSV API and extracting results.
81
+
82
+ Args:
83
+ repo_content_osv_data (Dict[str, Any]): A dictionary containing OSV.dev expected
84
+ request data format plus associated Pulp Content and RepositoryVersion.
85
+
86
+ Returns:
87
+ tuple:
88
+ - vulns (List[Dict]): List of vulnerability (vulns) returned from OSV.dev API
89
+ - pulp_content (Content): The Pulp Content instance that was scanned
90
+ - repo_version (RepositoryVersion or None): The RepositoryVersion where the
91
+ Content is present
92
+ """
93
+ vulns = []
94
+ async with self.semaphore:
95
+ repo_version = repo_content_osv_data.pop("repo_version", None)
96
+ pulp_content = repo_content_osv_data.pop("content")
97
+ vulnerability_data = await self._query_osv_api(repo_content_osv_data)
98
+ vulns = vulnerability_data.get("vulns", [])
99
+ while next_page_token := vulnerability_data.get("next_page_token"):
100
+ repo_content_osv_data["page_token"] = next_page_token
101
+ vulnerability_data = await self._query_osv_api(repo_content_osv_data)
102
+ vulns.extend(vulnerability_data)
103
+ await self.total_scanned.aincrement()
104
+ return vulns, pulp_content, repo_version
105
+
106
+ async def _query_osv_api(self, osv_data):
107
+ """
108
+ Make a single HTTP POST request to the OSV.dev vulnerability database API.
109
+
110
+ Args:
111
+ osv_data (Dict[str, Any]): OSV query data dictionary containing package information
112
+ in the format expected by the OSV.dev API.
113
+
114
+ Returns:
115
+ Dict[str, Any]: JSON response from the OSV API containing vulnerability data.
116
+ """
117
+ async with self.session.post(url=OSV_QUERY_URL, json=osv_data) as response:
118
+ try:
119
+ return await response.json()
120
+ except aiohttp.ContentTypeError:
121
+ raise RuntimeError("Vuln report task failed to query osv.dev data.")
122
+
123
+ async def _save_vulnerability_report(self, vulns, content, repo_version):
124
+ """
125
+ Save vulnerability report to the database.
126
+
127
+ Args:
128
+ vulns (List): osv.dev vulns output
129
+ content (Content): A Pulp Content instance which vulnerabilities were verified
130
+ repo_version (RepositoryVersion): The RepositoryVersion where Content is present
131
+ """
132
+
133
+ vuln_report, created = await sync_to_async(VulnerabilityReport.objects.update_or_create)(
134
+ vulns=vulns, pulp_domain=get_domain(), content=content
135
+ )
136
+ await vuln_report.repo_versions.aadd(repo_version)
137
+
138
+ if created:
139
+ await self.created.aincrement()
140
+ else:
141
+ await self.updated.aincrement()
142
+
143
+
144
+ async def check_content(func, args):
145
+ """
146
+ Spawn tasks for each async generator value to make the API requests (scan) to osv.dev.
147
+
148
+ Args:
149
+ func (callable | str): A Pulp plugin function that should return an async generator with
150
+ the osv.dev expected request data format plus associated Pulp Content and RepoVersion.
151
+ args (tuple): The positional arguments to pass on to the func.
152
+ """
153
+ module_name, function_name = func.rsplit(".", 1)
154
+ module = importlib.import_module(module_name)
155
+ func = getattr(module, function_name)
156
+
157
+ semaphore = asyncio.Semaphore(settings.VULN_REPORT_TASK_LIMITER)
158
+ vuln_report_scanner = VulnerabilityReportScanner(semaphore, func(*args))
159
+ await vuln_report_scanner.scan_packages()
@@ -82,6 +82,7 @@ from .user import (
82
82
  UserRoleViewSet,
83
83
  )
84
84
  from .replica import UpstreamPulpViewSet
85
+ from .vulnerability_report import VulnerabilityReportViewSet
85
86
  from .openpgp import (
86
87
  OpenPGPDistributionViewSet,
87
88
  OpenPGPKeyringViewSet,
@@ -0,0 +1,20 @@
1
+ from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin
2
+
3
+ from pulpcore.app.models.vulnerability_report import VulnerabilityReport
4
+ from pulpcore.app.serializers.vulnerability_report import VulnerabilityReportSerializer
5
+ from pulpcore.app.viewsets.base import NamedModelViewSet
6
+
7
+
8
+ class VulnerabilityReportViewSet(
9
+ NamedModelViewSet, ListModelMixin, RetrieveModelMixin, DestroyModelMixin
10
+ ):
11
+
12
+ endpoint_name = "vuln_report"
13
+ queryset = VulnerabilityReport.objects.prefetch_related("repo_versions").select_related(
14
+ "content"
15
+ )
16
+ serializer_class = VulnerabilityReportSerializer
17
+
18
+ @classmethod
19
+ def routable(cls):
20
+ return True
pulpcore/constants.py CHANGED
@@ -12,6 +12,10 @@ TASK_UNBLOCKING_LOCK = 84
12
12
  TASK_METRICS_HEARTBEAT_LOCK = 74
13
13
  STORAGE_METRICS_LOCK = 72
14
14
 
15
+ # Reasons to send along a task worker wakeup call.
16
+ TASK_WAKEUP_UNBLOCK = "unblock"
17
+ TASK_WAKEUP_HANDLE = "handle"
18
+
15
19
  #: All valid task states.
16
20
  TASK_STATES = SimpleNamespace(
17
21
  WAITING="waiting",
@@ -127,3 +131,7 @@ CHECKPOINT_TS_FORMAT = "%Y%m%dT%H%M%SZ"
127
131
  # The upper boundary represents an unsigned 32-bit integer and prevents overflow
128
132
  ORPHAN_PROTECTION_TIME_LOWER_BOUND = 0
129
133
  ORPHAN_PROTECTION_TIME_UPPER_BOUND = 4294967295 # (2^32)-1
134
+
135
+ # VULNERABILITY REPORT CONSTANTS
136
+ # OSV API URL
137
+ OSV_QUERY_URL = "https://api.osv.dev/v1/query"
@@ -7,7 +7,7 @@ import os
7
7
 
8
8
  from asgiref.sync import sync_to_async
9
9
  from aiohttp import web
10
-
10
+ from gunicorn.arbiter import Arbiter
11
11
  import django
12
12
 
13
13
 
@@ -16,12 +16,13 @@ django.setup()
16
16
 
17
17
  from django.conf import settings # noqa: E402: module level not at top of file
18
18
  from django.db.utils import ( # noqa: E402: module level not at top of file
19
+ IntegrityError,
19
20
  InterfaceError,
20
21
  DatabaseError,
21
22
  )
22
23
 
23
24
  from pulpcore.app.apps import pulp_plugin_configs # noqa: E402: module level not at top of file
24
- from pulpcore.app.models import ContentAppStatus # noqa: E402: module level not at top of file
25
+ from pulpcore.app.models import AppStatus # noqa: E402: module level not at top of file
25
26
  from pulpcore.app.util import get_worker_name # noqa: E402: module level not at top of file
26
27
 
27
28
  from .handler import Handler # noqa: E402: module level not at top of file
@@ -48,40 +49,40 @@ CONTENT_MODULE_NAME = "content"
48
49
 
49
50
 
50
51
  async def _heartbeat():
51
- content_app_status = None
52
52
  name = get_worker_name()
53
53
  heartbeat_interval = settings.CONTENT_APP_TTL // 4
54
54
  msg = "Content App '{name}' heartbeat written, sleeping for '{interarrival}' seconds".format(
55
55
  name=name, interarrival=heartbeat_interval
56
56
  )
57
- fail_msg = (
58
- "Content App '{name}' failed to write a heartbeat to the database, sleeping for "
59
- "'{interarrival}' seconds."
60
- ).format(name=name, interarrival=heartbeat_interval)
57
+ fail_msg = ("Content App '{name}' failed to write a heartbeat to the database.").format(
58
+ name=name
59
+ )
61
60
  versions = {app.label: app.version for app in pulp_plugin_configs()}
62
61
 
62
+ try:
63
+ app_status = await AppStatus.objects.acreate(
64
+ name=name, app_type="content", versions=versions
65
+ )
66
+ except IntegrityError:
67
+ log.error(f"A content app with name {name} already exists in the database.")
68
+ exit(Arbiter.WORKER_BOOT_ERROR)
63
69
  try:
64
70
  while True:
71
+ await asyncio.sleep(heartbeat_interval)
65
72
  try:
66
- content_app_status, created = await ContentAppStatus.objects.aget_or_create(
67
- name=name, defaults={"versions": versions}
68
- )
69
-
70
- if not created:
71
- await sync_to_async(content_app_status.save_heartbeat)()
72
-
73
- if content_app_status.versions != versions:
74
- content_app_status.versions = versions
75
- await content_app_status.asave(update_fields=["versions"])
76
-
73
+ await app_status.asave_heartbeat()
77
74
  log.debug(msg)
78
75
  except (InterfaceError, DatabaseError):
79
76
  await sync_to_async(Handler._reset_db_connection)()
80
- log.info(fail_msg)
81
- await asyncio.sleep(heartbeat_interval)
77
+ try:
78
+ await app_status.asave_heartbeat()
79
+ log.debug(msg)
80
+ except (InterfaceError, DatabaseError):
81
+ log.error(fail_msg)
82
+ exit(Arbiter.WORKER_BOOT_ERROR)
82
83
  finally:
83
- if content_app_status:
84
- await content_app_status.adelete()
84
+ if app_status:
85
+ await app_status.adelete()
85
86
 
86
87
 
87
88
  async def _heartbeat_ctx(app):
@@ -705,8 +705,11 @@ class Handler:
705
705
  rel_path = rel_path[len(distro.base_path) :]
706
706
  rel_path = rel_path.lstrip("/")
707
707
 
708
- if rel_path == "" and not path.endswith("/"):
709
- # The root of a distribution base_path was requested without a slash
708
+ # Check if we need to redirect to add a trailing slash for distro root
709
+ if not path.endswith("/") and (
710
+ rel_path == "" # Distro root
711
+ or (distro.checkpoint and "/" not in rel_path) # Timestamped checkpoint root
712
+ ):
710
713
  raise HTTPMovedPermanently(f"{request.path}/")
711
714
 
712
715
  original_rel_path = rel_path
pulpcore/migrations.py CHANGED
@@ -3,6 +3,8 @@ from packaging.version import parse as parse_version
3
3
  from django.conf import settings
4
4
  from django.utils import timezone
5
5
  from django.db.migrations.operations.base import Operation
6
+ from django.db.models import F
7
+ from django.db.models.functions import Now
6
8
 
7
9
 
8
10
  class RequireVersion(Operation):
@@ -22,21 +24,41 @@ class RequireVersion(Operation):
22
24
 
23
25
  def database_forwards(self, app_label, schema_editor, from_state, to_state):
24
26
  from_state.clear_delayed_apps_cache()
25
- ApiAppStatus = from_state.apps.get_model("core", "ApiAppStatus")
26
- ContentAppStatus = from_state.apps.get_model("core", "ContentAppStatus")
27
- Worker = from_state.apps.get_model("core", "Worker")
28
27
 
29
28
  needed_version = parse_version(self.version)
30
29
  errors = []
30
+ found_either_table = False
31
31
 
32
- for worker_class, ttl, class_name in [
33
- (ApiAppStatus, settings.API_APP_TTL, "api server"),
34
- (ContentAppStatus, settings.CONTENT_APP_TTL, "content server"),
35
- (Worker, settings.WORKER_TTL, "pulp worker"),
36
- ]:
37
- for worker in worker_class.objects.filter(
38
- last_heartbeat__gte=timezone.now() - timezone.timedelta(seconds=ttl)
39
- ):
32
+ try:
33
+ ApiAppStatus = from_state.apps.get_model("core", "ApiAppStatus")
34
+ ContentAppStatus = from_state.apps.get_model("core", "ContentAppStatus")
35
+ Worker = from_state.apps.get_model("core", "Worker")
36
+ except LookupError:
37
+ pass
38
+ else:
39
+ for worker_class, ttl, class_name in [
40
+ (ApiAppStatus, settings.API_APP_TTL, "api server"),
41
+ (ContentAppStatus, settings.CONTENT_APP_TTL, "content server"),
42
+ (Worker, settings.WORKER_TTL, "pulp worker"),
43
+ ]:
44
+ for worker in worker_class.objects.filter(
45
+ last_heartbeat__gte=timezone.now() - timezone.timedelta(seconds=ttl)
46
+ ):
47
+ present_version = worker.versions.get(self.plugin)
48
+ if (
49
+ present_version is not None
50
+ and parse_version(present_version) < needed_version
51
+ ):
52
+ errors.append(
53
+ f" - '{self.plugin}'='{present_version}' "
54
+ f"with {class_name} '{worker.name}'"
55
+ )
56
+
57
+ found_either_table = True
58
+
59
+ try:
60
+ AppStatus = from_state.apps.get_model("core", "AppStatus")
61
+ for worker in AppStatus.objects.filter(F("last_heartbeat") + F("ttl") >= Now()):
40
62
  present_version = worker.versions.get(self.plugin)
41
63
  if present_version is not None and parse_version(present_version) < needed_version:
42
64
  errors.append(
@@ -44,6 +66,11 @@ class RequireVersion(Operation):
44
66
  f"with {class_name} '{worker.name}'"
45
67
  )
46
68
 
69
+ found_either_table = True
70
+ except LookupError:
71
+ pass
72
+
73
+ assert found_either_table
47
74
  if errors:
48
75
  raise RuntimeError(
49
76
  "\n".join(
@@ -468,6 +468,14 @@ class BasicAuthenticationScheme(OpenApiAuthenticationExtension):
468
468
  }
469
469
 
470
470
 
471
+ class PulpRemoteUserAuthenticationScheme(OpenApiAuthenticationExtension):
472
+ target_class = "pulpcore.app.authentication.PulpRemoteUserAuthentication"
473
+ name = "remoteUserAuthentication"
474
+
475
+ def get_security_definition(self, auto_schema):
476
+ return settings.REMOTE_USER_OPENAPI_SECURITY_SCHEME
477
+
478
+
471
479
  class JSONHeaderRemoteAuthenticationScheme(OpenApiAuthenticationExtension):
472
480
  target_class = "pulpcore.app.authentication.JSONHeaderRemoteAuthentication"
473
481
  name = "json_header_remote_authentication"
@@ -40,6 +40,7 @@ from pulpcore.app.models import (
40
40
  TaskGroup,
41
41
  Upload,
42
42
  UploadChunk,
43
+ VulnerabilityReport,
43
44
  )
44
45
 
45
46
 
@@ -87,4 +88,5 @@ __all__ = [
87
88
  "UploadChunk",
88
89
  "EncryptedTextField",
89
90
  "system_id",
91
+ "VulnerabilityReport",
90
92
  ]
@@ -38,6 +38,7 @@ from pulpcore.app.serializers import (
38
38
  ValidateFieldsMixin,
39
39
  validate_unknown_fields,
40
40
  TaskSerializer,
41
+ VulnerabilityReportSerializer,
41
42
  )
42
43
 
43
44
  from .content import (
@@ -87,4 +88,5 @@ __all__ = [
87
88
  "TaskSerializer",
88
89
  "NoArtifactContentUploadSerializer",
89
90
  "SingleArtifactContentUploadSerializer",
91
+ "VulnerabilityReportSerializer",
90
92
  ]
@@ -12,11 +12,13 @@ from pulpcore.app.tasks import (
12
12
  orphan_cleanup,
13
13
  reclaim_space,
14
14
  )
15
+ from pulpcore.app.tasks.vulnerability_report import check_content
15
16
  from pulpcore.app.tasks.repository import add_and_remove
16
17
 
17
18
 
18
19
  __all__ = [
19
20
  "ageneral_update",
21
+ "check_content",
20
22
  "dispatch",
21
23
  "fs_publication_export",
22
24
  "fs_repo_version_export",
@@ -33,6 +33,7 @@ from pulpcore.app.viewsets import (
33
33
  RolesMixin,
34
34
  TaskGroupViewSet,
35
35
  TaskViewSet,
36
+ VulnerabilityReportViewSet,
36
37
  )
37
38
 
38
39
  from pulpcore.app.viewsets.custom_filters import (
@@ -89,4 +90,5 @@ __all__ = [
89
90
  "NoArtifactContentViewSet",
90
91
  "NoArtifactContentUploadViewSet",
91
92
  "SingleArtifactContentUploadViewSet",
93
+ "VulnerabilityReportViewSet",
92
94
  ]
pulpcore/pytest_plugin.py CHANGED
@@ -608,28 +608,28 @@ def backend_settings_factory(pulp_settings):
608
608
  "bucket_name",
609
609
  ]
610
610
  keys["storages.backends.azure_storage.AzureStorage"] = [
611
- "AZURE_ACCOUNT_NAME",
612
- "AZURE_CONTAINER",
613
- "AZURE_ACCOUNT_KEY",
614
- "AZURE_URL_EXPIRATION_SECS",
615
- "AZURE_OVERWRITE_FILES",
616
- "AZURE_LOCATION",
617
- "AZURE_CONNECTION_STRING",
611
+ "account_name",
612
+ "azure_container",
613
+ "account_key",
614
+ "expiration_secs",
615
+ "overwrite_files",
616
+ "location",
617
+ "connection_string",
618
618
  ]
619
- settings = storage_settings or dict()
620
- backend = storage_class or pulp_settings.STORAGES["default"]["BACKEND"]
621
- not_defined_settings = (k for k in keys[backend] if k not in settings)
622
- # The CI configures s3 with STORAGES and Azure with legacy
623
- # Move all to STORAGES structure on DEFAULT_FILE_STORAGE removal
624
- if backend == "storages.backends.s3boto3.S3Boto3Storage":
625
- storages_dict = getattr(pulp_settings, "STORAGES", {})
626
- storage_options = storages_dict.get("default", {}).get("OPTIONS", {})
627
- for key in not_defined_settings:
628
- settings[key] = storage_options.get(key)
629
- else:
630
- for key in not_defined_settings:
631
- settings[key] = getattr(pulp_settings, key, None)
632
- return backend, settings
619
+
620
+ def get_installation_storage_option(key, backend):
621
+ value = pulp_settings.STORAGES["default"]["OPTIONS"].get(key)
622
+ # Some FileSystem backend options may be defined in the top settings module
623
+ if backend == "pulpcore.app.models.storage.FileSystem" and not value:
624
+ value = getattr(pulp_settings, key, None)
625
+ return value
626
+
627
+ storage_settings = storage_settings or dict()
628
+ storage_backend = storage_class or pulp_settings.STORAGES["default"]["BACKEND"]
629
+ unset_storage_settings = (k for k in keys[storage_backend] if k not in storage_settings)
630
+ for key in unset_storage_settings:
631
+ storage_settings[key] = get_installation_storage_option(key, storage_backend)
632
+ return storage_backend, storage_settings
633
633
 
634
634
  return _settings_factory
635
635
 
@@ -21,8 +21,18 @@ _logger = logging.getLogger(__name__)
21
21
  @click.option(
22
22
  "--reload/--no-reload", help="Reload worker on code changes. [requires hupper to be installed.]"
23
23
  )
24
+ @click.option(
25
+ "--auxiliary/--no-auxiliary",
26
+ default=False,
27
+ help="Auxiliary workers do not perform housekeeping duties.",
28
+ )
24
29
  @click.command()
25
- def worker(pid, burst, reload):
30
+ def worker(
31
+ pid,
32
+ burst,
33
+ reload,
34
+ auxiliary,
35
+ ):
26
36
  """A Pulp worker."""
27
37
 
28
38
  if reload:
@@ -40,4 +50,4 @@ def worker(pid, burst, reload):
40
50
 
41
51
  _logger.info("Starting distributed type worker")
42
52
 
43
- PulpcoreWorker().run(burst=burst)
53
+ PulpcoreWorker(auxiliary=auxiliary).run(burst=burst)
pulpcore/tasking/tasks.py CHANGED
@@ -9,12 +9,11 @@ import traceback
9
9
  import tempfile
10
10
  import threading
11
11
  from asgiref.sync import sync_to_async
12
- from datetime import timedelta
13
12
  from gettext import gettext as _
14
13
 
15
14
  from django.conf import settings
16
15
  from django.db import connection, transaction
17
- from django.db.models import Model, Max
16
+ from django.db.models import Model
18
17
  from django_guid import get_guid
19
18
  from pulpcore.app.apps import MODULE_PLUGIN_VERSIONS
20
19
  from pulpcore.app.models import Task, TaskGroup
@@ -23,8 +22,8 @@ from pulpcore.constants import (
23
22
  TASK_FINAL_STATES,
24
23
  TASK_INCOMPLETE_STATES,
25
24
  TASK_STATES,
26
- TASK_DISPATCH_LOCK,
27
25
  IMMEDIATE_TIMEOUT,
26
+ TASK_WAKEUP_UNBLOCK,
28
27
  )
29
28
  from pulpcore.middleware import x_task_diagnostics_var
30
29
  from pulpcore.tasking.kafka import send_task_notification
@@ -47,10 +46,10 @@ def _validate_and_get_resources(resources):
47
46
  return list(resource_set)
48
47
 
49
48
 
50
- def wakeup_worker():
49
+ def wakeup_worker(reason="unknown"):
51
50
  # Notify workers
52
51
  with connection.connection.cursor() as cursor:
53
- cursor.execute("NOTIFY pulp_worker_wakeup")
52
+ cursor.execute("SELECT pg_notify('pulp_worker_wakeup', %s)", (reason,))
54
53
 
55
54
 
56
55
  def execute_task(task):
@@ -228,13 +227,6 @@ def dispatch(
228
227
  notify_workers = False
229
228
  with contextlib.ExitStack() as stack:
230
229
  with transaction.atomic():
231
- # Task creation need to be serialized so that pulp_created will provide a stable order
232
- # at every time. We specifically need to ensure that each task, when committed to the
233
- # task table will be the newest with respect to `pulp_created`.
234
- with connection.cursor() as cursor:
235
- # Wait for exclusive access and release automatically after transaction.
236
- cursor.execute("SELECT pg_advisory_xact_lock(%s, %s)", [0, TASK_DISPATCH_LOCK])
237
- newest_created = Task.objects.aggregate(Max("pulp_created"))["pulp_created__max"]
238
230
  task = Task.objects.create(
239
231
  state=TASK_STATES.WAITING,
240
232
  logging_cid=(get_guid()),
@@ -250,23 +242,6 @@ def dispatch(
250
242
  profile_options=x_task_diagnostics_var.get(None),
251
243
  )
252
244
  task.refresh_from_db() # The database may have assigned a timestamp for us.
253
- if newest_created and task.pulp_created <= newest_created:
254
- # Let this workaround not row forever into the future.
255
- if newest_created - task.pulp_created > timedelta(seconds=1):
256
- # Do not commit the transaction if this condition is not met.
257
- # If we ever hit this, think about delegating the timestamping to PostgresQL.
258
- raise RuntimeError("Clockscrew detected. Task dispatching would be dangerous.")
259
- # Try to work around the smaller glitch
260
- task.pulp_created = newest_created + timedelta(milliseconds=1)
261
- task.save()
262
- if task_group:
263
- task_group.refresh_from_db()
264
- if task_group.all_tasks_dispatched:
265
- task.set_canceling()
266
- task.set_canceled(
267
- TASK_STATES.CANCELED, "All tasks in group have been dispatched/canceled."
268
- )
269
- return task
270
245
  if immediate:
271
246
  # Grab the advisory lock before the task hits the db.
272
247
  stack.enter_context(task)
@@ -308,7 +283,7 @@ def dispatch(
308
283
  task.set_canceling()
309
284
  task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
310
285
  if notify_workers:
311
- wakeup_worker()
286
+ wakeup_worker(TASK_WAKEUP_UNBLOCK)
312
287
  return task
313
288
 
314
289