diff --git a/pulpcore/app/migrations/0001_initial.py b/pulpcore/app/migrations/0001_initial.py index 6b48a73dc3b..d61482c6868 100644 --- a/pulpcore/app/migrations/0001_initial.py +++ b/pulpcore/app/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.1 on 2019-05-08 18:30 +# Generated by Django 2.2.1 on 2019-05-08 21:46 from django.conf import settings import django.core.validators @@ -376,6 +376,18 @@ class Migration(migrations.Migration): }, bases=('core.progressreport',), ), + migrations.CreateModel( + name='RepositoryVersionDistribution', + fields=[ + ('basedistribution_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='_repository_version_distributions', serialize=False, to='core.BaseDistribution')), + ('repository', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_repository_version_distributions', to='core.Repository')), + ('repository_version', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_repository_version_distributions', to='core.RepositoryVersion')), + ], + options={ + 'default_related_name': '_repository_version_distributions', + }, + bases=('core.basedistribution',), + ), migrations.CreateModel( name='RemoteArtifact', fields=[ @@ -425,19 +437,17 @@ class Migration(migrations.Migration): ], options={ 'default_related_name': 'published_artifact', - 'unique_together': {('publication', 'content_artifact'), ('publication', 'relative_path')}, + 'unique_together': {('publication', 'relative_path'), ('publication', 'content_artifact')}, }, ), migrations.CreateModel( - name='Distribution', + name='PublicationDistribution', fields=[ - ('basedistribution_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='_distributions', serialize=False, to='core.BaseDistribution')), - ('publication', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_distributions', to='core.Publication')), - ('repository', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_distributions', to='core.Repository')), - ('repository_version', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_distributions', to='core.RepositoryVersion')), + ('basedistribution_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='_publication_distributions', serialize=False, to='core.BaseDistribution')), + ('publication', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='_publication_distributions', to='core.Publication')), ], options={ - 'default_related_name': '_distributions', + 'default_related_name': '_publication_distributions', }, bases=('core.basedistribution',), ), diff --git a/pulpcore/app/models/__init__.py b/pulpcore/app/models/__init__.py index e43e3c966be..dd5cc24a9bf 100644 --- a/pulpcore/app/models/__init__.py +++ b/pulpcore/app/models/__init__.py @@ -6,10 +6,11 @@ from .publication import ( # noqa ContentGuard, BaseDistribution, - Distribution, Publication, + PublicationDistribution, PublishedArtifact, - PublishedMetadata + PublishedMetadata, + RepositoryVersionDistribution, ) from .repository import ( # noqa Exporter, diff --git a/pulpcore/app/models/publication.py b/pulpcore/app/models/publication.py index b098eda2935..e5c505eeb85 100644 --- a/pulpcore/app/models/publication.py +++ b/pulpcore/app/models/publication.py @@ -232,23 +232,38 @@ class BaseDistribution(MasterModel): remote = models.ForeignKey(Remote, null=True, on_delete=models.SET_NULL) -class Distribution(BaseDistribution): +class PublicationDistribution(BaseDistribution): """ - A distribution defines how a publication is distributed by Pulp's content app. - - The use of repository, repository_version, or publication are mutually exclusive, and this - model's serializer will raise a ValidationError if they are used together. + Define how Pulp's content app will serve a :term:`Publication`. Relations: publication (models.ForeignKey): Publication to be served. + """ + + TYPE = 'publication_distributions' + + publication = models.ForeignKey(Publication, null=True, on_delete=models.SET_NULL) + + class Meta: + default_related_name = '_publication_distributions' + + +class RepositoryVersionDistribution(BaseDistribution): + """ + Define how Pulp's content app will serve a :term:`RepositoryVersion` or :term:`Repository`. + + The ``repository`` and ``repository_version`` fields cannot be used together. + + Relations: repository (models.ForeignKey): The latest RepositoryVersion for this Repository will be served. repository_version (models.ForeignKey): RepositoryVersion to be served. """ - publication = models.ForeignKey(Publication, null=True, on_delete=models.SET_NULL) + TYPE = 'repository_version_distributions' + repository = models.ForeignKey(Repository, null=True, on_delete=models.SET_NULL) repository_version = models.ForeignKey(RepositoryVersion, null=True, on_delete=models.SET_NULL) class Meta: - default_related_name = '_distributions' + default_related_name = '_repository_version_distributions' diff --git a/pulpcore/app/serializers/__init__.py b/pulpcore/app/serializers/__init__.py index 9c2ee51ee43..07f5156daf7 100644 --- a/pulpcore/app/serializers/__init__.py +++ b/pulpcore/app/serializers/__init__.py @@ -35,8 +35,9 @@ from .publication import ( # noqa BaseDistributionSerializer, ContentGuardSerializer, - DistributionSerializer, PublicationSerializer, + PublicationDistributionSerializer, + RepositoryVersionDistributionSerializer, ) from .repository import ( # noqa ExporterSerializer, diff --git a/pulpcore/app/serializers/publication.py b/pulpcore/app/serializers/publication.py index d67f3a6db93..94bd77ade84 100644 --- a/pulpcore/app/serializers/publication.py +++ b/pulpcore/app/serializers/publication.py @@ -21,7 +21,7 @@ class PublicationSerializer(MasterModelSerializer): _href = DetailIdentityField() - _distributions = DetailRelatedField( + _publication_distributions = DetailRelatedField( help_text=_('This publication is currently being served as' 'defined by these distributions.'), many=True, @@ -71,8 +71,8 @@ class Meta: abstract = True model = models.Publication fields = MasterModelSerializer.Meta.fields + ( + '_publication_distributions', 'publisher', - '_distributions', 'repository_version', 'repository' ) @@ -100,6 +100,21 @@ class Meta: class BaseDistributionSerializer(MasterModelSerializer): _href = DetailIdentityField() + base_path = serializers.CharField( + help_text=_('The base (relative) path component of the published url. Avoid paths that \ + overlap with other distribution base paths (e.g. "foo" and "foo/bar")'), + validators=[validators.MaxLengthValidator( + models.BaseDistribution._meta.get_field('base_path').max_length, + message=_('`base_path` length must be less than {} characters').format( + models.BaseDistribution._meta.get_field('base_path').max_length + )), + UniqueValidator(queryset=models.BaseDistribution.objects.all()), + ] + ) + base_url = BaseURLField( + source='base_path', read_only=True, + help_text=_('The URL for accessing the publication as defined by this distribution.') + ) content_guard = DetailRelatedField( required=False, help_text=_('An optional content-guard.'), @@ -107,13 +122,13 @@ class BaseDistributionSerializer(MasterModelSerializer): allow_null=True ) name = serializers.CharField( - help_text=_('A unique distribution name. Ex, `rawhide` and `stable`.'), + help_text=_('A unique name. Ex, `rawhide` and `stable`.'), validators=[validators.MaxLengthValidator( - models.Distribution._meta.get_field('name').max_length, - message=_('Distribution name length must be less than {} characters').format( - models.Distribution._meta.get_field('name').max_length + models.BaseDistribution._meta.get_field('name').max_length, + message=_('`name` length must be less than {} characters').format( + models.BaseDistribution._meta.get_field('name').max_length )), - UniqueValidator(queryset=models.Distribution.objects.all())] + UniqueValidator(queryset=models.BaseDistribution.objects.all())] ) remote = DetailRelatedField( required=False, @@ -124,61 +139,13 @@ class BaseDistributionSerializer(MasterModelSerializer): class Meta: fields = ModelSerializer.Meta.fields + ( + 'base_path', + 'base_url', 'content_guard', 'name', 'remote', ) - -class DistributionSerializer(BaseDistributionSerializer): - base_path = serializers.CharField( - help_text=_('The base (relative) path component of the published url. Avoid paths that \ - overlap with other distribution base paths (e.g. "foo" and "foo/bar")'), - validators=[validators.MaxLengthValidator( - models.Distribution._meta.get_field('base_path').max_length, - message=_('Distribution base_path length must be less than {} characters').format( - models.Distribution._meta.get_field('base_path').max_length - )), - UniqueValidator(queryset=models.Distribution.objects.all()), - ] - ) - base_url = BaseURLField( - source='base_path', read_only=True, - help_text=_('The URL for accessing the publication as defined by this distribution.') - ) - publication = DetailRelatedField( - required=False, - help_text=_('Publication to be served'), - queryset=models.Publication.objects.exclude(complete=False), - allow_null=True - ) - repository = RelatedField( - required=False, - help_text=_('The latest RepositoryVersion for this Repository will be served.'), - queryset=models.Repository.objects.all(), - view_name='repositories-detail', - allow_null=True - ) - repository_version = NestedRelatedField( - required=False, - help_text=_('RepositoryVersion to be served'), - queryset=models.RepositoryVersion.objects.exclude(complete=False), - view_name='versions-detail', - allow_null=True, - lookup_field='number', - parent_lookup_kwargs={'repository_pk': 'repository__pk'}, - ) - - class Meta: - model = models.Distribution - fields = BaseDistributionSerializer.Meta.fields + ( - 'base_path', - 'base_url', - 'publication', - 'repository', - 'repository_version', - ) - def _validate_path_overlap(self, path): # look for any base paths nested in path search = path.split("/")[0] @@ -208,14 +175,50 @@ def validate_base_path(self, path): def validate(self, data): super().validate(data) - mutex_keys = ['publication', 'repository', 'repository_version'] - in_use_keys = [] - for mkey in mutex_keys: - if mkey in data: - in_use_keys.append(mkey) - - if len(in_use_keys) > 1: - msg = _("The attributes {keys} must be used exclusively.".format(keys=in_use_keys)) + if 'repository' in data and 'repository_version' in data: + msg = _("The attributes 'repository' and 'repository_version' must be used" + "exclusively.") raise serializers.ValidationError(msg) return data + + +class PublicationDistributionSerializer(BaseDistributionSerializer): + publication = DetailRelatedField( + required=False, + help_text=_('Publication to be served'), + queryset=models.Publication.objects.exclude(complete=False), + allow_null=True + ) + + class Meta: + model = models.PublicationDistribution + fields = BaseDistributionSerializer.Meta.fields + ( + 'publication', + ) + + +class RepositoryVersionDistributionSerializer(BaseDistributionSerializer): + repository = RelatedField( + required=False, + help_text=_('The latest RepositoryVersion for this Repository will be served.'), + queryset=models.Repository.objects.all(), + view_name='repositories-detail', + allow_null=True + ) + repository_version = NestedRelatedField( + required=False, + help_text=_('RepositoryVersion to be served'), + queryset=models.RepositoryVersion.objects.exclude(complete=False), + view_name='versions-detail', + allow_null=True, + lookup_field='number', + parent_lookup_kwargs={'repository_pk': 'repository__pk'}, + ) + + class Meta: + model = models.RepositoryVersionDistribution + fields = BaseDistributionSerializer.Meta.fields + ( + 'repository', + 'repository_version', + ) diff --git a/pulpcore/app/viewsets/__init__.py b/pulpcore/app/viewsets/__init__.py index 4970a0bb751..b92da04b834 100644 --- a/pulpcore/app/viewsets/__init__.py +++ b/pulpcore/app/viewsets/__init__.py @@ -18,8 +18,9 @@ from .publication import ( # noqa ContentGuardFilter, ContentGuardViewSet, - DistributionViewSet, + PublicationDistributionViewSet, PublicationViewSet, + RepositoryVersionDistributionViewSet, ) from .repository import ( # noqa ExporterViewSet, diff --git a/pulpcore/app/viewsets/publication.py b/pulpcore/app/viewsets/publication.py index 4ebda018ae5..e6d8715a122 100644 --- a/pulpcore/app/viewsets/publication.py +++ b/pulpcore/app/viewsets/publication.py @@ -4,13 +4,15 @@ from pulpcore.app.models import ( ContentGuard, - Distribution, + PublicationDistribution, Publication, + RepositoryVersionDistribution, ) from pulpcore.app.serializers import ( ContentGuardSerializer, - DistributionSerializer, + PublicationDistributionSerializer, PublicationSerializer, + RepositoryVersionDistributionSerializer, ) from pulpcore.app.viewsets import ( AsyncCreateMixin, @@ -55,52 +57,55 @@ class ContentGuardViewSet(NamedModelViewSet, filterset_class = ContentGuardFilter -class DistributionFilter(BaseFilterSet): - # e.g. - # /?name=foo - # /?name__in=foo,bar - # /?base_path__contains=foo - # /?base_path__icontains=foo - name = filters.CharFilter() - base_path = filters.CharFilter() - - class Meta: - model = Distribution - fields = { - 'name': NAME_FILTER_OPTIONS, - 'base_path': ['exact', 'contains', 'icontains', 'in'] - } - - -class DistributionViewSet(NamedModelViewSet, - mixins.RetrieveModelMixin, - mixins.ListModelMixin, - AsyncCreateMixin, - AsyncRemoveMixin, - AsyncUpdateMixin): +# class DistributionFilter(BaseFilterSet): +# # e.g. +# # /?name=foo +# # /?name__in=foo,bar +# # /?base_path__contains=foo +# # /?base_path__icontains=foo +# name = filters.CharFilter() +# base_path = filters.CharFilter() +# +# class Meta: +# model = Distribution +# fields = { +# 'name': NAME_FILTER_OPTIONS, +# 'base_path': ['exact', 'contains', 'icontains', 'in'] +# } + + +class BaseDistributionViewSet(NamedModelViewSet, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + AsyncCreateMixin, + AsyncRemoveMixin, + AsyncUpdateMixin): """ Provides read and list methods and also provides asynchronous CUD methods to dispatch tasks with reservation that lock all Distributions preventing race conditions during base_path checking. """ - endpoint_name = 'distributions' - queryset = Distribution.objects.all() - serializer_class = DistributionSerializer - filterset_class = DistributionFilter def async_reserved_resources(self, instance): """Return resource that locks all Distributions.""" return "/api/v3/distributions/" - @classmethod - def is_master_viewset(cls): - """ - Declare DistributionViewSet to be a master ViewSet. - - This Viewset is a master ViewSet despite that its associated Model isn't - a master model. This prevents registering the ViewSet with a router. - """ - if cls is DistributionViewSet: - return True - else: - return super().is_master_viewset() + +class PublicationDistributionViewSet(BaseDistributionViewSet): + """ + ViewSet for PublicationDistribution objects. + """ + + endpoint_name = 'publication' + queryset = PublicationDistribution.objects.all() + serializer_class = PublicationDistributionSerializer + + +class RepositoryVersionDistributionViewSet(BaseDistributionViewSet): + """ + ViewSet for RepositoryVersionDistribution objects. + """ + + endpoint_name = 'repository_version' + queryset = RepositoryVersionDistribution.objects.all() + serializer_class = RepositoryVersionDistributionSerializer diff --git a/pulpcore/content/handler.py b/pulpcore/content/handler.py index 47670cc4ecf..df989b31862 100644 --- a/pulpcore/content/handler.py +++ b/pulpcore/content/handler.py @@ -11,7 +11,7 @@ from django.conf import settings from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db import IntegrityError, transaction -from pulpcore.app.models import Artifact, ContentArtifact, Distribution, Remote, RemoteArtifact +from pulpcore.app.models import Artifact, ContentArtifact, BaseDistribution, Remote, RemoteArtifact log = logging.getLogger(__name__) @@ -94,14 +94,14 @@ def _match_distribution(path): path (str): The path component of the URL. Returns: - Distribution: The matched distribution. + BaseDistribution: The matched distribution. Raises: PathNotResolved: when not matched. """ base_paths = Handler._base_paths(path) try: - return Distribution.objects.get(base_path__in=base_paths) + return BaseDistribution.objects.get(base_path__in=base_paths) except ObjectDoesNotExist: log.debug(_('Distribution not matched for {path} using: {base_paths}').format( path=path, base_paths=base_paths @@ -117,7 +117,8 @@ def _permit(request, distribution): Args: request (:class:`aiohttp.web.Request`): A request for a published file. - distribution (:class:`pulpcore.plugin.models.Distribution`): The matched distribution. + distribution (:class:`pulpcore.plugin.models.BaseDistribution`): The matched + distribution. Raises: :class:`aiohttp.web_exceptions.HTTPForbidden`: When not permitted.