pulpcore 3.89.1__py3-none-any.whl → 3.90.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.
- pulp_certguard/app/__init__.py +1 -1
- pulp_file/app/__init__.py +1 -1
- pulp_file/tests/functional/api/test_filesystem_export.py +220 -0
- pulp_file/tests/functional/api/test_pulp_export.py +103 -3
- pulpcore/app/apps.py +1 -1
- pulpcore/app/importexport.py +18 -2
- pulpcore/app/management/commands/shell.py +8 -0
- pulpcore/app/migrations/0144_delete_old_appstatus.py +28 -0
- pulpcore/app/migrations/0145_domainize_import_export.py +53 -0
- pulpcore/app/modelresource.py +61 -21
- pulpcore/app/models/__init__.py +2 -5
- pulpcore/app/models/exporter.py +7 -1
- pulpcore/app/models/fields.py +0 -1
- pulpcore/app/models/importer.py +8 -1
- pulpcore/app/models/repository.py +16 -0
- pulpcore/app/models/status.py +8 -138
- pulpcore/app/models/task.py +15 -25
- pulpcore/app/serializers/domain.py +1 -1
- pulpcore/app/serializers/exporter.py +4 -4
- pulpcore/app/serializers/importer.py +2 -2
- pulpcore/app/serializers/task.py +11 -8
- pulpcore/app/tasks/importer.py +44 -10
- pulpcore/app/tasks/repository.py +27 -0
- pulpcore/app/viewsets/base.py +18 -14
- pulpcore/app/viewsets/domain.py +1 -1
- pulpcore/app/viewsets/exporter.py +1 -8
- pulpcore/app/viewsets/importer.py +1 -6
- pulpcore/app/viewsets/task.py +0 -1
- pulpcore/openapi/__init__.py +16 -2
- pulpcore/plugin/tasking.py +4 -2
- pulpcore/tasking/tasks.py +245 -127
- pulpcore/tasking/worker.py +6 -17
- pulpcore/tests/functional/api/test_crud_domains.py +7 -0
- pulpcore/tests/functional/api/test_tasking.py +2 -2
- pulpcore/tests/functional/api/using_plugin/test_crud_repos.py +9 -2
- pulpcore/tests/unit/content/test_handler.py +43 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/METADATA +7 -7
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/RECORD +42 -38
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/WHEEL +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/entry_points.txt +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/licenses/LICENSE +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/top_level.txt +0 -0
pulpcore/app/models/__init__.py
CHANGED
|
@@ -69,7 +69,7 @@ from .repository import (
|
|
|
69
69
|
RepositoryVersionContentDetails,
|
|
70
70
|
)
|
|
71
71
|
|
|
72
|
-
from .status import AppStatus
|
|
72
|
+
from .status import AppStatus
|
|
73
73
|
|
|
74
74
|
from .task import (
|
|
75
75
|
CreatedResource,
|
|
@@ -77,7 +77,6 @@ from .task import (
|
|
|
77
77
|
Task,
|
|
78
78
|
TaskGroup,
|
|
79
79
|
TaskSchedule,
|
|
80
|
-
Worker,
|
|
81
80
|
)
|
|
82
81
|
|
|
83
82
|
from .analytics import SystemID
|
|
@@ -106,6 +105,7 @@ from .openpgp import (
|
|
|
106
105
|
)
|
|
107
106
|
|
|
108
107
|
__all__ = [
|
|
108
|
+
"AppStatus",
|
|
109
109
|
"BaseModel",
|
|
110
110
|
"MasterModel",
|
|
111
111
|
"pulp_uuid",
|
|
@@ -151,14 +151,11 @@ __all__ = [
|
|
|
151
151
|
"RepositoryContent",
|
|
152
152
|
"RepositoryVersion",
|
|
153
153
|
"RepositoryVersionContentDetails",
|
|
154
|
-
"ApiAppStatus",
|
|
155
|
-
"ContentAppStatus",
|
|
156
154
|
"CreatedResource",
|
|
157
155
|
"ProfileArtifact",
|
|
158
156
|
"Task",
|
|
159
157
|
"TaskGroup",
|
|
160
158
|
"TaskSchedule",
|
|
161
|
-
"Worker",
|
|
162
159
|
"SystemID",
|
|
163
160
|
"Upload",
|
|
164
161
|
"UploadChunk",
|
pulpcore/app/models/exporter.py
CHANGED
|
@@ -9,6 +9,7 @@ from pulpcore.app.models import (
|
|
|
9
9
|
MasterModel,
|
|
10
10
|
)
|
|
11
11
|
from pulpcore.app.models.repository import Repository
|
|
12
|
+
from pulpcore.app.util import get_domain_pk
|
|
12
13
|
from pulpcore.constants import FS_EXPORT_CHOICES, FS_EXPORT_METHODS
|
|
13
14
|
|
|
14
15
|
|
|
@@ -29,6 +30,7 @@ class Export(BaseModel):
|
|
|
29
30
|
params = models.JSONField(null=True)
|
|
30
31
|
task = models.ForeignKey("Task", on_delete=models.SET_NULL, null=True)
|
|
31
32
|
exporter = models.ForeignKey("Exporter", on_delete=models.CASCADE)
|
|
33
|
+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class ExportedResource(GenericRelationModel):
|
|
@@ -54,7 +56,11 @@ class Exporter(MasterModel):
|
|
|
54
56
|
name (models.TextField): The exporter unique name.
|
|
55
57
|
"""
|
|
56
58
|
|
|
57
|
-
name = models.TextField(
|
|
59
|
+
name = models.TextField()
|
|
60
|
+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
61
|
+
|
|
62
|
+
class Meta:
|
|
63
|
+
unique_together = ("name", "pulp_domain")
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
class FilesystemExport(Export):
|
pulpcore/app/models/fields.py
CHANGED
pulpcore/app/models/importer.py
CHANGED
|
@@ -5,6 +5,8 @@ from pulpcore.app.models import (
|
|
|
5
5
|
BaseModel,
|
|
6
6
|
MasterModel,
|
|
7
7
|
)
|
|
8
|
+
from pulpcore.app.util import get_domain_pk
|
|
9
|
+
|
|
8
10
|
from .repository import Repository
|
|
9
11
|
|
|
10
12
|
|
|
@@ -23,6 +25,7 @@ class Import(BaseModel):
|
|
|
23
25
|
params = models.JSONField(null=True)
|
|
24
26
|
task = models.ForeignKey("Task", on_delete=models.PROTECT)
|
|
25
27
|
importer = models.ForeignKey("Importer", on_delete=models.CASCADE)
|
|
28
|
+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class Importer(MasterModel):
|
|
@@ -35,7 +38,11 @@ class Importer(MasterModel):
|
|
|
35
38
|
name (models.TextField): The importer unique name.
|
|
36
39
|
"""
|
|
37
40
|
|
|
38
|
-
name = models.TextField(
|
|
41
|
+
name = models.TextField()
|
|
42
|
+
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
unique_together = ("name", "pulp_domain")
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
class PulpImporter(Importer):
|
|
@@ -381,6 +381,21 @@ class Repository(MasterModel):
|
|
|
381
381
|
body = {"repository_pk": self.pk, "add_content_units": [cpk], "remove_content_units": []}
|
|
382
382
|
return dispatch(add_and_remove, kwargs=body, exclusive_resources=[self], immediate=True)
|
|
383
383
|
|
|
384
|
+
async def async_pull_through_add_content(self, content_artifact):
|
|
385
|
+
cpk = content_artifact.content_id
|
|
386
|
+
already_present = RepositoryContent.objects.filter(
|
|
387
|
+
content__pk=cpk, repository=self, version_removed__isnull=True
|
|
388
|
+
)
|
|
389
|
+
if not cpk or await already_present.aexists():
|
|
390
|
+
return None
|
|
391
|
+
|
|
392
|
+
from pulpcore.plugin.tasking import adispatch, aadd_and_remove
|
|
393
|
+
|
|
394
|
+
body = {"repository_pk": self.pk, "add_content_units": [cpk], "remove_content_units": []}
|
|
395
|
+
return await adispatch(
|
|
396
|
+
aadd_and_remove, kwargs=body, exclusive_resources=[self], immediate=True
|
|
397
|
+
)
|
|
398
|
+
|
|
384
399
|
@hook(AFTER_UPDATE, when="retain_repo_versions", has_changed=True)
|
|
385
400
|
def _cleanup_old_versions_hook(self):
|
|
386
401
|
# Do not attempt to clean up anything, while there is a transaction involving repo versions
|
|
@@ -1105,6 +1120,7 @@ class RepositoryVersion(BaseModel):
|
|
|
1105
1120
|
|
|
1106
1121
|
if not content or not content.count():
|
|
1107
1122
|
return
|
|
1123
|
+
|
|
1108
1124
|
assert (
|
|
1109
1125
|
not Content.objects.filter(pk__in=content)
|
|
1110
1126
|
.exclude(pulp_domain_id=get_domain_pk())
|
pulpcore/app/models/status.py
CHANGED
|
@@ -15,44 +15,12 @@ from pulpcore.app.models import BaseModel
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class AppStatusManager(models.Manager):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
To be considered 'online', a AppStatus must have a heartbeat timestamp within
|
|
24
|
-
``self.model.APP_TTL`` from now.
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
[django.db.models.query.QuerySet][]: A query set of the
|
|
28
|
-
objects which are considered 'online'.
|
|
29
|
-
"""
|
|
30
|
-
age_threshold = timezone.now() - self.model.APP_TTL
|
|
31
|
-
return self.filter(last_heartbeat__gte=age_threshold)
|
|
32
|
-
|
|
33
|
-
def missing(self, age=None):
|
|
34
|
-
"""
|
|
35
|
-
Returns a queryset of workers meeting the criteria to be considered 'missing'
|
|
36
|
-
|
|
37
|
-
To be considered missing, a AppsStatus must have a stale timestamp. By default, stale is
|
|
38
|
-
defined here as longer than the ``self.model.APP_TTL``, or you can specify age as a
|
|
39
|
-
timedelta.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
age (datetime.timedelta): Objects who have heartbeats older than this time interval are
|
|
43
|
-
considered missing.
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
[django.db.models.query.QuerySet][]: A query set of the objects objects which
|
|
47
|
-
are considered to be 'missing'.
|
|
48
|
-
"""
|
|
49
|
-
age_threshold = timezone.now() - (age or self.model.APP_TTL)
|
|
50
|
-
return self.filter(last_heartbeat__lt=age_threshold)
|
|
51
|
-
|
|
18
|
+
_APP_TTL = {
|
|
19
|
+
"api": settings.API_APP_TTL,
|
|
20
|
+
"content": settings.CONTENT_APP_TTL,
|
|
21
|
+
"worker": settings.WORKER_TTL,
|
|
22
|
+
}
|
|
52
23
|
|
|
53
|
-
class _AppStatusManager(AppStatusManager):
|
|
54
|
-
# This is an intermediate class in order to allow a ZDU.
|
|
55
|
-
# It should be made the real thing with 3.87.
|
|
56
24
|
def __init__(self):
|
|
57
25
|
super().__init__()
|
|
58
26
|
self._current_app_status = None
|
|
@@ -61,6 +29,7 @@ class _AppStatusManager(AppStatusManager):
|
|
|
61
29
|
if self._current_app_status is not None:
|
|
62
30
|
raise RuntimeError("There is already an app status in this process.")
|
|
63
31
|
|
|
32
|
+
kwargs.setdefault("ttl", timedelta(seconds=self._APP_TTL[app_type]))
|
|
64
33
|
obj = super().create(app_type=app_type, **kwargs)
|
|
65
34
|
self._current_app_status = obj
|
|
66
35
|
return obj
|
|
@@ -69,6 +38,7 @@ class _AppStatusManager(AppStatusManager):
|
|
|
69
38
|
if self._current_app_status is not None:
|
|
70
39
|
raise RuntimeError("There is already an app status in this process.")
|
|
71
40
|
|
|
41
|
+
kwargs.setdefault("ttl", timedelta(seconds=self._APP_TTL[app_type]))
|
|
72
42
|
obj = await super().acreate(app_type=app_type, **kwargs)
|
|
73
43
|
self._current_app_status = obj
|
|
74
44
|
return obj
|
|
@@ -103,12 +73,7 @@ class AppStatus(BaseModel):
|
|
|
103
73
|
("content", "content"),
|
|
104
74
|
("worker", "worker"),
|
|
105
75
|
]
|
|
106
|
-
|
|
107
|
-
"api": settings.API_APP_TTL,
|
|
108
|
-
"content": settings.CONTENT_APP_TTL,
|
|
109
|
-
"worker": settings.WORKER_TTL,
|
|
110
|
-
}
|
|
111
|
-
objects = _AppStatusManager()
|
|
76
|
+
objects = AppStatusManager()
|
|
112
77
|
|
|
113
78
|
app_type = models.CharField(max_length=10, choices=APP_TYPES)
|
|
114
79
|
name = models.TextField()
|
|
@@ -116,10 +81,6 @@ class AppStatus(BaseModel):
|
|
|
116
81
|
ttl = models.DurationField(null=False)
|
|
117
82
|
last_heartbeat = models.DateTimeField(auto_now=True)
|
|
118
83
|
|
|
119
|
-
def __init__(self, *args, **kwargs):
|
|
120
|
-
super().__init__(*args, **kwargs)
|
|
121
|
-
self.ttl = timedelta(seconds=self._APP_TTL[self.app_type])
|
|
122
|
-
|
|
123
84
|
@property
|
|
124
85
|
def online(self) -> bool:
|
|
125
86
|
"""
|
|
@@ -170,94 +131,3 @@ class AppStatus(BaseModel):
|
|
|
170
131
|
The task this worker is currently executing, if any.
|
|
171
132
|
"""
|
|
172
133
|
return self.tasks.filter(state="running").first()
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class BaseAppStatus(BaseModel):
|
|
176
|
-
"""
|
|
177
|
-
Represents an AppStatus.
|
|
178
|
-
Deprecated, to be removed with 3.87.
|
|
179
|
-
|
|
180
|
-
This class is abstract. Subclasses must define `APP_TTL` as a `timedelta`.
|
|
181
|
-
|
|
182
|
-
Fields:
|
|
183
|
-
|
|
184
|
-
name (models.TextField): The name of the app.
|
|
185
|
-
last_heartbeat (models.DateTimeField): A timestamp of this worker's last heartbeat.
|
|
186
|
-
versions (HStoreField): A dictionary with versions of all pulp components.
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
objects = AppStatusManager()
|
|
190
|
-
|
|
191
|
-
name = models.TextField(db_index=True, unique=True)
|
|
192
|
-
last_heartbeat = models.DateTimeField(auto_now=True)
|
|
193
|
-
versions = HStoreField(default=dict)
|
|
194
|
-
|
|
195
|
-
@property
|
|
196
|
-
def online(self):
|
|
197
|
-
"""
|
|
198
|
-
Whether an app can be considered 'online'
|
|
199
|
-
|
|
200
|
-
To be considered 'online', an app must have a timestamp more recent than ``self.APP_TTL``.
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
bool: True if the app is considered online, otherwise False
|
|
204
|
-
"""
|
|
205
|
-
age_threshold = timezone.now() - self.APP_TTL
|
|
206
|
-
return self.last_heartbeat >= age_threshold
|
|
207
|
-
|
|
208
|
-
@property
|
|
209
|
-
def missing(self):
|
|
210
|
-
"""
|
|
211
|
-
Whether an app can be considered 'missing'
|
|
212
|
-
|
|
213
|
-
To be considered 'missing', an App must have a timestamp older than ``self.APP_TTL``.
|
|
214
|
-
|
|
215
|
-
Returns:
|
|
216
|
-
bool: True if the app is considered missing, otherwise False
|
|
217
|
-
"""
|
|
218
|
-
return not self.online
|
|
219
|
-
|
|
220
|
-
def save_heartbeat(self):
|
|
221
|
-
"""
|
|
222
|
-
Update the last_heartbeat field to now and save it.
|
|
223
|
-
|
|
224
|
-
Only the last_heartbeat field will be saved. No other changes will be saved.
|
|
225
|
-
|
|
226
|
-
Raises:
|
|
227
|
-
ValueError: When the model instance has never been saved before. This method can
|
|
228
|
-
only update an existing database record.
|
|
229
|
-
"""
|
|
230
|
-
self.save(update_fields=["last_heartbeat"])
|
|
231
|
-
|
|
232
|
-
async def asave_heartbeat(self):
|
|
233
|
-
"""
|
|
234
|
-
Update the last_heartbeat field to now and save it.
|
|
235
|
-
|
|
236
|
-
Only the last_heartbeat field will be saved. No other changes will be saved.
|
|
237
|
-
|
|
238
|
-
Raises:
|
|
239
|
-
ValueError: When the model instance has never been saved before. This method can
|
|
240
|
-
only update an existing database record.
|
|
241
|
-
"""
|
|
242
|
-
await self.asave(update_fields=["last_heartbeat"])
|
|
243
|
-
|
|
244
|
-
class Meta:
|
|
245
|
-
abstract = True
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
class ApiAppStatus(BaseAppStatus):
|
|
249
|
-
"""
|
|
250
|
-
Represents a Api App Status
|
|
251
|
-
Deprecated, to be removed with 3.87.
|
|
252
|
-
"""
|
|
253
|
-
|
|
254
|
-
APP_TTL = timedelta(seconds=settings.API_APP_TTL)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
class ContentAppStatus(BaseAppStatus):
|
|
258
|
-
"""
|
|
259
|
-
Represents a Content App Status
|
|
260
|
-
Deprecated, to be removed with 3.87.
|
|
261
|
-
"""
|
|
262
|
-
|
|
263
|
-
APP_TTL = timedelta(seconds=settings.CONTENT_APP_TTL)
|
pulpcore/app/models/task.py
CHANGED
|
@@ -5,10 +5,8 @@ Django models related to the Tasking system
|
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import traceback
|
|
8
|
-
from datetime import timedelta
|
|
9
8
|
from gettext import gettext as _
|
|
10
9
|
|
|
11
|
-
from django.conf import settings
|
|
12
10
|
from django.contrib.postgres.fields import ArrayField, HStoreField
|
|
13
11
|
from django.contrib.postgres.indexes import GinIndex
|
|
14
12
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
@@ -21,7 +19,7 @@ from pulpcore.app.models import (
|
|
|
21
19
|
BaseModel,
|
|
22
20
|
GenericRelationModel,
|
|
23
21
|
)
|
|
24
|
-
from pulpcore.app.models.status import AppStatus
|
|
22
|
+
from pulpcore.app.models.status import AppStatus
|
|
25
23
|
from pulpcore.app.models.fields import EncryptedJSONField
|
|
26
24
|
from pulpcore.constants import TASK_CHOICES, TASK_INCOMPLETE_STATES, TASK_STATES
|
|
27
25
|
from pulpcore.exceptions import exception_to_dict
|
|
@@ -31,25 +29,6 @@ from pulpcore.app.loggers import deprecation_logger
|
|
|
31
29
|
_logger = logging.getLogger(__name__)
|
|
32
30
|
|
|
33
31
|
|
|
34
|
-
class Worker(BaseAppStatus):
|
|
35
|
-
"""
|
|
36
|
-
Represents a worker
|
|
37
|
-
Deprecated, to be removed with 3.87.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
APP_TTL = timedelta(seconds=settings.WORKER_TTL)
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def current_task(self):
|
|
44
|
-
"""
|
|
45
|
-
The task this worker is currently executing, if any.
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
Task: The currently executing task
|
|
49
|
-
"""
|
|
50
|
-
return self.tasks.filter(state="running").first()
|
|
51
|
-
|
|
52
|
-
|
|
53
32
|
def _uuid_to_advisory_lock(value):
|
|
54
33
|
return ((value >> 64) ^ value) & 0x7FFFFFFFFFFFFFFF
|
|
55
34
|
|
|
@@ -140,7 +119,6 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
140
119
|
enc_args = EncryptedJSONField(null=True, encoder=DjangoJSONEncoder)
|
|
141
120
|
enc_kwargs = EncryptedJSONField(null=True, encoder=DjangoJSONEncoder)
|
|
142
121
|
|
|
143
|
-
worker = models.ForeignKey("Worker", null=True, related_name="tasks", on_delete=models.SET_NULL)
|
|
144
122
|
# This field is the lock to protect tasks.
|
|
145
123
|
app_lock = models.ForeignKey(
|
|
146
124
|
"AppStatus", null=True, related_name="tasks", on_delete=models.SET_NULL
|
|
@@ -363,8 +341,20 @@ class Task(BaseModel, AutoAddObjPermsMixin):
|
|
|
363
341
|
self.unblocked_at = unblocked_at
|
|
364
342
|
else:
|
|
365
343
|
self.refresh_from_db()
|
|
366
|
-
raise RuntimeError(
|
|
367
|
-
|
|
344
|
+
raise RuntimeError("Failed to set task {} unblocked in state '{}'.").format(
|
|
345
|
+
self.pk, self.state
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
async def aunblock(self):
|
|
349
|
+
# This should be safe to be called without holding the lock.
|
|
350
|
+
unblocked_at = timezone.now()
|
|
351
|
+
rows = await Task.objects.filter(pk=self.pk).aupdate(unblocked_at=unblocked_at)
|
|
352
|
+
if rows == 1:
|
|
353
|
+
self.unblocked_at = unblocked_at
|
|
354
|
+
else:
|
|
355
|
+
await self.arefresh_from_db()
|
|
356
|
+
raise RuntimeError("Failed to set task {} unblocked in state '{}'.").format(
|
|
357
|
+
self.pk, self.state
|
|
368
358
|
)
|
|
369
359
|
|
|
370
360
|
class Meta:
|
|
@@ -2,7 +2,7 @@ from gettext import gettext as _
|
|
|
2
2
|
import json
|
|
3
3
|
|
|
4
4
|
from django.conf import settings
|
|
5
|
-
from django.
|
|
5
|
+
from django.utils.module_loading import import_string
|
|
6
6
|
from django.core.exceptions import ImproperlyConfigured
|
|
7
7
|
from drf_spectacular.types import OpenApiTypes
|
|
8
8
|
from drf_spectacular.utils import extend_schema_field
|
|
@@ -3,12 +3,12 @@ from gettext import gettext as _
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
5
|
from rest_framework import serializers
|
|
6
|
-
from rest_framework.validators import UniqueValidator
|
|
7
6
|
|
|
8
7
|
from pulpcore.app import models, settings
|
|
9
8
|
from pulpcore.app.serializers import (
|
|
10
9
|
DetailIdentityField,
|
|
11
10
|
DetailRelatedField,
|
|
11
|
+
DomainUniqueValidator,
|
|
12
12
|
ExportIdentityField,
|
|
13
13
|
ExportRelatedField,
|
|
14
14
|
ModelSerializer,
|
|
@@ -37,8 +37,8 @@ class ExporterSerializer(ModelSerializer):
|
|
|
37
37
|
|
|
38
38
|
pulp_href = DetailIdentityField(view_name_pattern=r"exporter(-.*/.*)-detail")
|
|
39
39
|
name = serializers.CharField(
|
|
40
|
-
help_text=_("Unique name of the
|
|
41
|
-
validators=[
|
|
40
|
+
help_text=_("Unique name of the exporter."),
|
|
41
|
+
validators=[DomainUniqueValidator(queryset=models.Exporter.objects.all())],
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
@staticmethod
|
|
@@ -308,7 +308,7 @@ class FilesystemExportSerializer(ExportSerializer):
|
|
|
308
308
|
raise serializers.ValidationError(
|
|
309
309
|
_("publication or repository_version must either be supplied but not both.")
|
|
310
310
|
)
|
|
311
|
-
return data
|
|
311
|
+
return super().validate(data)
|
|
312
312
|
|
|
313
313
|
class Meta:
|
|
314
314
|
model = models.FilesystemExport
|
|
@@ -3,11 +3,11 @@ from gettext import gettext as _
|
|
|
3
3
|
|
|
4
4
|
from django.core.exceptions import ObjectDoesNotExist
|
|
5
5
|
from rest_framework import serializers
|
|
6
|
-
from rest_framework.validators import UniqueValidator
|
|
7
6
|
|
|
8
7
|
from pulpcore.app import models, settings
|
|
9
8
|
from pulpcore.app.serializers import (
|
|
10
9
|
DetailIdentityField,
|
|
10
|
+
DomainUniqueValidator,
|
|
11
11
|
ImportIdentityField,
|
|
12
12
|
ModelSerializer,
|
|
13
13
|
RelatedField,
|
|
@@ -22,7 +22,7 @@ class ImporterSerializer(ModelSerializer):
|
|
|
22
22
|
pulp_href = DetailIdentityField(view_name_pattern=r"importer(-.*/.*)-detail")
|
|
23
23
|
name = serializers.CharField(
|
|
24
24
|
help_text=_("Unique name of the Importer."),
|
|
25
|
-
validators=[
|
|
25
|
+
validators=[DomainUniqueValidator(queryset=models.Importer.objects.all())],
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
class Meta:
|
pulpcore/app/serializers/task.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import typing as t
|
|
1
2
|
from gettext import gettext as _
|
|
2
3
|
|
|
3
4
|
from rest_framework import serializers
|
|
5
|
+
from drf_spectacular.utils import extend_schema_serializer
|
|
6
|
+
from drf_spectacular.types import OpenApiTypes
|
|
4
7
|
|
|
5
8
|
from pulpcore.app import models
|
|
6
9
|
from pulpcore.app.serializers import (
|
|
@@ -52,13 +55,9 @@ class TaskSerializer(ModelSerializer):
|
|
|
52
55
|
),
|
|
53
56
|
read_only=True,
|
|
54
57
|
)
|
|
55
|
-
worker =
|
|
56
|
-
help_text=_(
|
|
57
|
-
"The worker associated with this task."
|
|
58
|
-
" This field is empty if a worker is not yet assigned."
|
|
59
|
-
),
|
|
58
|
+
worker = serializers.SerializerMethodField(
|
|
59
|
+
help_text=_("DEPRECATED - Always null"),
|
|
60
60
|
read_only=True,
|
|
61
|
-
view_name="workers-detail",
|
|
62
61
|
)
|
|
63
62
|
parent_task = RelatedField(
|
|
64
63
|
help_text=_("The parent task that spawned this task."),
|
|
@@ -93,7 +92,10 @@ class TaskSerializer(ModelSerializer):
|
|
|
93
92
|
help_text=_("The result of this task."),
|
|
94
93
|
)
|
|
95
94
|
|
|
96
|
-
def
|
|
95
|
+
def get_worker(self, obj) -> t.Optional[OpenApiTypes.URI]:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def get_created_by(self, obj) -> t.Optional[OpenApiTypes.URI]:
|
|
97
99
|
if task_user_map := self.context.get("task_user_mapping"):
|
|
98
100
|
if user_id := task_user_map.get(str(obj.pk)):
|
|
99
101
|
kwargs = {"pk": user_id}
|
|
@@ -136,11 +138,12 @@ class MinimalTaskSerializer(TaskSerializer):
|
|
|
136
138
|
)
|
|
137
139
|
|
|
138
140
|
|
|
141
|
+
@extend_schema_serializer(deprecate_fields=["all_tasks_dispatched"])
|
|
139
142
|
class TaskGroupSerializer(ModelSerializer):
|
|
140
143
|
pulp_href = IdentityField(view_name="task-groups-detail")
|
|
141
144
|
description = serializers.CharField(help_text=_("A description of the task group."))
|
|
142
145
|
all_tasks_dispatched = serializers.BooleanField(
|
|
143
|
-
help_text=_("Whether all tasks have been spawned for this task group.")
|
|
146
|
+
help_text=_("Whether all tasks have been spawned for this task group."),
|
|
144
147
|
)
|
|
145
148
|
|
|
146
149
|
waiting = TaskGroupStatusCountField(
|
pulpcore/app/tasks/importer.py
CHANGED
|
@@ -35,7 +35,12 @@ from pulpcore.app.modelresource import (
|
|
|
35
35
|
ContentArtifactResource,
|
|
36
36
|
RepositoryResource,
|
|
37
37
|
)
|
|
38
|
-
from pulpcore.app.util import
|
|
38
|
+
from pulpcore.app.util import (
|
|
39
|
+
compute_file_hash,
|
|
40
|
+
Crc32Hasher,
|
|
41
|
+
get_domain,
|
|
42
|
+
get_domain_pk,
|
|
43
|
+
)
|
|
39
44
|
from pulpcore.constants import TASK_STATES
|
|
40
45
|
from pulpcore.tasking.tasks import dispatch
|
|
41
46
|
|
|
@@ -435,7 +440,6 @@ def pulp_import(importer_pk, path, toc, create_repositories):
|
|
|
435
440
|
create_repositories (bool): Indicates whether missing repositories should be automatically
|
|
436
441
|
created or not.
|
|
437
442
|
"""
|
|
438
|
-
|
|
439
443
|
if toc:
|
|
440
444
|
path = toc
|
|
441
445
|
fileobj = ChunkedFile(toc)
|
|
@@ -443,13 +447,12 @@ def pulp_import(importer_pk, path, toc, create_repositories):
|
|
|
443
447
|
fileobj.validate_chunks()
|
|
444
448
|
else:
|
|
445
449
|
fileobj = nullcontext()
|
|
446
|
-
|
|
447
450
|
log.info(_("Importing {}.").format(path))
|
|
448
451
|
current_task = Task.current()
|
|
449
452
|
task_group = TaskGroup.current()
|
|
450
453
|
importer = PulpImporter.objects.get(pk=importer_pk)
|
|
451
454
|
the_import = PulpImport.objects.create(
|
|
452
|
-
importer=importer, task=current_task, params={"path": path}
|
|
455
|
+
importer=importer, task=current_task, params={"path": path}, pulp_domain=get_domain()
|
|
453
456
|
)
|
|
454
457
|
CreatedResource.objects.create(content_object=the_import)
|
|
455
458
|
|
|
@@ -485,18 +488,47 @@ def pulp_import(importer_pk, path, toc, create_repositories):
|
|
|
485
488
|
message="Importing Artifacts",
|
|
486
489
|
code="import.artifacts",
|
|
487
490
|
)
|
|
491
|
+
into_default = "default" == get_domain().name
|
|
488
492
|
with ProgressReport(**data) as pb:
|
|
489
493
|
# Import artifacts, and place their binary blobs, one batch at a time.
|
|
490
494
|
# Skip artifacts that already exist in storage.
|
|
491
495
|
for ar_result in _import_file(os.path.join(temp_dir, ARTIFACT_FILE), ArtifactResource):
|
|
492
496
|
for row in pb.iter(ar_result.rows):
|
|
493
497
|
artifact = Artifact.objects.get(pk=row.object_id)
|
|
494
|
-
base_path = os.path.join("artifact", artifact.sha256[0:2], artifact.sha256[2:])
|
|
495
|
-
src = os.path.join(temp_dir, base_path)
|
|
496
498
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
499
|
+
# If we are domain-enabled, and not importing into "default", then the
|
|
500
|
+
# destination is "to the current domain's artifact directory". Otherwise, it's
|
|
501
|
+
# just to /artifact/.
|
|
502
|
+
if settings.DOMAIN_ENABLED and not into_default:
|
|
503
|
+
destination_path = os.path.join(
|
|
504
|
+
"artifact",
|
|
505
|
+
str(get_domain_pk()),
|
|
506
|
+
artifact.sha256[0:2],
|
|
507
|
+
artifact.sha256[2:],
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
else:
|
|
511
|
+
destination_path = os.path.join(
|
|
512
|
+
"artifact", artifact.sha256[0:2], artifact.sha256[2:]
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# If *the upstream* was domain-enabled, the tarfile will have artifact/DOMAIN/
|
|
516
|
+
# in its path, and the Artifact will have artifact/current-domain-id in its
|
|
517
|
+
# "file" attribute. We need to copy from artifact/DOMAIN/ in the tarfile.
|
|
518
|
+
domain_path = os.path.join(
|
|
519
|
+
"artifact", "DOMAIN", artifact.sha256[0:2], artifact.sha256[2:]
|
|
520
|
+
)
|
|
521
|
+
if os.path.exists(os.path.join(temp_dir, domain_path)):
|
|
522
|
+
tar_path = domain_path
|
|
523
|
+
else:
|
|
524
|
+
tar_path = os.path.join(
|
|
525
|
+
"artifact", artifact.sha256[0:2], artifact.sha256[2:]
|
|
526
|
+
)
|
|
527
|
+
src_in_tar = os.path.join(temp_dir, tar_path)
|
|
528
|
+
|
|
529
|
+
if not default_storage.exists(destination_path):
|
|
530
|
+
with open(src_in_tar, "rb") as f:
|
|
531
|
+
default_storage.save(destination_path, f)
|
|
500
532
|
|
|
501
533
|
# Now import repositories, in parallel.
|
|
502
534
|
|
|
@@ -529,7 +561,9 @@ def pulp_import(importer_pk, path, toc, create_repositories):
|
|
|
529
561
|
worker_rsrc = f"import-worker-{index % import_workers}"
|
|
530
562
|
exclusive_resources = [worker_rsrc]
|
|
531
563
|
try:
|
|
532
|
-
dest_repo = Repository.objects.get(
|
|
564
|
+
dest_repo = Repository.objects.get(
|
|
565
|
+
name=dest_repo_name, pulp_domain=get_domain()
|
|
566
|
+
)
|
|
533
567
|
except Repository.DoesNotExist:
|
|
534
568
|
if create_repositories:
|
|
535
569
|
dest_repo_pk = ""
|
pulpcore/app/tasks/repository.py
CHANGED
|
@@ -236,3 +236,30 @@ def add_and_remove(repository_pk, add_content_units, remove_content_units, base_
|
|
|
236
236
|
with repository.new_version(base_version=base_version) as new_version:
|
|
237
237
|
new_version.remove_content(models.Content.objects.filter(pk__in=remove_content_units))
|
|
238
238
|
new_version.add_content(models.Content.objects.filter(pk__in=add_content_units))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def aadd_and_remove(
|
|
242
|
+
repository_pk, add_content_units, remove_content_units, base_version_pk=None
|
|
243
|
+
):
|
|
244
|
+
"""Aynsc version of add_and_remove."""
|
|
245
|
+
repository = await models.Repository.objects.aget(pk=repository_pk)
|
|
246
|
+
repository = await repository.acast()
|
|
247
|
+
|
|
248
|
+
if base_version_pk:
|
|
249
|
+
base_version = await models.RepositoryVersion.objects.aget(pk=base_version_pk)
|
|
250
|
+
else:
|
|
251
|
+
base_version = None
|
|
252
|
+
|
|
253
|
+
if "*" in remove_content_units:
|
|
254
|
+
latest = await repository.alatest_version()
|
|
255
|
+
if latest:
|
|
256
|
+
remove_content_units = latest.content.values_list("pk", flat=True)
|
|
257
|
+
else:
|
|
258
|
+
remove_content_units = []
|
|
259
|
+
|
|
260
|
+
def add_to_repository():
|
|
261
|
+
with repository.new_version(base_version=base_version) as new_version:
|
|
262
|
+
new_version.remove_content(models.Content.objects.filter(pk__in=remove_content_units))
|
|
263
|
+
new_version.add_content(models.Content.objects.filter(pk__in=add_content_units))
|
|
264
|
+
|
|
265
|
+
await sync_to_async(add_to_repository)()
|