pulpcore 3.76.1__py3-none-any.whl → 3.77.1__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.

@@ -6,6 +6,6 @@ class PulpCertGuardPluginAppConfig(PulpPluginAppConfig):
6
6
 
7
7
  name = "pulp_certguard.app"
8
8
  label = "certguard"
9
- version = "3.76.1"
9
+ version = "3.77.1"
10
10
  python_package_name = "pulpcore"
11
11
  domain_compatible = True
pulp_file/app/__init__.py CHANGED
@@ -8,6 +8,6 @@ class PulpFilePluginAppConfig(PulpPluginAppConfig):
8
8
 
9
9
  name = "pulp_file.app"
10
10
  label = "file"
11
- version = "3.76.1"
11
+ version = "3.77.1"
12
12
  python_package_name = "pulpcore"
13
13
  domain_compatible = True
@@ -1,10 +1,89 @@
1
- import uuid
2
-
3
1
  import pytest
2
+ import uuid
4
3
 
5
4
  from pulpcore.client.pulp_file import (
6
5
  RepositorySyncURL,
7
6
  )
7
+ from pulpcore.client.pulp_file.exceptions import BadRequestException
8
+
9
+ GOOD_CERT = """-----BEGIN CERTIFICATE-----
10
+ MIICoDCCAYgCCQC2c2uY34HNlzANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDDAdn
11
+ b3ZlZ2FuMB4XDTE5MDMxMzIxMDMzMFoXDTM4MDYxNjIxMDMzMFowEjEQMA4GA1UE
12
+ AwwHZ292ZWdhbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEatWsZ
13
+ 1iwGmTxD02dxMI4ci+Au4FzvmWLBWD07H5GGTVFwnqmNOKhP6DHs1EsMZevkUvaG
14
+ CRxZlPYhjNFLZr2c2FnoDZ5nBXlSW6sodXURbMfyT187nDeBXVYFuh4T2eNCatnm
15
+ t3vgdi+pWsF0LbOgpu7GJI2sh5K1imxyB77tJ7PFTDZCSohkK+A+0nDCnJqDUNXD
16
+ 5CK8iaBciCbnzp3nRKuM2EmgXno9Repy/HYxIgB7ZodPwDvYNjMGfvs0s9mJIKmc
17
+ CKgkPXVO9y9gaRrrytICcPOs+YoU/PN4Ttg6wzxaWvJgw44vsR8wM/0i4HlXfBdl
18
+ 9br+cgn8jukDOgECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAyNHV6NA+0GfUrvBq
19
+ AHXHNnBE3nzMhGPhF/0B/dO4o0n6pgGZyzRxaUaoo6+5oQnBf/2NmDyLWdalFWX7
20
+ D1WBaxkhK+FU922+qwQKhABlwMxGCnfZ8F+rlk4lNotm3fP4wHbnO1SGIDvvZFt/
21
+ mpMgkhwL4lShUFv57YylXr+D2vSFcAryKiVGk1X3sHMXlFAMLHUm3d97fJnmb1qQ
22
+ wC43BlJCBQF98wKtYNwTUG/9gblfk8lCB2DL1hwmPy3q9KbSDOdUK3HW6a75ZzCD
23
+ 6mXc/Y0bJcwweDsywbPBYP13hYUcpw4htcU6hg6DsoAjLNkSrlY+GGo7htx+L9HH
24
+ IwtfRg==
25
+ -----END CERTIFICATE-----
26
+ """
27
+
28
+ GOOD_CERT_WITH_COMMENT = """saydas Intermédiaire CA
29
+ -----BEGIN CERTIFICATE-----
30
+ MIICoDCCAYgCCQC2c2uY34HNlzANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDDAdn
31
+ b3ZlZ2FuMB4XDTE5MDMxMzIxMDMzMFoXDTM4MDYxNjIxMDMzMFowEjEQMA4GA1UE
32
+ AwwHZ292ZWdhbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEatWsZ
33
+ 1iwGmTxD02dxMI4ci+Au4FzvmWLBWD07H5GGTVFwnqmNOKhP6DHs1EsMZevkUvaG
34
+ CRxZlPYhjNFLZr2c2FnoDZ5nBXlSW6sodXURbMfyT187nDeBXVYFuh4T2eNCatnm
35
+ t3vgdi+pWsF0LbOgpu7GJI2sh5K1imxyB77tJ7PFTDZCSohkK+A+0nDCnJqDUNXD
36
+ 5CK8iaBciCbnzp3nRKuM2EmgXno9Repy/HYxIgB7ZodPwDvYNjMGfvs0s9mJIKmc
37
+ CKgkPXVO9y9gaRrrytICcPOs+YoU/PN4Ttg6wzxaWvJgw44vsR8wM/0i4HlXfBdl
38
+ 9br+cgn8jukDOgECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAyNHV6NA+0GfUrvBq
39
+ AHXHNnBE3nzMhGPhF/0B/dO4o0n6pgGZyzRxaUaoo6+5oQnBf/2NmDyLWdalFWX7
40
+ D1WBaxkhK+FU922+qwQKhABlwMxGCnfZ8F+rlk4lNotm3fP4wHbnO1SGIDvvZFt/
41
+ mpMgkhwL4lShUFv57YylXr+D2vSFcAryKiVGk1X3sHMXlFAMLHUm3d97fJnmb1qQ
42
+ wC43BlJCBQF98wKtYNwTUG/9gblfk8lCB2DL1hwmPy3q9KbSDOdUK3HW6a75ZzCD
43
+ 6mXc/Y0bJcwweDsywbPBYP13hYUcpw4htcU6hg6DsoAjLNkSrlY+GGo7htx+L9HH
44
+ IwtfRg==
45
+ -----END CERTIFICATE-----
46
+ """
47
+
48
+ GOOD_CERT_TWO = """-----BEGIN CERTIFICATE-----
49
+ MIICqjCCAZICAgtCMA0GCSqGSIb3DQEBBQUAMBIxEDAOBgNVBAMMB2dvdmVnYW4w
50
+ HhcNMTkwMzEzMjEwMzMwWhcNMjkwMzEwMjEwMzMwWjAjMRAwDgYDVQQDDAdnb3Zl
51
+ Z2FuMQ8wDQYDVQQKDAZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
52
+ AoIBAQCxJWx5t25jY4womWtKxGqv2LHg9YnU0b2VCECLhu5JjoAzFPja5VHB0Maz
53
+ G8m5c0+N2ubrPcBC+KdoGMd2MqrrGyzKOiwbVDW0YOgnFqh58p796iKtVboWx41y
54
+ Gzn289PzYccxH6mhhPmRVD25KyV1TenqvGIHJTepF7mgIemGDTv+j7+mYPT/3r6I
55
+ pnwTkEVPr+Q4iW0l3fNESlFFRt2b7yhz9f0E4SMhmIRnSIGOLO1zE02IJ1hTuGkx
56
+ /MZ1AqQdVSdm4jenTIMp91R1kYylI66yMcpU6w6x4j8qvJ8nBZ4r4DqOHcOobyHp
57
+ qlaJjv/K5SGJxV2k0EFk7b483lbrAgMBAAEwDQYJKoZIhvcNAQEFBQADggEBALYh
58
+ SHLGJCxVL8ePFLs294fhTq4pTQsvHm8q3SyJD9DaB+HKTceCFErNv18Dsl/QwBis
59
+ WPHWKpDN0EUcuuE/8oUaGjjzByJ8bPafMicFCHSSefcJw+IOOqKBkWDT+4YGkvfs
60
+ RpwxSLqLOhEt7aSkiPcMvD20v8cvj0O36c5G3Vv0E8WmPWOEqjyPFoU9X1vACr+h
61
+ DdIKvxFbvRU9ObektFxOYHuvP010IBv2dGyw3G5W5fh9A5OSXHAShWSwkRU36oft
62
+ ugB47fIIlb7zLm4GBmxGG0yBwAf4otBlUXVNqNx15bbUuVgKbGMFfItQgEo9AQcz
63
+ gGsetwDOs/NgZ95oH40=
64
+ -----END CERTIFICATE-----
65
+ """
66
+
67
+ BAD_CERT = """-----BEGIN CERTIFICATE-----\nBOGUS==\n-----END CERTIFICATE-----
68
+ """
69
+
70
+ NO_CERT = """
71
+ MIICoDCCAYgCCQC2c2uY34HNlzANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDDAdn
72
+ b3ZlZ2FuMB4XDTE5MDMxMzIxMDMzMFoXDTM4MDYxNjIxMDMzMFowEjEQMA4GA1UE
73
+ AwwHZ292ZWdhbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANEatWsZ
74
+ 1iwGmTxD02dxMI4ci+Au4FzvmWLBWD07H5GGTVFwnqmNOKhP6DHs1EsMZevkUvaG
75
+ CRxZlPYhjNFLZr2c2FnoDZ5nBXlSW6sodXURbMfyT187nDeBXVYFuh4T2eNCatnm
76
+ t3vgdi+pWsF0LbOgpu7GJI2sh5K1imxyB77tJ7PFTDZCSohkK+A+0nDCnJqDUNXD
77
+ 5CK8iaBciCbnzp3nRKuM2EmgXno9Repy/HYxIgB7ZodPwDvYNjMGfvs0s9mJIKmc
78
+ CKgkPXVO9y9gaRrrytICcPOs+YoU/PN4Ttg6wzxaWvJgw44vsR8wM/0i4HlXfBdl
79
+ 9br+cgn8jukDOgECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAyNHV6NA+0GfUrvBq
80
+ AHXHNnBE3nzMhGPhF/0B/dO4o0n6pgGZyzRxaUaoo6+5oQnBf/2NmDyLWdalFWX7
81
+ D1WBaxkhK+FU922+qwQKhABlwMxGCnfZ8F+rlk4lNotm3fP4wHbnO1SGIDvvZFt/
82
+ mpMgkhwL4lShUFv57YylXr+D2vSFcAryKiVGk1X3sHMXlFAMLHUm3d97fJnmb1qQ
83
+ wC43BlJCBQF98wKtYNwTUG/9gblfk8lCB2DL1hwmPy3q9KbSDOdUK3HW6a75ZzCD
84
+ 6mXc/Y0bJcwweDsywbPBYP13hYUcpw4htcU6hg6DsoAjLNkSrlY+GGo7htx+L9HH
85
+ IwtfRg==
86
+ """
8
87
 
9
88
 
10
89
  def _run_basic_sync_and_assert(file_bindings, remote, file_repo, monitor_task):
@@ -211,3 +290,55 @@ def test_header_for_sync(
211
290
  assert requests_record[0].path == "/basic/PULP_MANIFEST"
212
291
  assert header_name in requests_record[0].headers
213
292
  assert header_value == requests_record[0].headers[header_name]
293
+
294
+
295
+ @pytest.mark.parallel
296
+ def test_certificate_clean(file_remote_factory):
297
+ # Check that a good cert validates
298
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=GOOD_CERT)
299
+ assert a_remote.ca_cert == GOOD_CERT
300
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=GOOD_CERT)
301
+ assert a_remote.client_cert == GOOD_CERT
302
+
303
+ # Check that a good-cert-with-comments validates and strips the comments
304
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=GOOD_CERT_WITH_COMMENT)
305
+ assert a_remote.ca_cert == GOOD_CERT
306
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=GOOD_CERT_WITH_COMMENT)
307
+ assert a_remote.client_cert == GOOD_CERT
308
+
309
+ # Check that a bad-cert gets rejected
310
+ with pytest.raises(BadRequestException):
311
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=BAD_CERT)
312
+ with pytest.raises(BadRequestException):
313
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=BAD_CERT)
314
+
315
+ # Check that a no-cert string returns the expected error
316
+ with pytest.raises(BadRequestException):
317
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=NO_CERT)
318
+ with pytest.raises(BadRequestException):
319
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=NO_CERT)
320
+
321
+
322
+ @pytest.mark.parallel
323
+ def test_multi_certificate_clean(file_remote_factory):
324
+ multi_cert = GOOD_CERT + GOOD_CERT_TWO
325
+
326
+ # Check that a good multi-cert PEM validates
327
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=multi_cert)
328
+ assert a_remote.ca_cert == multi_cert
329
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=multi_cert)
330
+ assert a_remote.client_cert == multi_cert
331
+
332
+ # Check that a good-cert-with-comments validates and strips the comments
333
+ multi_cert_with_comment = GOOD_CERT_WITH_COMMENT + GOOD_CERT_TWO
334
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=multi_cert_with_comment)
335
+ assert a_remote.ca_cert == multi_cert
336
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=multi_cert_with_comment)
337
+ assert a_remote.client_cert == multi_cert
338
+
339
+ # Check that multi-with-bad is rejected
340
+ multi_bad = GOOD_CERT + BAD_CERT
341
+ with pytest.raises(BadRequestException):
342
+ a_remote = file_remote_factory(url="http://example.com/", ca_cert=multi_bad)
343
+ with pytest.raises(BadRequestException):
344
+ a_remote = file_remote_factory(url="http://example.com/", client_cert=multi_bad)
pulpcore/app/apps.py CHANGED
@@ -247,7 +247,7 @@ class PulpAppConfig(PulpPluginAppConfig):
247
247
  label = "core"
248
248
 
249
249
  # The version of this app
250
- version = "3.76.1"
250
+ version = "3.77.1"
251
251
 
252
252
  # The python package name providing this app
253
253
  python_package_name = "pulpcore"
pulpcore/app/replica.py CHANGED
@@ -8,7 +8,7 @@ from pulp_glue.common.context import PulpContext
8
8
  from pulpcore.app.models import UpstreamPulp
9
9
  from pulpcore.tasking.tasks import dispatch
10
10
  from pulpcore.app.tasks.base import (
11
- general_update,
11
+ ageneral_update,
12
12
  general_create,
13
13
  general_multi_delete,
14
14
  )
@@ -133,7 +133,7 @@ class Replicator:
133
133
  needs_update = self.needs_update(remote_fields_dict, remote)
134
134
  if needs_update:
135
135
  dispatch(
136
- general_update,
136
+ ageneral_update,
137
137
  task_group=self.task_group,
138
138
  shared_resources=[self.server],
139
139
  exclusive_resources=[remote],
@@ -162,7 +162,7 @@ class Replicator:
162
162
  needs_update = self.needs_update(repo_fields_dict, repository)
163
163
  if needs_update:
164
164
  dispatch(
165
- general_update,
165
+ ageneral_update,
166
166
  task_group=self.task_group,
167
167
  shared_resources=[self.server],
168
168
  exclusive_resources=[repository],
@@ -197,7 +197,7 @@ class Replicator:
197
197
  if needs_update:
198
198
  # Update the distribution
199
199
  dispatch(
200
- general_update,
200
+ ageneral_update,
201
201
  task_group=self.task_group,
202
202
  shared_resources=[repository, self.server],
203
203
  exclusive_resources=self.distros_uris,
@@ -407,14 +407,34 @@ def validate_repo_and_remote(repository, field, **kwargs):
407
407
 
408
408
  detail_repo = repository.cast()
409
409
  if detail_repo.remote and type(detail_repo.remote.cast()) not in detail_repo.REMOTE_TYPES:
410
- msg = (
411
- f"Type for Remote '{get_prn(repository.remote)}' "
412
- f"does not match Repository '{get_prn(repository)}'"
413
- )
414
- if repository_version := kwargs.pop("repository_version", None):
415
- msg += f" from RepositoryVersion '{get_prn(repository_version)}'"
416
- if publication := kwargs.pop("publication", None):
417
- msg += f" from Publication '{get_prn(publication)}'"
418
- msg += "."
410
+ repository_version = kwargs.pop("repository_version", None)
411
+ publication = kwargs.pop("publication", None)
412
+
413
+ if repository_version and publication:
414
+ msg_template = _(
415
+ "Type for Remote '{remote}' does not match Repository '{repository}' "
416
+ "from RepositoryVersion '{repository_version}' from Publication '{publication}'."
417
+ )
418
+ elif repository_version:
419
+ msg_template = _(
420
+ "Type for Remote '{remote}' does not match Repository '{repository}' "
421
+ "from RepositoryVersion '{repository_version}'."
422
+ )
423
+ elif publication:
424
+ msg_template = _(
425
+ "Type for Remote '{remote}' does not match Repository '{repository}' "
426
+ "from Publication '{publication}'."
427
+ )
428
+ else:
429
+ msg_template = _("Type for Remote '{remote}' does not match Repository '{repository}'.")
419
430
 
420
- raise serializers.ValidationError({field: _(msg)})
431
+ raise serializers.ValidationError(
432
+ {
433
+ field: msg_template.format(
434
+ remote=get_prn(repository.remote),
435
+ repository=get_prn(repository),
436
+ repository_version=get_prn(repository_version) if repository_version else "",
437
+ publication=get_prn(publication) if publication else "",
438
+ )
439
+ }
440
+ )
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from cryptography.x509 import load_pem_x509_certificate
2
3
  from gettext import gettext as _
3
4
  from urllib.parse import urlparse
4
5
 
@@ -72,6 +73,46 @@ class RepositorySerializer(ModelSerializer):
72
73
  )
73
74
 
74
75
 
76
+ def validate_certificate(which_cert, value):
77
+ """
78
+ Validate and return *just* the certs and not any commentary that came along with them.
79
+
80
+ Args:
81
+ which_cert: The attribute-name whose cert we're validating (only used for error-message).
82
+ value: The string being proposed as a certificate-containing PEM.
83
+
84
+ Raises:
85
+ ValidationError: When the provided value has no or an invalid certificate.
86
+
87
+ Returns:
88
+ The pem-string with *just* the validated BEGIN/END CERTIFICATE segments.
89
+ """
90
+ if value:
91
+ try:
92
+ # Find any/all CERTIFICATE entries in the proposed PEM and let crypto validate them.
93
+ # NOTE: crypto/39 includes load_certificates(), which will let us remove this whole
94
+ # loop. But we want to fix the current problem on older supported branches that
95
+ # allow 38, so we do it ourselves for now
96
+ certs = list()
97
+ a_cert = ""
98
+ for line in value.split("\n"):
99
+ if "-----BEGIN CERTIFICATE-----" in line or a_cert:
100
+ a_cert += line + "\n"
101
+ if "-----END CERTIFICATE-----" in line:
102
+ load_pem_x509_certificate(bytes(a_cert, "ASCII"))
103
+ certs.append(a_cert.strip())
104
+ a_cert = ""
105
+ if not certs:
106
+ raise serializers.ValidationError(
107
+ "No {} specified in string {}".format(which_cert, value)
108
+ )
109
+ return "\n".join(certs) + "\n"
110
+ except ValueError as e:
111
+ raise serializers.ValidationError(
112
+ "Invalid {} specified, error '{}'".format(which_cert, e.args)
113
+ )
114
+
115
+
75
116
  class RemoteSerializer(ModelSerializer, HiddenFieldsMixin):
76
117
  """
77
118
  Every remote defined by a plugin should have a Remote serializer that inherits from this
@@ -271,6 +312,12 @@ class RemoteSerializer(ModelSerializer, HiddenFieldsMixin):
271
312
  raise serializers.ValidationError(_("proxy_url must not contain credentials"))
272
313
  return value
273
314
 
315
+ def validate_ca_cert(self, value):
316
+ return validate_certificate("ca_cert", value)
317
+
318
+ def validate_client_cert(self, value):
319
+ return validate_certificate("client_cert", value)
320
+
274
321
  def validate(self, data):
275
322
  """
276
323
  Check, that proxy credentials are only provided completely and if a proxy is configured.
@@ -365,9 +412,8 @@ class RepositorySyncURLSerializer(ValidateFieldsMixin, serializers.Serializer):
365
412
  if repository and type(remote.cast()) not in repository.cast().REMOTE_TYPES:
366
413
  raise serializers.ValidationError(
367
414
  {
368
- "remote": _(
369
- f"Type for Remote '{get_prn(remote)}' "
370
- f"does not match Repository '{get_prn(repository)}'."
415
+ "remote": _("Type for Remote '{}' does not match Repository '{}'.").format(
416
+ get_prn(remote), get_prn(repository)
371
417
  )
372
418
  }
373
419
  )
pulpcore/app/settings.py CHANGED
@@ -382,7 +382,19 @@ ALLOWED_CONTENT_CHECKSUMS = ["sha224", "sha256", "sha384", "sha512"]
382
382
 
383
383
  DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
384
384
 
385
- TASK_DIAGNOSTICS = False
385
+ # Possible diagnostics:
386
+ # * "memory"
387
+ # Logs the task process RSS every couple of seconds
388
+ # * "pyinstrument"
389
+ # Dumps an HTML profile report produced by pyinstrument, showing time spent in various
390
+ # callstacks. This adds ~10% overhead to the task process and consumes extra memory.
391
+ # Tweaking code might be warranted for some advanced settings.
392
+ # * "memray" - Dumps a report produced by memray which logs how much memory was allocated by which
393
+ # lines and functions, at the time of peak RSS of the task process. This adds significant
394
+ # runtime overhead to the task process, 20-40%. Tweaking code might be warranted for
395
+ # some advanced settings.
396
+ # NOTE: "memray" and "pyinstrument" require additional packages to be installed on the system.
397
+ TASK_DIAGNOSTICS = [] # ["memory", "pyinstrument", "memray"]
386
398
 
387
399
  ANALYTICS = True
388
400
 
@@ -1,11 +1,11 @@
1
1
  from pulpcore.app.tasks import base, repository, upload
2
2
 
3
3
  from .base import (
4
+ ageneral_update,
4
5
  general_create,
5
6
  general_create_from_temp_file,
6
7
  general_delete,
7
8
  general_multi_delete,
8
- general_update,
9
9
  )
10
10
 
11
11
  from .export import fs_publication_export, fs_repo_version_export
@@ -2,8 +2,11 @@ from django.db import transaction
2
2
 
3
3
  from pulpcore.app.apps import get_plugin_config
4
4
  from pulpcore.app.models import CreatedResource
5
+ from pulpcore.app.loggers import deprecation_logger
5
6
  from pulpcore.plugin.models import MasterModel
6
7
 
8
+ from asgiref.sync import sync_to_async
9
+
7
10
 
8
11
  def general_create_from_temp_file(app_label, serializer_name, temp_file_pk, *args, **kwargs):
9
12
  """
@@ -63,6 +66,10 @@ def general_update(instance_id, app_label, serializer_name, *args, **kwargs):
63
66
  due to validation error. This theoretically should never occur since validation is
64
67
  performed before the task is dispatched.
65
68
  """
69
+ deprecation_logger.warning(
70
+ "`pulpcore.app.tasks.base.general_update` is deprecated and will be removed in Pulp 4. "
71
+ "Use `pulpcore.app.tasks.base.ageneral_update` instead."
72
+ )
66
73
  data = kwargs.pop("data", None)
67
74
  partial = kwargs.pop("partial", False)
68
75
  serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
@@ -85,6 +92,10 @@ def general_delete(instance_id, app_label, serializer_name):
85
92
  app_label (str): the Django app label of the plugin that provides the model
86
93
  serializer_name (str): name of the serializer class for the model
87
94
  """
95
+ deprecation_logger.warning(
96
+ "`pulpcore.app.tasks.base.general_delete` is deprecated and will be removed in Pulp 4. "
97
+ "Use `pulpcore.app.tasks.base.ageneral_delete` instead."
98
+ )
88
99
  serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
89
100
  instance = serializer_class.Meta.model.objects.get(pk=instance_id)
90
101
  if isinstance(instance, MasterModel):
@@ -111,3 +122,29 @@ def general_multi_delete(instance_ids):
111
122
  with transaction.atomic():
112
123
  for instance in instances:
113
124
  instance.delete()
125
+
126
+
127
+ async def ageneral_update(instance_id, app_label, serializer_name, *args, **kwargs):
128
+ """
129
+ Async version of [pulpcore.app.tasks.base.general_update][].
130
+ """
131
+ data = kwargs.pop("data", None)
132
+ partial = kwargs.pop("partial", False)
133
+ serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
134
+ instance = await serializer_class.Meta.model.objects.aget(pk=instance_id)
135
+ if isinstance(instance, MasterModel):
136
+ instance = await instance.acast()
137
+ serializer = serializer_class(instance, data=data, partial=partial)
138
+ await sync_to_async(serializer.is_valid)(raise_exception=True)
139
+ await sync_to_async(serializer.save)()
140
+
141
+
142
+ async def ageneral_delete(instance_id, app_label, serializer_name):
143
+ """
144
+ Async version of [pulpcore.app.tasks.base.general_delete][].
145
+ """
146
+ serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
147
+ instance = await serializer_class.Meta.model.objects.aget(pk=instance_id)
148
+ if isinstance(instance, MasterModel):
149
+ instance = await instance.acast()
150
+ await instance.adelete()
@@ -1,4 +1,6 @@
1
+ import asyncio
1
2
  import backoff
3
+ import time
2
4
  from pulpcore.app.models import TaskGroup
3
5
  from pulpcore.tasking.tasks import dispatch
4
6
 
@@ -9,17 +11,18 @@ def dummy_task():
9
11
 
10
12
 
11
13
  def sleep(interval):
12
- from time import sleep
14
+ time.sleep(interval)
13
15
 
14
- sleep(interval)
16
+
17
+ async def asleep(interval):
18
+ """Async function that sleeps."""
19
+ await asyncio.sleep(interval)
15
20
 
16
21
 
17
22
  @backoff.on_exception(backoff.expo, BaseException)
18
23
  def gooey_task(interval):
19
24
  """A sleep task that tries to avoid being killed by ignoring all exceptions."""
20
- from time import sleep
21
-
22
- sleep(interval)
25
+ time.sleep(interval)
23
26
 
24
27
 
25
28
  def dummy_group_task(inbetween=3, intervals=None):
@@ -28,5 +31,5 @@ def dummy_group_task(inbetween=3, intervals=None):
28
31
  task_group = TaskGroup.current()
29
32
  for interval in intervals:
30
33
  dispatch(sleep, args=(interval,), task_group=task_group)
31
- sleep(inbetween)
34
+ time.sleep(inbetween)
32
35
  task_group.finish()
@@ -492,7 +492,7 @@ class AsyncUpdateMixin(AsyncReservedObjectMixin):
492
492
  serializer.is_valid(raise_exception=True)
493
493
  app_label = instance._meta.app_label
494
494
  task = dispatch(
495
- tasks.base.general_update,
495
+ tasks.base.ageneral_update,
496
496
  exclusive_resources=self.async_reserved_resources(instance),
497
497
  args=(pk, app_label, serializer.__class__.__name__),
498
498
  kwargs={"data": request.data, "partial": partial},
@@ -528,7 +528,7 @@ class AsyncRemoveMixin(AsyncReservedObjectMixin):
528
528
  serializer = self.get_serializer(instance)
529
529
  app_label = instance._meta.app_label
530
530
  task = dispatch(
531
- tasks.base.general_delete,
531
+ tasks.base.ageneral_delete,
532
532
  exclusive_resources=self.async_reserved_resources(instance),
533
533
  args=(pk, app_label, serializer.__class__.__name__),
534
534
  immediate=self.ALLOW_NON_BLOCKING_DELETE,
pulpcore/cache/cache.py CHANGED
@@ -8,7 +8,7 @@ from django.http import HttpResponseRedirect, HttpResponse, FileResponse as ApiF
8
8
 
9
9
  from rest_framework.request import Request as ApiRequest
10
10
 
11
- from aiohttp.web import FileResponse, Response, HTTPSuccessful, Request
11
+ from aiohttp.web import FileResponse, Response, HTTPSuccessful, Request, StreamResponse
12
12
  from aiohttp.web_exceptions import HTTPFound
13
13
 
14
14
  from redis import ConnectionError
@@ -401,6 +401,11 @@ class AsyncContentCache(AsyncCache):
401
401
  except (HTTPSuccessful, HTTPFound) as e:
402
402
  response = e
403
403
 
404
+ original_response = response
405
+ if isinstance(response, StreamResponse):
406
+ if hasattr(response, "future_response"):
407
+ response = response.future_response
408
+
404
409
  entry = {"headers": dict(response.headers), "status": response.status}
405
410
  if expires is not None:
406
411
  # Redis TTL is not sufficient: https://github.com/pulp/pulpcore/issues/4845
@@ -427,12 +432,12 @@ class AsyncContentCache(AsyncCache):
427
432
  entry["location"] = str(response.location)
428
433
  entry["type"] = "Redirect"
429
434
  else:
430
- # We don't cache StreamResponses or errors
435
+ # We don't cache errors
431
436
  return response
432
437
 
433
438
  # TODO look into smaller format, maybe some compression on the text
434
439
  await self.set(key, json.dumps(entry), expires, base_key=base_key)
435
- return response
440
+ return original_response
436
441
 
437
442
  def make_key(self, request):
438
443
  """Makes the key based off the request"""
pulpcore/constants.py CHANGED
@@ -45,6 +45,8 @@ TASK_FINAL_STATES = (
45
45
  #: Tasks in an incomplete state have not finished their work yet.
46
46
  TASK_INCOMPLETE_STATES = (TASK_STATES.WAITING, TASK_STATES.RUNNING, TASK_STATES.CANCELING)
47
47
 
48
+ #: Timeout for immediate tasks in seconds
49
+ IMMEDIATE_TIMEOUT = 5
48
50
 
49
51
  SYNC_MODES = SimpleNamespace(ADDITIVE="additive", MIRROR="mirror")
50
52
  SYNC_CHOICES = (
@@ -1093,26 +1093,8 @@ class Handler:
1093
1093
  ret.update({ca.relative_path: ca for ca in cas})
1094
1094
  return ret
1095
1095
 
1096
- async def _serve_content_artifact(self, content_artifact, headers, request):
1097
- """
1098
- Handle response for a Content Artifact with the file present.
1099
-
1100
- Depending on where the file storage (e.g. filesystem, S3, etc) this could be responding with
1101
- the file (filesystem) or a redirect (S3).
1102
-
1103
- Args:
1104
- content_artifact (pulpcore.app.models.ContentArtifact) The Content Artifact to
1105
- respond with.
1106
- headers (dict): A dictionary of response headers.
1107
- request(aiohttp.web.Request) The request to prepare a response for.
1108
-
1109
- Raises:
1110
- [aiohttp.web_exceptions.HTTPFound][]: When we need to redirect to the file
1111
- NotImplementedError: If file is stored in a file storage we can't handle
1112
-
1113
- Returns:
1114
- The [aiohttp.web.FileResponse][] for the file.
1115
- """
1096
+ def _build_response_from_content_artifact(self, content_artifact, headers, request):
1097
+ """Helper method to build the correct response to serve a ContentArtifact."""
1116
1098
 
1117
1099
  def _set_params_from_headers(hdrs, storage_domain):
1118
1100
  # Map standard-response-headers to storage-object-specific keys
@@ -1137,7 +1119,47 @@ class Handler:
1137
1119
  artifact_name = artifact_file.name
1138
1120
  domain = get_domain()
1139
1121
  storage = domain.get_storage()
1122
+ headers["X-PULP-ARTIFACT-SIZE"] = str(artifact_file.size)
1123
+
1124
+ if domain.storage_class == "pulpcore.app.models.storage.FileSystem":
1125
+ path = storage.path(artifact_name)
1126
+ if not os.path.exists(path):
1127
+ raise Exception(_("Expected path '{}' is not found").format(path))
1128
+ return FileResponse(path, headers=headers)
1129
+ elif not domain.redirect_to_object_storage:
1130
+ return ArtifactResponse(content_artifact.artifact, headers=headers)
1131
+ elif domain.storage_class == "storages.backends.s3boto3.S3Boto3Storage":
1132
+ return HTTPFound(_build_url(http_method=request.method), headers=headers)
1133
+ elif domain.storage_class in (
1134
+ "storages.backends.azure_storage.AzureStorage",
1135
+ "storages.backends.gcloud.GoogleCloudStorage",
1136
+ ):
1137
+ return HTTPFound(_build_url(), headers=headers)
1138
+ else:
1139
+ raise NotImplementedError()
1140
1140
 
1141
+ async def _serve_content_artifact(self, content_artifact, headers, request):
1142
+ """
1143
+ Handle response for a Content Artifact with the file present.
1144
+
1145
+ Depending on where the file storage (e.g. filesystem, S3, etc) this could be responding with
1146
+ the file (filesystem) or a redirect (S3).
1147
+
1148
+ Args:
1149
+ content_artifact (pulpcore.app.models.ContentArtifact) The Content Artifact to
1150
+ respond with.
1151
+ headers (dict): A dictionary of response headers.
1152
+ request(aiohttp.web.Request) The request to prepare a response for.
1153
+
1154
+ Raises:
1155
+ [aiohttp.web_exceptions.HTTPFound][]: When we need to redirect to the file
1156
+ NotImplementedError: If file is stored in a file storage we can't handle
1157
+ HTTPRequestRangeNotSatisfiable: If the request is for a range that is not
1158
+ satisfiable.
1159
+ Returns:
1160
+ The [aiohttp.web.FileResponse][] for the file.
1161
+ """
1162
+ artifact_file = content_artifact.artifact.file
1141
1163
  content_length = artifact_file.size
1142
1164
 
1143
1165
  try:
@@ -1152,25 +1174,13 @@ class Handler:
1152
1174
  size = artifact_file.size or "*"
1153
1175
  raise HTTPRequestRangeNotSatisfiable(headers={"Content-Range": f"bytes */{size}"})
1154
1176
 
1155
- headers["X-PULP-ARTIFACT-SIZE"] = str(content_length)
1156
1177
  artifacts_size_counter.add(content_length)
1157
1178
 
1158
- if domain.storage_class == "pulpcore.app.models.storage.FileSystem":
1159
- path = storage.path(artifact_name)
1160
- if not os.path.exists(path):
1161
- raise Exception(_("Expected path '{}' is not found").format(path))
1162
- return FileResponse(path, headers=headers)
1163
- elif not domain.redirect_to_object_storage:
1164
- return ArtifactResponse(content_artifact.artifact, headers=headers)
1165
- elif domain.storage_class == "storages.backends.s3boto3.S3Boto3Storage":
1166
- raise HTTPFound(_build_url(http_method=request.method), headers=headers)
1167
- elif domain.storage_class in (
1168
- "storages.backends.azure_storage.AzureStorage",
1169
- "storages.backends.gcloud.GoogleCloudStorage",
1170
- ):
1171
- raise HTTPFound(_build_url(), headers=headers)
1179
+ response = self._build_response_from_content_artifact(content_artifact, headers, request)
1180
+ if isinstance(response, HTTPFound):
1181
+ raise response
1172
1182
  else:
1173
- raise NotImplementedError()
1183
+ return response
1174
1184
 
1175
1185
  async def _stream_remote_artifact(
1176
1186
  self, request, response, remote_artifact, save_artifact=True, repository=None
@@ -1214,6 +1224,7 @@ class Handler:
1214
1224
  size = remote_artifact.size or "*"
1215
1225
  raise HTTPRequestRangeNotSatisfiable(headers={"Content-Range": f"bytes */{size}"})
1216
1226
 
1227
+ original_headers = response.headers.copy()
1217
1228
  actual_content_length = None
1218
1229
 
1219
1230
  if range_start or range_stop:
@@ -1323,9 +1334,14 @@ class Handler:
1323
1334
  content_artifacts = await asyncio.shield(
1324
1335
  sync_to_async(self._save_artifact)(download_result, remote_artifact, request)
1325
1336
  )
1337
+ ca = content_artifacts[remote_artifact.content_artifact.relative_path]
1338
+ # If cache is enabled, add the future response to our stream response
1339
+ if settings.CACHE_ENABLED:
1340
+ response.future_response = self._build_response_from_content_artifact(
1341
+ ca, original_headers, request
1342
+ )
1326
1343
  # Try to add content to repository if present & supported
1327
1344
  if repository and repository.PULL_THROUGH_SUPPORTED:
1328
- ca = content_artifacts[remote_artifact.content_artifact.relative_path]
1329
1345
  await sync_to_async(repository.pull_through_add_content)(ca)
1330
1346
  await response.write_eof()
1331
1347