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.

Files changed (42) hide show
  1. pulp_certguard/app/__init__.py +1 -1
  2. pulp_file/app/__init__.py +1 -1
  3. pulp_file/tests/functional/api/test_filesystem_export.py +220 -0
  4. pulp_file/tests/functional/api/test_pulp_export.py +103 -3
  5. pulpcore/app/apps.py +1 -1
  6. pulpcore/app/importexport.py +18 -2
  7. pulpcore/app/management/commands/shell.py +8 -0
  8. pulpcore/app/migrations/0144_delete_old_appstatus.py +28 -0
  9. pulpcore/app/migrations/0145_domainize_import_export.py +53 -0
  10. pulpcore/app/modelresource.py +61 -21
  11. pulpcore/app/models/__init__.py +2 -5
  12. pulpcore/app/models/exporter.py +7 -1
  13. pulpcore/app/models/fields.py +0 -1
  14. pulpcore/app/models/importer.py +8 -1
  15. pulpcore/app/models/repository.py +16 -0
  16. pulpcore/app/models/status.py +8 -138
  17. pulpcore/app/models/task.py +15 -25
  18. pulpcore/app/serializers/domain.py +1 -1
  19. pulpcore/app/serializers/exporter.py +4 -4
  20. pulpcore/app/serializers/importer.py +2 -2
  21. pulpcore/app/serializers/task.py +11 -8
  22. pulpcore/app/tasks/importer.py +44 -10
  23. pulpcore/app/tasks/repository.py +27 -0
  24. pulpcore/app/viewsets/base.py +18 -14
  25. pulpcore/app/viewsets/domain.py +1 -1
  26. pulpcore/app/viewsets/exporter.py +1 -8
  27. pulpcore/app/viewsets/importer.py +1 -6
  28. pulpcore/app/viewsets/task.py +0 -1
  29. pulpcore/openapi/__init__.py +16 -2
  30. pulpcore/plugin/tasking.py +4 -2
  31. pulpcore/tasking/tasks.py +245 -127
  32. pulpcore/tasking/worker.py +6 -17
  33. pulpcore/tests/functional/api/test_crud_domains.py +7 -0
  34. pulpcore/tests/functional/api/test_tasking.py +2 -2
  35. pulpcore/tests/functional/api/using_plugin/test_crud_repos.py +9 -2
  36. pulpcore/tests/unit/content/test_handler.py +43 -0
  37. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/METADATA +7 -7
  38. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/RECORD +42 -38
  39. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/WHEEL +0 -0
  40. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/entry_points.txt +0 -0
  41. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/licenses/LICENSE +0 -0
  42. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/top_level.txt +0 -0
@@ -69,7 +69,7 @@ from .repository import (
69
69
  RepositoryVersionContentDetails,
70
70
  )
71
71
 
72
- from .status import AppStatus, ApiAppStatus, ContentAppStatus
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",
@@ -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(db_index=True, unique=True)
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):
@@ -79,7 +79,6 @@ class ArtifactFileField(FileField):
79
79
  "prior to Artifact creation."
80
80
  )
81
81
  )
82
-
83
82
  move = file._committed and file.name != artifact_storage_path
84
83
  if move:
85
84
  if not already_in_place:
@@ -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(db_index=True, unique=True)
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())
@@ -15,44 +15,12 @@ from pulpcore.app.models import BaseModel
15
15
 
16
16
 
17
17
  class AppStatusManager(models.Manager):
18
- # This should be replaced with 3.87.
19
- def online(self):
20
- """
21
- Returns a queryset of objects that are online.
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
- _APP_TTL = {
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)
@@ -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, BaseAppStatus
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
- _("Falied to set task {} unblocked in state '{}'.").format(self.pk, self.state)
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.core.files.storage import import_string
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 file system exporter."),
41
- validators=[UniqueValidator(queryset=models.Exporter.objects.all())],
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=[UniqueValidator(queryset=models.Importer.objects.all())],
25
+ validators=[DomainUniqueValidator(queryset=models.Importer.objects.all())],
26
26
  )
27
27
 
28
28
  class Meta:
@@ -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 = RelatedField(
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 get_created_by(self, obj):
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(
@@ -35,7 +35,12 @@ from pulpcore.app.modelresource import (
35
35
  ContentArtifactResource,
36
36
  RepositoryResource,
37
37
  )
38
- from pulpcore.app.util import compute_file_hash, Crc32Hasher
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
- if not default_storage.exists(base_path):
498
- with open(src, "rb") as f:
499
- default_storage.save(base_path, f)
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(name=dest_repo_name)
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 = ""
@@ -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)()