diff --git a/CHANGES/6137.feature b/CHANGES/6137.feature new file mode 100644 index 0000000000..91ae5a110d --- /dev/null +++ b/CHANGES/6137.feature @@ -0,0 +1 @@ +Added API for importing Pulp Exports at ``POST /importers/core/pulp//imports/``. diff --git a/CHANGES/6329.feature b/CHANGES/6329.feature new file mode 100644 index 0000000000..bac8035b93 --- /dev/null +++ b/CHANGES/6329.feature @@ -0,0 +1,2 @@ +Added PulpImporter API at ``/pulp/api/v3/importers/core/pulp/``. PulpImporters are used for +importing exports from Pulp. diff --git a/CHANGES/plugin_api/6329.feature b/CHANGES/plugin_api/6329.feature new file mode 100644 index 0000000000..122e58b829 --- /dev/null +++ b/CHANGES/plugin_api/6329.feature @@ -0,0 +1,2 @@ +Added models Import and Importer (as well as serializers and viewsets) that can be used for +importing data into Pulp. diff --git a/pulpcore/app/apps.py b/pulpcore/app/apps.py index 9ccd24a323..061493bc15 100644 --- a/pulpcore/app/apps.py +++ b/pulpcore/app/apps.py @@ -1,4 +1,3 @@ -import inspect from collections import defaultdict from importlib import import_module @@ -146,10 +145,7 @@ def import_modelresources(self): modelrsrc_module_name = '{name}.{module}'.format( name=self.name, module=MODELRESOURCE_MODULE_NAME) self.modelresource_module = import_module(modelrsrc_module_name) - self.exportable_classes = [] - for (classname, cls) in inspect.getmembers(self.modelresource_module, inspect.isclass): - if cls.__module__ == self.modelresource_module.__name__: - self.exportable_classes.append(cls) + self.exportable_classes = self.modelresource_module.IMPORT_ORDER class PulpAppConfig(PulpPluginAppConfig): diff --git a/pulpcore/app/migrations/0028_import_importer_pulpimporter_pulpimporterrepository.py b/pulpcore/app/migrations/0028_import_importer_pulpimporter_pulpimporterrepository.py new file mode 100644 index 0000000000..01f04661da --- /dev/null +++ b/pulpcore/app/migrations/0028_import_importer_pulpimporter_pulpimporterrepository.py @@ -0,0 +1,85 @@ +# Generated by Django 2.2.11 on 2020-04-13 16:39 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0027_export_backend'), + ] + + operations = [ + migrations.CreateModel( + name='Import', + fields=[ + ('pulp_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('pulp_created', models.DateTimeField(auto_now_add=True)), + ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), + ('params', django.contrib.postgres.fields.jsonb.JSONField(null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Importer', + fields=[ + ('pulp_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('pulp_created', models.DateTimeField(auto_now_add=True)), + ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), + ('pulp_type', models.TextField(default=None)), + ('name', models.TextField(db_index=True, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='PulpImport', + fields=[ + ('import_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='core_pulp_export', serialize=False, to='core.Import')), + ], + options={ + 'default_related_name': '%(app_label)s_pulp_export', + }, + bases=('core.import',), + ), + migrations.CreateModel( + name='PulpImporter', + fields=[ + ('importer_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='core_pulp_importer', serialize=False, to='core.Importer')), + ], + options={ + 'default_related_name': '%(app_label)s_pulp_importer', + }, + bases=('core.importer',), + ), + migrations.AddField( + model_name='import', + name='importer', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Importer'), + ), + migrations.AddField( + model_name='import', + name='task', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.Task'), + ), + migrations.CreateModel( + name='PulpImporterRepository', + fields=[ + ('pulp_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('pulp_created', models.DateTimeField(auto_now_add=True)), + ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), + ('source_repo', models.TextField()), + ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.Repository')), + ('pulp_importer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='repo_map', to='core.PulpImporter')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/pulpcore/app/modelresource.py b/pulpcore/app/modelresource.py index 81d8e0b873..6a713140cb 100644 --- a/pulpcore/app/modelresource.py +++ b/pulpcore/app/modelresource.py @@ -26,9 +26,16 @@ class QueryModelResource(resources.ModelResource): queryset (django.db.models.query.QuerySet): filtering queryset for this resource (driven by repo_version) """ + def set_up_queryset(self): + return None + def __init__(self, repo_version=None): self.repo_version = repo_version - self.queryset = None + if repo_version: + self.queryset = self.set_up_queryset() + + class Meta: + import_id_fields = ('pulp_id',) # @@ -52,18 +59,17 @@ class Meta: # follow the same pattern as a plugin writer would follow # class ContentResource(QueryModelResource): - def __init__(self, repo_version): - QueryModelResource.__init__(repo_version) - self.queryset = repo_version.content + def set_up_queryset(self): + return self.repo_version.content class Meta: model = Content + fields = ('pulp_id', 'pulp_type') class ContentArtifactResource(QueryModelResource): - def __init__(self, repo_version): - QueryModelResource.__init__(repo_version) - self.queryset = ContentArtifact.objects.filter(content__in=repo_version.content) + def set_up_queryset(self): + return ContentArtifact.objects.filter(content__in=self.repo_version.content) class Meta: model = ContentArtifact diff --git a/pulpcore/app/models/__init__.py b/pulpcore/app/models/__init__.py index e1921e3931..74d8da9b73 100644 --- a/pulpcore/app/models/__init__.py +++ b/pulpcore/app/models/__init__.py @@ -18,6 +18,12 @@ PulpExport, PulpExporter, ) +from .importer import ( # noqa + Import, + Importer, + PulpImport, + PulpImporter, +) from .publication import ( # noqa BaseDistribution, ContentGuard, diff --git a/pulpcore/app/models/importer.py b/pulpcore/app/models/importer.py new file mode 100644 index 0000000000..a5bc56c615 --- /dev/null +++ b/pulpcore/app/models/importer.py @@ -0,0 +1,84 @@ +from django.contrib.postgres.fields import JSONField +from django.db import models + +from pulpcore.app.models import ( + BaseModel, + MasterModel, +) +from .repository import Repository + + +class Import(BaseModel): + """ + A model that represents imports into Pulp. + + Fields: + params (models.JSONField): A set of parameters used to run the import + + Relations: + task (models.ForeignKey): The Task that ran the import + importer (models.ForeignKey): The Importer that imported the export + """ + params = JSONField(null=True) + task = models.ForeignKey("Task", on_delete=models.PROTECT) + importer = models.ForeignKey("Importer", on_delete=models.CASCADE) + + +class Importer(MasterModel): + """ + A base model that provides logic to import data into Pulp. + + Can be extended by plugins to provide import functionality. + + Fields: + name (models.TextField): The importer unique name. + """ + name = models.TextField(db_index=True, unique=True) + + +class PulpImporter(Importer): + """ + A model that can be used to import exports from other Pulp instances. + """ + TYPE = 'pulp' + + @property + def repo_mapping(self): + return {repo.source_repo: repo.repository.name for repo in self.repo_map.all()} + + @repo_mapping.setter + def repo_mapping(self, mapping): + self.repo_map.all().delete() + for source, repo_name in mapping.items(): + repo = Repository.objects.get(name=repo_name) + self.repo_map.create(source_repo=source, + repository=repo) + + class Meta: + default_related_name = '%(app_label)s_pulp_importer' + + +class PulpImporterRepository(BaseModel): + """ + A model that maps repo names in an export to repos in Pulp. + + Fields: + source_repo (models.TextField): The name of the repo in the export + + Relations: + pulp_importer (models.ForeignKey): The associated Pulp importer + repository (models.ForeignKey): The repository in Pulp + """ + source_repo = models.TextField() + pulp_importer = models.ForeignKey( + PulpImporter, + related_name="repo_map", + on_delete=models.CASCADE) + repository = models.ForeignKey(Repository, on_delete=models.CASCADE) + + +class PulpImport(Import): + """A model that represents imports into Pulp from another Pulp instance.""" + + class Meta: + default_related_name = '%(app_label)s_pulp_export' diff --git a/pulpcore/app/models/repository.py b/pulpcore/app/models/repository.py index ede423c02d..f73677de20 100644 --- a/pulpcore/app/models/repository.py +++ b/pulpcore/app/models/repository.py @@ -612,6 +612,20 @@ def remove_content(self, content): version_removed=None) q_set.update(version_removed=self) + def set_content(self, content): + """ + Sets the repo version content by calling remove_content() then add_content(). + + Args: + content (django.db.models.QuerySet): Set of desired content + + Raise: + pulpcore.exception.ResourceImmutableError: if set_content is called on a + complete RepositoryVersion + """ + self.remove_content(self.content.exclude(pk__in=content)) + self.add_content(content.exclude(pk__in=self.content)) + def next(self): """ Returns: diff --git a/pulpcore/app/serializers/__init__.py b/pulpcore/app/serializers/__init__.py index 48e9149329..a7ebd9c6d4 100644 --- a/pulpcore/app/serializers/__init__.py +++ b/pulpcore/app/serializers/__init__.py @@ -25,6 +25,9 @@ ExportsIdentityFromExporterField, ExportRelatedField, ExportIdentityField, + ImportsIdentityFromImporterField, + ImportRelatedField, + ImportIdentityField, LatestVersionField, SecretCharField, SingleContentArtifactField, @@ -42,6 +45,12 @@ PulpExporterSerializer, PulpExportSerializer, ) +from .importer import ( # noqa + ImportSerializer, + ImporterSerializer, + PulpImporterSerializer, + PulpImportSerializer, +) from .progress import ProgressReportSerializer # noqa from .publication import ( # noqa BaseDistributionSerializer, diff --git a/pulpcore/app/serializers/exporter.py b/pulpcore/app/serializers/exporter.py index 088a51dc27..d6c1c8bf5e 100644 --- a/pulpcore/app/serializers/exporter.py +++ b/pulpcore/app/serializers/exporter.py @@ -24,7 +24,7 @@ class ExporterSerializer(ModelSerializer): pulp_href = DetailIdentityField() name = serializers.CharField( help_text=_("Unique name of the file system exporter."), - validators=[UniqueValidator(queryset=models.BaseDistribution.objects.all())] + validators=[UniqueValidator(queryset=models.Exporter.objects.all())] ) @staticmethod diff --git a/pulpcore/app/serializers/fields.py b/pulpcore/app/serializers/fields.py index 31510f0171..c74920ce31 100644 --- a/pulpcore/app/serializers/fields.py +++ b/pulpcore/app/serializers/fields.py @@ -320,6 +320,48 @@ def get_object(self, view_name, view_args, view_kwargs): return self.get_queryset().get(**lookup_kwargs) +class ImportsIdentityFromImporterField(DetailIdentityField): + view_name = 'importers-detail' + + def __init__(self, view_name=None, **kwargs): + assert view_name is None, 'The `view_name` must not be set.' + super().__init__(view_name=self.view_name, **kwargs) + + def get_url(self, obj, view_name, request, *args, **kwargs): + return super().get_url(obj, self.view_name, request, *args, **kwargs) + "imports/" + + +class ImportFieldGetURLMixin: + view_name = 'imports-detail' + + def __init__(self, view_name=None, **kwargs): + assert view_name is None, 'The `view_name` must not be set.' + super().__init__(view_name=self.view_name, **kwargs) + + def get_url(self, obj, view_name, request, *args, **kwargs): + imports_field = ImportsIdentityFromImporterField() + importer_url = imports_field.get_url(obj.importer, None, request, *args, **kwargs) + return f"{importer_url}{obj.pk}/" + + def use_pk_only_optimization(self): + return False + + +class ImportIdentityField(ImportFieldGetURLMixin, IdentityField): + pass + + +class ImportRelatedField(ImportFieldGetURLMixin, RelatedField): + queryset = models.Import.objects.all() + + def get_object(self, view_name, view_args, view_kwargs): + lookup_kwargs = { + 'importer__pk': view_kwargs['importer_pk'], + 'pk': view_kwargs['pk'] + } + return self.get_queryset().get(**lookup_kwargs) + + class SecretCharField(serializers.CharField): """ Serializer field for secrets. diff --git a/pulpcore/app/serializers/importer.py b/pulpcore/app/serializers/importer.py new file mode 100644 index 0000000000..229ae9efea --- /dev/null +++ b/pulpcore/app/serializers/importer.py @@ -0,0 +1,114 @@ +import os +from gettext import gettext as _ + +from rest_framework import serializers +from rest_framework.validators import UniqueValidator + +from pulpcore.app import models, settings +from pulpcore.app.serializers import ( + DetailIdentityField, + ImportIdentityField, + ModelSerializer, + RelatedField, +) + + +class ImporterSerializer(ModelSerializer): + """Base serializer for Importers.""" + pulp_href = DetailIdentityField() + name = serializers.CharField( + help_text=_("Unique name of the Importer."), + validators=[UniqueValidator(queryset=models.Importer.objects.all())] + ) + + class Meta: + model = models.Importer + fields = ModelSerializer.Meta.fields + ('name',) + + +class ImportSerializer(ModelSerializer): + """Serializer for Imports.""" + pulp_href = ImportIdentityField() + + task = RelatedField( + help_text=_('A URI of the Task that ran the Import.'), + queryset=models.Task.objects.all(), + view_name='tasks-detail', + ) + + params = serializers.JSONField( + help_text=_('Any parameters that were used to create the import.'), + ) + + class Meta: + model = models.Importer + fields = ModelSerializer.Meta.fields + ('task', 'params') + + +class PulpImporterSerializer(ImporterSerializer): + """Serializer for PulpImporters.""" + repo_mapping = serializers.DictField( + child=serializers.CharField(), + help_text=_("Mapping of repo names in an export file to the repo names in Pulp. " + "For example, if the export has a repo named 'foo' and the repo to " + "import content into was 'bar', the mapping would be \"{'foo': 'bar'}\"."), + required=False + ) + + def create(self, validated_data): + """ + Save the PulpImporter and handle saving repo mapping. + + Args: + validated_data (dict): A dict of validated data to create the PulpImporter + + Raises: + ValidationError: When there's a problem with the repo mapping. + + Returns: + PulpImporter: the created PulpImporter + """ + repo_mapping = validated_data.pop("repo_mapping", {}) + importer = super().create(validated_data) + try: + importer.repo_mapping = repo_mapping + except Exception as err: + importer.delete() + raise serializers.ValidationError(_("Bad repo mapping: {}").format(err)) + else: + return importer + + class Meta: + model = models.PulpImporter + fields = ImporterSerializer.Meta.fields + ('repo_mapping',) + + +class PulpImportSerializer(ModelSerializer): + """Serializer for call to import into Pulp.""" + path = serializers.CharField( + help_text=_("Path to export that will be imported.") + ) + + def validate_path(self, value): + """ + Check if path is in ALLOWED_IMPORT_PATHS. + + Args: + value (str): The user-provided value path to be validated. + + Raises: + ValidationError: When path is not in the ALLOWED_IMPORT_PATHS setting. + + Returns: + The validated value. + """ + for allowed_path in settings.ALLOWED_IMPORT_PATHS: + user_provided_realpath = os.path.realpath(value) + if user_provided_realpath.startswith(allowed_path): + return value + raise serializers.ValidationError(_("Path '{}' is not an allowed import " + "path").format(value)) + + class Meta: + model = models.Import + fields = ('path',) diff --git a/pulpcore/app/tasks/__init__.py b/pulpcore/app/tasks/__init__.py index e91b35413b..7078cf71e5 100644 --- a/pulpcore/app/tasks/__init__.py +++ b/pulpcore/app/tasks/__init__.py @@ -2,4 +2,6 @@ from .export import fs_publication_export, fs_repo_version_export # noqa +from .importer import pulp_import # noqa + from .orphan import orphan_cleanup # noqa diff --git a/pulpcore/app/tasks/importer.py b/pulpcore/app/tasks/importer.py new file mode 100644 index 0000000000..70ee6d42bc --- /dev/null +++ b/pulpcore/app/tasks/importer.py @@ -0,0 +1,121 @@ +import json +import os +import tempfile +import tarfile +from gettext import gettext as _ +from logging import getLogger + +from django.conf import settings +from django.core.files.storage import default_storage +from tablib import Dataset + +from pulpcore.app.apps import get_plugin_config +from pulpcore.app.models import ( + Artifact, + Content, + CreatedResource, + PulpImport, + PulpImporter, + Repository, + Task, +) +from pulpcore.app.modelresource import ( + ArtifactResource, + ContentArtifactResource, + ContentResource, +) + +log = getLogger(__name__) + +ARTIFACT_FILE = "pulpcore.app.modelresource.ArtifactResource.json" +REPO_FILE = "pulpcore.app.modelresource.RepositoryResource.json" +CONTENT_FILE = "pulpcore.app.modelresource.ContentResource.json" +CA_FILE = "pulpcore.app.modelresource.ContentArtifactResource.json" + + +def pulp_import(importer_pk, path): + """ + Import a Pulp export into Pulp. + + Args: + importer_pk (str): Primary key of PulpImporter to do the import + path (str): Path to the export to be imported + """ + def import_file(fpath, resource_class): + log.info(_("Importing file {}.").format(fpath)) + with open(fpath, "r") as json_file: + data = Dataset().load(json_file.read(), format="json") + resource = resource_class() + return resource.import_data(data, raise_errors=True) + + def destination_repo(source_repo_name): + """Find the destination repository based on source repo's name.""" + if importer.repo_mapping and importer.repo_mapping.get(source_repo_name): + dest_repo_name = importer.repo_mapping[source_repo_name] + else: + dest_repo_name = source_repo_name + return Repository.objects.get(name=dest_repo_name) + + def repo_version_path(temp_dir, src_repo): + """Find the repo version path in the export based on src_repo json.""" + src_repo_version = int(src_repo["next_version"]) - 1 + return os.path.join(temp_dir, f"repository-{src_repo['pulp_id']}_{src_repo_version}") + + log.info(_("Importing {}.").format(path)) + importer = PulpImporter.objects.get(pk=importer_pk) + pulp_import = PulpImport.objects.create(importer=importer, + task=Task.current(), + params={"path": path}) + CreatedResource.objects.create(content_object=pulp_import) + + with tempfile.TemporaryDirectory() as temp_dir: + with tarfile.open(path, "r|gz") as tar: + tar.extractall(path=temp_dir) + + # Artifacts + ar_result = import_file(os.path.join(temp_dir, ARTIFACT_FILE), ArtifactResource) + for row in ar_result.rows: + artifact = Artifact.objects.get(pk=row.object_id) + base_path = os.path.join('artifact', artifact.sha256[0:2], artifact.sha256[2:]) + src = os.path.join(temp_dir, base_path) + dest = os.path.join(settings.MEDIA_ROOT, base_path) + + if not default_storage.exists(dest): + with open(src, 'rb') as f: + default_storage.save(dest, f) + + # Repo Versions + with open(os.path.join(temp_dir, REPO_FILE), "r") as repo_data_file: + data = json.load(repo_data_file) + + for src_repo in data: + try: + dest_repo = destination_repo(src_repo["name"]) + except Repository.DoesNotExist: + log.warn(_("Could not find destination repo for {}. " + "Skipping.").format(src_repo["name"])) + continue + + rv_path = repo_version_path(temp_dir, src_repo) + + # Untyped Content + content_path = os.path.join(rv_path, CONTENT_FILE) + c_result = import_file(content_path, ContentResource) + content = Content.objects.filter(pk__in=[r.object_id for r in c_result.rows]) + + # Content Artifacts + ca_path = os.path.join(rv_path, CA_FILE) + import_file(ca_path, ContentArtifactResource) + + # Content + plugin_name = src_repo["pulp_type"].split('.')[0] + cfg = get_plugin_config(plugin_name) + for res_class in cfg.exportable_classes: + filename = f"{res_class.__module__}.{res_class.__name__}.json" + import_file(os.path.join(rv_path, filename), res_class) + + # Create the repo version + with dest_repo.new_version() as new_version: + new_version.set_content(content) + + return importer diff --git a/pulpcore/app/viewsets/__init__.py b/pulpcore/app/viewsets/__init__.py index 28a3522340..c83fb3c0a9 100644 --- a/pulpcore/app/viewsets/__init__.py +++ b/pulpcore/app/viewsets/__init__.py @@ -24,6 +24,12 @@ PulpExporterViewSet, PulpExportViewSet, ) +from .importer import ( # noqa + ImportViewSet, + ImporterViewSet, + PulpImportViewSet, + PulpImporterViewSet, +) from .publication import ( # noqa BaseDistributionViewSet, ContentGuardFilter, diff --git a/pulpcore/app/viewsets/importer.py b/pulpcore/app/viewsets/importer.py new file mode 100644 index 0000000000..37326fb94c --- /dev/null +++ b/pulpcore/app/viewsets/importer.py @@ -0,0 +1,103 @@ +from django.http import Http404 +from django_filters.rest_framework import filters +from drf_yasg.utils import swagger_auto_schema +from rest_framework import mixins + +from pulpcore.app.models import ( + Import, + Importer, + PulpImport, + PulpImporter, +) +from pulpcore.app.response import OperationPostponedResponse +from pulpcore.app.serializers import ( + AsyncOperationResponseSerializer, + ImportSerializer, + ImporterSerializer, + PulpImporterSerializer, + PulpImportSerializer, +) +from pulpcore.app.tasks import pulp_import +from pulpcore.app.viewsets import ( + BaseFilterSet, + NamedModelViewSet, +) +from pulpcore.app.viewsets.base import NAME_FILTER_OPTIONS +from pulpcore.tasking.tasks import enqueue_with_reservation + + +class ImporterFilter(BaseFilterSet): + """Filter for Importers.""" + name = filters.CharFilter() + + class Meta: + model = Importer + fields = { + 'name': NAME_FILTER_OPTIONS, + } + + +class ImporterViewSet(NamedModelViewSet, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.DestroyModelMixin): + """ViewSet for Importers.""" + queryset = Importer.objects.all() + serializer_class = ImporterSerializer + endpoint_name = 'importers' + router_lookup = 'importer' + filterset_class = ImporterFilter + + +class ImportViewSet(NamedModelViewSet, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.DestroyModelMixin): + """ViewSet for viewing imports from an Importer.""" + endpoint_name = 'imports' + nest_prefix = 'importers' + router_lookup = 'import' + lookup_field = 'pk' + parent_viewset = ImporterViewSet + parent_lookup_kwargs = {'importer_pk': 'importer__pk'} + serializer_class = ImportSerializer + queryset = Import.objects.all() + + +class PulpImporterViewSet(ImporterViewSet): + """ViewSet for PulpImporters.""" + endpoint_name = 'pulp' + serializer_class = PulpImporterSerializer + queryset = PulpImporter.objects.all() + + +class PulpImportViewSet(ImportViewSet): + """ViewSet for PulpImports.""" + parent_viewset = PulpImporterViewSet + queryset = PulpImport.objects.all() + + @swagger_auto_schema( + request_body=PulpImportSerializer, + operation_description="Trigger an asynchronous task to import a Pulp export.", + responses={202: AsyncOperationResponseSerializer}, + ) + def create(self, request, importer_pk): + """Import a Pulp export into Pulp.""" + try: + importer = PulpImporter.objects.get(pk=importer_pk) + except PulpImporter.DoesNotExist: + raise Http404 + + serializer = PulpImportSerializer(data=request.data, context={"request": request}) + serializer.is_valid(raise_exception=True) + path = serializer.validated_data.get("path") + # resources = [pulp_importer] + + result = enqueue_with_reservation( + pulp_import, + [importer], + kwargs={"importer_pk": importer.pk, "path": path}, + ) + return OperationPostponedResponse(result, request) diff --git a/pulpcore/plugin/models/__init__.py b/pulpcore/plugin/models/__init__.py index 3a55485131..54e7a7b2d3 100644 --- a/pulpcore/plugin/models/__init__.py +++ b/pulpcore/plugin/models/__init__.py @@ -10,7 +10,10 @@ ContentArtifact, ContentGuard, CreatedResource, + Export, Exporter, + Import, + Importer, FileSystemExporter, MasterModel, ProgressReport, diff --git a/pulpcore/plugin/serializers/__init__.py b/pulpcore/plugin/serializers/__init__.py index 442e51d2f0..776131aa7e 100644 --- a/pulpcore/plugin/serializers/__init__.py +++ b/pulpcore/plugin/serializers/__init__.py @@ -5,7 +5,10 @@ BaseDistributionSerializer, ContentChecksumSerializer, ContentGuardSerializer, + ExportSerializer, ExporterSerializer, + ImportSerializer, + ImporterSerializer, FileSystemExporterSerializer, NoArtifactContentSerializer, SingleArtifactContentSerializer, diff --git a/pulpcore/plugin/viewsets/__init__.py b/pulpcore/plugin/viewsets/__init__.py index 3bf2bfa36f..69eedb2a3c 100644 --- a/pulpcore/plugin/viewsets/__init__.py +++ b/pulpcore/plugin/viewsets/__init__.py @@ -11,6 +11,8 @@ ContentViewSet, ExportViewSet, ExporterViewSet, + ImportViewSet, + ImporterViewSet, NamedModelViewSet, PublicationFilter, PublicationViewSet,