diff --git a/CHANGES/2909.feature b/CHANGES/2909.feature new file mode 100644 index 000000000..e7a0efc67 --- /dev/null +++ b/CHANGES/2909.feature @@ -0,0 +1 @@ +Added /rpm/prune/ endpoint to allow "pruning" old Packages from repositories. diff --git a/pulp_rpm/app/serializers/__init__.py b/pulp_rpm/app/serializers/__init__.py index 49308a5fd..66d9ac245 100644 --- a/pulp_rpm/app/serializers/__init__.py +++ b/pulp_rpm/app/serializers/__init__.py @@ -27,6 +27,7 @@ ModulemdObsoleteSerializer, ) from .package import PackageSerializer, MinimalPackageSerializer # noqa +from .prune import PrunePackagesSerializer # noqa from .repository import ( # noqa CopySerializer, RpmDistributionSerializer, diff --git a/pulp_rpm/app/serializers/prune.py b/pulp_rpm/app/serializers/prune.py new file mode 100644 index 000000000..d59781a09 --- /dev/null +++ b/pulp_rpm/app/serializers/prune.py @@ -0,0 +1,72 @@ +from gettext import gettext as _ + +from rest_framework import fields, serializers + +from pulp_rpm.app.models import RpmRepository + +from pulpcore.plugin.serializers import ValidateFieldsMixin +from pulpcore.plugin.util import get_domain + + +class PrunePackagesSerializer(serializers.Serializer, ValidateFieldsMixin): + """ + Serializer for prune-old-Packages operation. + """ + + repo_hrefs = fields.ListField( + required=True, + help_text=_( + "Will prune old packages from the specified list of repos. " + "Use ['*'] to specify all repos. " + "Will prune based on the specified repositories' latest_versions." + ), + child=serializers.CharField(), + ) + + keep_days = serializers.IntegerField( + help_text=_( + "Prune packages introduced *prior-to* this many days ago. " + "Default is 14. A value of 0 implies 'keep latest package only.'" + ), + required=False, + min_value=0, + default=14, + ) + + dry_run = serializers.BooleanField( + help_text=_( + "Determine what would-be-pruned and log the list of packages. " + "Intended as a debugging aid." + ), + default=False, + required=False, + ) + + def validate_repo_hrefs(self, value): + """ + Insure repo_hrefs is not empty and contains either valid RPM Repository hrefs or "*". + Args: + value (list): The list supplied by the user + Returns: + The list of RpmRepositories after validation + Raises: + ValidationError: If the list is empty or contains invalid hrefs. + """ + if len(value) == 0: + raise serializers.ValidationError("Must not be [].") + + # prune-all-repos is "*" - find all RPM repos in this domain + if "*" in value: + if len(value) != 1: + raise serializers.ValidationError("Can't specify specific HREFs when using '*'") + return RpmRepository.objects.filter(pulp_domain=get_domain()) + + from pulpcore.plugin.viewsets import NamedModelViewSet + + # We're pruning a specific list of RPM repositories. + # Validate that they are for RpmRepositories. + hrefs_to_return = [] + for href in value: + hrefs_to_return.append(NamedModelViewSet.get_resource(href, RpmRepository)) + + return hrefs_to_return diff --git a/pulp_rpm/app/settings.py b/pulp_rpm/app/settings.py index 02186b359..79add69ca 100644 --- a/pulp_rpm/app/settings.py +++ b/pulp_rpm/app/settings.py @@ -16,3 +16,4 @@ SOLVER_DEBUG_LOGS = True RPM_METADATA_USE_REPO_PACKAGE_TIME = False NOCACHE_LIST = ["repomd.xml", "repomd.xml.asc", "repomd.xml.key"] +PRUNE_WORKERS_MAX = 5 diff --git a/pulp_rpm/app/tasks/__init__.py b/pulp_rpm/app/tasks/__init__.py index 9a7407322..87c407ed0 100644 --- a/pulp_rpm/app/tasks/__init__.py +++ b/pulp_rpm/app/tasks/__init__.py @@ -2,3 +2,4 @@ from .synchronizing import synchronize # noqa from .copy import copy_content # noqa from .comps import upload_comps # noqa +from .prune import prune_packages # noqa diff --git a/pulp_rpm/app/tasks/prune.py b/pulp_rpm/app/tasks/prune.py new file mode 100644 index 000000000..4489988f8 --- /dev/null +++ b/pulp_rpm/app/tasks/prune.py @@ -0,0 +1,169 @@ +from datetime import datetime, timedelta +from logging import getLogger, DEBUG + +from django.conf import settings +from django.db.models import F, Subquery +from django.utils import timezone + +from pulpcore.plugin.models import ProgressReport +from pulpcore.plugin.constants import TASK_STATES +from pulpcore.plugin.models import ( + GroupProgressReport, + RepositoryContent, + TaskGroup, +) +from pulpcore.plugin.tasking import dispatch +from pulp_rpm.app.models.package import Package +from pulp_rpm.app.models.repository import RpmRepository + +log = getLogger(__name__) + + +def prune_repo_packages(repo_pk, keep_days, dry_run): + """ + This task prunes old Packages from the latest_version of the specified repository. + + Args: + repo_pk (UUID): UUID of the RpmRepository to be pruned. + keep_days(int): Keep RepositoryContent created less than this many days ago. + dry_run (boolean): If True, don't actually do the prune, just log to-be-pruned Packages. + """ + repo = RpmRepository.objects.filter(pk=repo_pk).get() + curr_vers = repo.latest_version() + eldest_datetime = datetime.now(tz=timezone.utc) - timedelta(days=keep_days) + log.info(f"PRUNING REPOSITORY {repo.name}.") + log.debug(f">>> TOTAL RPMS: {curr_vers.get_content(Package.objects).count()}") + + # We only care about RPM-Names that have more than one EVRA - "singles" are always kept. + rpm_by_name_age = ( + curr_vers.get_content(Package.objects.with_age()) + .filter(age__gt=1) + .order_by("name", "epoch", "version", "release", "arch") + .values("pk") + ) + log.debug(f">>> NAME/AGE COUNT {rpm_by_name_age.count()}") + log.debug( + ">>> # NAME/ARCH w/ MULTIPLE EVRs: {}".format( + curr_vers.get_content(Package.objects) + .filter(pk__in=rpm_by_name_age) + .values("name", "arch") + .distinct() + .count() + ) + ) + log.debug( + ">>> # UNIQUE NAME/ARCHS: {}".format( + curr_vers.get_content(Package.objects).values("name", "arch").distinct().count() + ) + ) + + # Find the RepositoryContents associated with the multi-EVR-names from above, + # whose maximum-pulp-created date is LESS THAN eldest_datetime. + # + # Note that we can "assume" the latest-date is an "add" with no "remove", since we're + # limiting ourselves to the list of ids that we know are in the repo's current latest-version! + target_ids_q = ( + RepositoryContent.objects.filter( + content__in=Subquery(rpm_by_name_age), repository=repo, version_removed=None + ) + .filter(pulp_created__lt=eldest_datetime) + .values("content_id") + ) + log.debug(f">>> TARGET IDS: {target_ids_q.count()}.") + to_be_removed = target_ids_q.count() + # Use the progressreport to report back numbers. The prune happens as one + # action. + data = dict( + message=f"Pruning {repo.name}", + code="rpm.package.prune.repository", + total=to_be_removed, + state=TASK_STATES.COMPLETED, + done=0, + ) + + if dry_run: + if log.getEffectiveLevel() == DEBUG: # Don't go through the loop unless debugging + log.debug(">>> Packages to be removed : ") + for p in ( + Package.objects.filter(pk__in=target_ids_q) + .order_by("name", "epoch", "version", "release", "arch") + .values("name", "epoch", "version", "release", "arch") + ): + log.debug(f'{p["name"]}-{p["epoch"]}:{p["version"]}-{p["release"]}.{p["arch"]}') + else: + with repo.new_version(base_version=None) as new_version: + new_version.remove_content(target_ids_q) + data["done"] = to_be_removed + + pb = ProgressReport(**data) + pb.save() + + # Report back that this repo has completed. + gpr = TaskGroup.current().group_progress_reports.filter(code="rpm.package.prune") + gpr.update(done=F("done") + 1) + + +def prune_packages( + repo_pks, + keep_days=14, + dry_run=False, +): + """ + This task prunes old Packages from the latest_version of the specified list of repos. + + "Old" in this context is defined by the RepositoryContent record that added a Package + to the repository in question. + + It will issue one task-per-repository. + + Kwargs: + repo_pks (list): A list of repo pks the pruning is performed on. + keep_days(int): Keep RepositoryContent created less than this many days ago. + repo_concurrency (int): number of repos to prune at a time. + dry_run (boolean): If True, don't actually do the prune, just record to-be-pruned Packages.. + """ + + repos_to_prune = RpmRepository.objects.filter(pk__in=repo_pks) + task_group = TaskGroup.current() + + # We want to be able to limit the number of available-workers that prune will consume, + # so that pulp can continue to work while pruning many repositories. We accomplish this by + # creating a reserved-resource string for each repo-prune-task based on that repo's index in + # the dispatch loop, mod number-of-workers-to-consume. + # + # By default, prune will consume up to 5 workers. + # + # (This comment and code below based on + # https://github.com/pulp/pulpcore/blob/main/pulpcore/app/tasks/importer.py#L503-L512 + # When we have a generic-approach to throttling mass-task-spawning, both places should + # be refactored to take advantage thereof. + prune_workers = int(settings.get("PRUNE_WORKERS_MAX", 5)) + + gpr = GroupProgressReport( + message="Pruning old Packages", + code="rpm.package.prune", + total=len(repo_pks), + done=0, + task_group=task_group, + ) + gpr.save() + + # Dispatch a task-per-repository. + # Lock on the the repository *and* to insure the max-concurrency specified. + # This will keep an "all repositories" prune from locking up all the workers + # until all repositories are completed. + for index, a_repo in enumerate(repos_to_prune): + worker_rsrc = f"rpm-prune-worker-{index % prune_workers}" + exclusive_resources = [worker_rsrc, a_repo] + + dispatch( + prune_repo_packages, + exclusive_resources=exclusive_resources, + args=( + a_repo.pk, + keep_days, + dry_run, + ), + task_group=task_group, + ) + task_group.finish() diff --git a/pulp_rpm/app/urls.py b/pulp_rpm/app/urls.py index e90889e2d..3b51fc673 100644 --- a/pulp_rpm/app/urls.py +++ b/pulp_rpm/app/urls.py @@ -1,7 +1,7 @@ from django.conf import settings from django.urls import path -from .viewsets import CopyViewSet, CompsXmlViewSet +from .viewsets import CopyViewSet, CompsXmlViewSet, PrunePackagesViewSet if settings.DOMAIN_ENABLED: V3_API_ROOT = settings.V3_DOMAIN_API_ROOT_NO_FRONT_SLASH @@ -11,4 +11,5 @@ urlpatterns = [ path(f"{V3_API_ROOT}rpm/copy/", CopyViewSet.as_view({"post": "create"})), path(f"{V3_API_ROOT}rpm/comps/", CompsXmlViewSet.as_view({"post": "create"})), + path(f"{V3_API_ROOT}rpm/prune/", PrunePackagesViewSet.as_view({"post": "prune_packages"})), ] diff --git a/pulp_rpm/app/viewsets/__init__.py b/pulp_rpm/app/viewsets/__init__.py index cdf64bbbe..9d5f0aeed 100644 --- a/pulp_rpm/app/viewsets/__init__.py +++ b/pulp_rpm/app/viewsets/__init__.py @@ -11,6 +11,7 @@ from .distribution import DistributionTreeViewSet # noqa from .modulemd import ModulemdViewSet, ModulemdDefaultsViewSet, ModulemdObsoleteViewSet # noqa from .package import PackageViewSet # noqa +from .prune import PrunePackagesViewSet # noqa from .repository import ( # noqa RpmRepositoryViewSet, RpmRepositoryVersionViewSet, diff --git a/pulp_rpm/app/viewsets/prune.py b/pulp_rpm/app/viewsets/prune.py new file mode 100644 index 000000000..1037871e1 --- /dev/null +++ b/pulp_rpm/app/viewsets/prune.py @@ -0,0 +1,72 @@ +from drf_spectacular.utils import extend_schema +from django.conf import settings +from rest_framework.viewsets import ViewSet + +from pulpcore.plugin.viewsets import TaskGroupOperationResponse +from pulpcore.plugin.models import TaskGroup +from pulpcore.plugin.serializers import TaskGroupOperationResponseSerializer +from pulp_rpm.app.serializers import PrunePackagesSerializer +from pulp_rpm.app.tasks import prune_packages +from pulpcore.plugin.tasking import dispatch + + +class PrunePackagesViewSet(ViewSet): + """ + Viewset for prune-old-Packages endpoint. + """ + + serializer_class = PrunePackagesSerializer + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["prune_packages"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_domain_or_obj_perms:rpm.modify_content_rpmrepository", + "has_repository_model_or_domain_or_obj_perms:rpm.view_rpmrepository", + ], + }, + ], + } + + @extend_schema( + description="Trigger an asynchronous old-Package-prune operation.", + responses={202: TaskGroupOperationResponseSerializer}, + ) + def prune_packages(self, request): + """ + Triggers an asynchronous old-Package-purge operation. + + This returns a task-group that contains a "master" task that dispatches one task + per repo being pruned. This allows repositories to become available for other + processing as soon as their task completes, rather than having to wait for *all* + repositories to be pruned. + """ + serializer = PrunePackagesSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + repos = serializer.validated_data.get("repo_hrefs", []) + repos_to_prune_pks = [] + for repo in repos: + repos_to_prune_pks.append(repo.pk) + + uri = "/api/v3/rpm/prune/" + if settings.DOMAIN_ENABLED: + uri = f"/{request.pulp_domain.name}{uri}" + exclusive_resources = [uri, f"pdrn:{request.pulp_domain.pulp_id}:rpm:prune"] + + task_group = TaskGroup.objects.create(description="Prune old Packages.") + + dispatch( + prune_packages, + exclusive_resources=exclusive_resources, + task_group=task_group, + kwargs={ + "repo_pks": repos_to_prune_pks, + "keep_days": serializer.validated_data["keep_days"], + "dry_run": serializer.validated_data["dry_run"], + }, + ) + return TaskGroupOperationResponse(task_group, request) diff --git a/pulp_rpm/tests/conftest.py b/pulp_rpm/tests/conftest.py index 4450539db..c1d4ff311 100644 --- a/pulp_rpm/tests/conftest.py +++ b/pulp_rpm/tests/conftest.py @@ -10,6 +10,7 @@ RemotesRpmApi, RepositoriesRpmApi, RepositoriesRpmVersionsApi, + RpmPruneApi, RpmRepositorySyncURL, ) @@ -36,6 +37,12 @@ def rpm_distribution_api(rpm_client): return DistributionsRpmApi(rpm_client) +@pytest.fixture(scope="session") +def rpm_prune_api(rpm_client): + """Fixture for RPM Prune API.""" + return RpmPruneApi(rpm_client) + + @pytest.fixture(scope="session") def rpm_client(bindings_cfg): """Fixture for RPM client.""" diff --git a/pulp_rpm/tests/functional/api/test_prune.py b/pulp_rpm/tests/functional/api/test_prune.py new file mode 100644 index 000000000..543909361 --- /dev/null +++ b/pulp_rpm/tests/functional/api/test_prune.py @@ -0,0 +1,126 @@ +import pytest + +from pulp_rpm.tests.functional.utils import set_up_module as setUpModule # noqa:F401 + +from pulpcore.client.pulp_rpm import PrunePackages +from pulpcore.client.pulp_rpm.exceptions import ApiException + + +def test_01_prune_params(init_and_sync, rpm_prune_api, monitor_task_group): + """Assert on various param-validation errors.""" + # create/sync rpm repo + repo, _ = init_and_sync(policy="on_demand") + + params = PrunePackages(repo_hrefs=[]) + # requires repo-href or * + with pytest.raises(ApiException) as exc: + rpm_prune_api.prune_packages(params) + assert "Must not be []" in exc.value.body + + params.repo_hrefs = ["foo"] + with pytest.raises(ApiException) as exc: + rpm_prune_api.prune_packages(params) + assert "URI not valid" in exc.value.body + + params.repo_hrefs = ["*", repo.pulp_href] + with pytest.raises(ApiException) as exc: + rpm_prune_api.prune_packages(params) + assert "Can't specify specific HREFs when using" in exc.value.body + + # '*' only' + params.repo_hrefs = ["*"] + params.dry_run = True + params.keep_days = 1000 + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 0 == task_group.failed + + # Valid repo-href + params.repo_hrefs = [repo.pulp_href] + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == task_group.completed + assert 0 == task_group.failed + + # Valid +int + params.keep_days = 1000 + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == task_group.completed + assert 0 == task_group.failed + + # Valid 0 + params.keep_days = 0 + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == task_group.completed + assert 0 == task_group.failed + + # Valid dry-run + params.dry_run = True + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == task_group.completed + assert 0 == task_group.failed + + +def test_02_prune_dry_run(init_and_sync, rpm_prune_api, monitor_task_group, monitor_task): + # create/sync rpm repo + repo, _ = init_and_sync(policy="on_demand") + + # prune keep=0 dry_run=True -> expect total=4 done=0 + params = PrunePackages(repo_hrefs=[repo.pulp_href], keep_days=0, dry_run=True) + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == len(task_group.tasks) + assert 2 == task_group.completed + assert 0 == task_group.failed + assert 1 == len(task_group.group_progress_reports) + + prog_rpt = task_group.group_progress_reports[0] + assert 1 == prog_rpt.done + assert 1 == prog_rpt.total + for t in task_group.tasks: + if t.name == "pulp_rpm.app.tasks.prune.prune_repo_packages": + prune_task = monitor_task(t.pulp_href) + assert 1 == len(prune_task.progress_reports) + assert 4 == prune_task.progress_reports[0].total + assert 0 == prune_task.progress_reports[0].done + + # prune keep=1000 dry_run=True -> expect total=0 done=0 + params = PrunePackages(repo_hrefs=[repo.pulp_href], keep_days=1000, dry_run=True) + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == len(task_group.tasks) + assert 2 == task_group.completed + assert 0 == task_group.failed + for t in task_group.tasks: + if t.name == "pulp_rpm.app.tasks.prune.prune_repo_packages": + prune_task = monitor_task(t.pulp_href) + assert 1 == len(prune_task.progress_reports) + assert 0 == prune_task.progress_reports[0].total + assert 0 == prune_task.progress_reports[0].done + + +def test_03_prune_results( + init_and_sync, + rpm_prune_api, + monitor_task_group, + monitor_task, + rpm_repository_api, + rpm_repository_version_api, +): + # create/sync rpm repo + repo, _ = init_and_sync(policy="on_demand") + + # prune keep=0 dry_run=False -> expect total=4 done=4 + params = PrunePackages(repo_hrefs=[repo.pulp_href], keep_days=0, dry_run=False) + task_group = monitor_task_group(rpm_prune_api.prune_packages(params).task_group) + assert 2 == len(task_group.tasks) + assert 2 == task_group.completed + assert 0 == task_group.failed + + for t in task_group.tasks: + if t.name == "pulp_rpm.app.tasks.prune.prune_repo_packages": + prune_task = monitor_task(t.pulp_href) + assert 1 == len(prune_task.progress_reports) + assert 4 == prune_task.progress_reports[0].total + assert 4 == prune_task.progress_reports[0].done + + # investigate content -> 4 fewer packages, correct dups gone + repo2 = rpm_repository_api.read(repo.pulp_href) + rv = rpm_repository_version_api.read(repo2.latest_version_href) + assert 4 == rv.content_summary.removed["rpm.package"]["count"] diff --git a/staging_docs/user/guides/06-prune.md b/staging_docs/user/guides/06-prune.md new file mode 100644 index 000000000..ef8be50b4 --- /dev/null +++ b/staging_docs/user/guides/06-prune.md @@ -0,0 +1,144 @@ +# Prune Repository Content + +A workflow that can be useful for specific kinds of installation is the "prune" workflow. +For repositories that see frequent updates followed by long periods of stability, it can +be desirable to eventually "age out" RPMs that have been superseded, after a period of time. + +The `/pulp/api/v3/rpm/prune/` API exists to provide to the repository-owner/admin a tool to +accomplish this workflow. + +- `repo_hrefs` allows the user to specify a list of specific `RpmRepository` HREFs, or +the wildcard "*" to prune all repositories available in the user's domain. +- `keep_days` allows the user to specify the number of days to allow "old" content to remain in the +repository. The default is 14 days. +- `dry_run` is available as a debugging tool. Instead of actually-pruning, it will log to Pulp's system +log the Packages it **would have pruned**, while making no actual changes. + +This workflow will operate on the `latest_version` of the specified RpmRepositor(ies), creating a new RepositoryVersion +with the pruned list of Packages. All the "standard rules" apply at that point: + +- Space is not reclaimed unless the older versions are deleted/removed (e.g., older versions are removed manually or `retain_repository_versions` is 1) and orphan-cleanup runs. +- The version must be published to generate the repo-metadata reflecting the new content (e.g., a new Publication is created or`autopublish` is `True`). +- The version will not be available until it is Distributed (e.g. a Distribution is created to point to the new Publication or a Distribution exists that points at the **Repository** directly) + +!!! note + + Support for `/prune/` is not yet available in `pulp-cli`. Until it is, this relies on the direct REST calls + to invoke the API. + +!!! note + + This workflow dispatches a separate task for each repository being pruned. In order to avoid using all available + workers (and hence blocking regular Pulp processing), the prune workflow will consume no more workers than are + specified by the `PRUNE_WORKERS_MAX` setting, defaulting to 5. + +## Example + +=== "Prune a repository" + + ```bash + $ http POST :5001/pulp/api/v3/rpm/prune/ \ + repo_hrefs:='["/pulp/api/v3/repositories/rpm/rpm/018f73d1-8ba2-779c-8956-854b33b6899c/"]' \ + keep_days=0 \ + dry_run=True + ``` + +=== "Output" + + ```json + { + "task_group": "/pulp/api/v3/task-groups/018f7468-a024-7330-b65e-991203d49064/" + } + $ pulp show --href /pulp/api/v3/task-groups/018f7468-a024-7330-b65e-991203d49064/ + { + "pulp_href": "/pulp/api/v3/task-groups/018f7468-a024-7330-b65e-991203d49064/", + "description": "Prune old Packages.", + "all_tasks_dispatched": true, + "waiting": 0, + "skipped": 0, + "running": 0, + "completed": 2, + "canceled": 0, + "failed": 0, + "canceling": 0, + "group_progress_reports": [ + { + "message": "Pruning old Packages", + "code": "rpm.package.prune", + "total": 1, + "done": 1, + "suffix": null + } + ], + "tasks": [ + { + "pulp_href": "/pulp/api/v3/tasks/018f7468-a058-7e11-a57d-db9096eb16bc/", + "pulp_created": "2024-05-14T00:02:44.953250Z", + "pulp_last_updated": "2024-05-14T00:02:44.953262Z", + "name": "pulp_rpm.app.tasks.prune.prune_packages", + "state": "completed", + "unblocked_at": "2024-05-14T00:02:44.974458Z", + "started_at": "2024-05-14T00:02:45.042580Z", + "finished_at": "2024-05-14T00:02:45.262785Z", + "worker": "/pulp/api/v3/workers/018f743a-d793-78df-b3e9-7cca5e20b99b/" + }, + { + "pulp_href": "/pulp/api/v3/tasks/018f7468-a16f-7da0-a530-67bcbd003d6a/", + "pulp_created": "2024-05-14T00:02:45.231599Z", + "pulp_last_updated": "2024-05-14T00:02:45.231611Z", + "name": "pulp_rpm.app.tasks.prune.prune_repo_packages", + "state": "completed", + "unblocked_at": "2024-05-14T00:02:45.258585Z", + "started_at": "2024-05-14T00:02:45.321801Z", + "finished_at": "2024-05-14T00:02:45.504732Z", + "worker": "/pulp/api/v3/workers/018f743a-d85b-77d8-80e7-53850c2b878c/" + } + ] + } + ``` + +=== "Show Spawned Task" + + ```bash + $ pulp task show --href /pulp/api/v3/tasks/018f7468-a16f-7da0-a530-67bcbd003d6a/ + ``` + +=== "Output" + + ```json + { + "pulp_href": "/pulp/api/v3/tasks/018f7468-a16f-7da0-a530-67bcbd003d6a/", + "pulp_created": "2024-05-14T00:02:45.231599Z", + "pulp_last_updated": "2024-05-14T00:02:45.231611Z", + "state": "completed", + "name": "pulp_rpm.app.tasks.prune.prune_repo_packages", + "logging_cid": "9553043efcb74085a32606569f230610", + "created_by": "/pulp/api/v3/users/1/", + "unblocked_at": "2024-05-14T00:02:45.258585Z", + "started_at": "2024-05-14T00:02:45.321801Z", + "finished_at": "2024-05-14T00:02:45.504732Z", + "error": null, + "worker": "/pulp/api/v3/workers/018f743a-d85b-77d8-80e7-53850c2b878c/", + "parent_task": "/pulp/api/v3/tasks/018f7468-a058-7e11-a57d-db9096eb16bc/", + "child_tasks": [], + "task_group": "/pulp/api/v3/task-groups/018f7468-a024-7330-b65e-991203d49064/", + "progress_reports": [ + { + "message": "Pruning unfoo", + "code": "rpm.package.prune.repository", + "state": "completed", + "total": 4, + "done": 0, + "suffix": null + } + ], + "created_resources": [], + "reserved_resources_record": [ + "prn:rpm.rpmrepository:018f73d1-8ba2-779c-8956-854b33b6899c", + "/pulp/api/v3/repositories/rpm/rpm/018f73d1-8ba2-779c-8956-854b33b6899c/", + "rpm-prune-worker-0", + "shared:prn:core.domain:018e770d-1009-786d-a08a-36acd238d229", + "shared:/pulp/api/v3/domains/018e770d-1009-786d-a08a-36acd238d229/" + ] + } + ``` \ No newline at end of file diff --git a/staging_docs/user/guides/modify.md b/staging_docs/user/guides/modify.md index 541600ce3..e4c4ca560 100644 --- a/staging_docs/user/guides/modify.md +++ b/staging_docs/user/guides/modify.md @@ -1,6 +1,6 @@ # Modify Repository Content -Modyfing existing Repository Content lets you filter what content you want in a Repository. +Modifying existing Repository Content lets you filter what content you want in a Repository. Keep in mind that none of these operations introduces new Content or deletes a Content from a Pulp instance. To populate Pulp, see [Post and Delete Content](site:pulp_rpm/docs/user/guides/02-upload/) or [Create, Sync and Publish a Repository](site:pulp_rpm/docs/user/tutorials/01-create_sync_publish/).