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.
- pulp_certguard/app/__init__.py +1 -1
- pulp_file/app/__init__.py +1 -1
- pulp_file/tests/functional/api/test_remote_settings.py +133 -2
- pulpcore/app/apps.py +1 -1
- pulpcore/app/replica.py +4 -4
- pulpcore/app/serializers/publication.py +30 -10
- pulpcore/app/serializers/repository.py +49 -3
- pulpcore/app/settings.py +13 -1
- pulpcore/app/tasks/__init__.py +1 -1
- pulpcore/app/tasks/base.py +37 -0
- pulpcore/app/tasks/test.py +9 -6
- pulpcore/app/viewsets/base.py +2 -2
- pulpcore/cache/cache.py +8 -3
- pulpcore/constants.py +2 -0
- pulpcore/content/handler.py +53 -37
- pulpcore/plugin/tasking.py +2 -2
- pulpcore/pytest_plugin.py +2 -0
- pulpcore/tasking/_util.py +46 -16
- pulpcore/tasking/tasks.py +44 -4
- pulpcore/tests/functional/api/test_tasking.py +151 -1
- pulpcore/tests/functional/api/using_plugin/test_content_cache.py +18 -2
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/METADATA +8 -5
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/RECORD +27 -27
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/WHEEL +1 -1
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/entry_points.txt +0 -0
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/licenses/LICENSE +0 -0
- {pulpcore-3.76.1.dist-info → pulpcore-3.77.1.dist-info}/top_level.txt +0 -0
pulp_certguard/app/__init__.py
CHANGED
pulp_file/app/__init__.py
CHANGED
|
@@ -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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
pulpcore/app/tasks/__init__.py
CHANGED
|
@@ -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
|
pulpcore/app/tasks/base.py
CHANGED
|
@@ -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()
|
pulpcore/app/tasks/test.py
CHANGED
|
@@ -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
|
-
|
|
14
|
+
time.sleep(interval)
|
|
13
15
|
|
|
14
|
-
|
|
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
|
-
|
|
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()
|
pulpcore/app/viewsets/base.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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 = (
|
pulpcore/content/handler.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
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
|
|