Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add fedora-ostree-pruner #79

Merged
merged 6 commits into from
Dec 13, 2022
Merged

Conversation

dustymabe
Copy link
Member

This will be run in Fedora's infrastructure and will prune out OSTree
repos that are used to serve content to Fedora users.

@dustymabe
Copy link
Member Author

ok this is ready for an initial round of review. I've done some initial tests, but need to run more

prune_prod_repo(args.test)

# If we were asked to run in a loop, then run once a day
if args.loop:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason I wanted to run this in a loop rather than run it as part of a kubernetes cron jobs is because historically it's been really hard to get logs from openshift pods in fedora infra. I've recently learned we have a kibana instance and should be able to get logs more easily. I'll try to confirm that and then maybe we'll convert this into a cronjob..

on the topic of frequency. Pruning is actually pretty I/O intensive (since it has to read a lot from the repo just to see if there is anything to prune) and slow on these large repos. I think maybe we should just run it on like Friday or Saturday (once a week) during the night.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason I wanted to run this in a loop rather than run it as part of a kubernetes cron jobs is because historically it's been really hard to get logs from openshift pods in fedora infra. I've recently learned we have a kibana instance and should be able to get logs more easily. I'll try to confirm that and then maybe we'll convert this into a cronjob..

I've found getting logs from the kibana instance to be unreliable. Never really got it to work well. So I stand by this.

on the topic of frequency. Pruning is actually pretty I/O intensive (since it has to read a lot from the repo just to see if there is anything to prune) and slow on these large repos. I think maybe we should just run it on like Friday or Saturday (once a week) during the night.

But I still think we should do this once a week during the night. Maybe I'll convert this into a systemd timer (run inside the container).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally would stick with this approach over a systemd timer.

Yeah, agreed this will be a very heavy I/O operation. Every seven days sounds reasonable to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the problem is trying to make it happen at a specific day/time. basically what happens here is it waits X period of time, runs (could take a really long time to run) then waits X period of time. It's offset by the time it took to run, so you get an inconsistent pattern. Maybe there is a python package for "sleep until X time on X day" that we could use instead.

@dustymabe
Copy link
Member Author

here is some output from running this against the prod repo from composer.stg.phx2.fedoraproject.org:

2020-02-20 20:39:53,976 INFO fedora-ostree-pruner - Running command: ['ostree', 'refs', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo']
2020-02-20 20:39:54,135 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/x86_64/updates/silverblue
2020-02-20 20:39:54,136 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/x86_64/testing/silverblue
2020-02-20 20:39:54,136 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/x86_64/silverblue
2020-02-20 20:39:54,136 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/x86_64/updates/silverblue
2020-02-20 20:39:54,136 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/x86_64/silverblue
2020-02-20 20:39:54,137 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/x86_64/updates/silverblue
2020-02-20 20:39:54,137 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/aarch64/updates/silverblue
2020-02-20 20:39:54,137 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/aarch64/testing/silverblue
2020-02-20 20:39:54,137 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/aarch64/silverblue
2020-02-20 20:39:54,138 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/aarch64/updates/silverblue
2020-02-20 20:39:54,138 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/aarch64/testing/silverblue
2020-02-20 20:39:54,138 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/aarch64/silverblue
2020-02-20 20:39:54,139 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/aarch64/updates/silverblue
2020-02-20 20:39:54,139 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/aarch64/silverblue
2020-02-20 20:39:54,139 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/aarch64/updates/silverblue
2020-02-20 20:39:54,139 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/29/aarch64/silverblue
2020-02-20 20:39:54,139 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/29/aarch64/updates/silverblue
2020-02-20 20:39:54,140 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/ppc64le/updates/silverblue
2020-02-20 20:39:54,140 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/32/ppc64le/testing/silverblue
2020-02-20 20:39:54,140 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/ppc64le/silverblue
2020-02-20 20:39:54,140 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/ppc64le/updates/silverblue
2020-02-20 20:39:54,141 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/30/ppc64le/testing/silverblue
2020-02-20 20:39:54,141 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/ppc64le/silverblue
2020-02-20 20:39:54,141 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/27/ppc64le/updates/silverblue
2020-02-20 20:39:54,141 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/ppc64le/silverblue
2020-02-20 20:39:54,141 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/28/ppc64le/updates/silverblue
2020-02-20 20:39:54,142 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/29/ppc64le/silverblue
2020-02-20 20:39:54,142 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/29/ppc64le/updates/silverblue
2020-02-20 20:39:54,142 WARNING fedora-ostree-pruner - policy defined for a ref that is not in the repo /mnt/fedora_koji_prod/koji/ostree/repo: fedora/x86_64/coreos/next
2020-02-20 20:39:54,142 INFO fedora-ostree-pruner - Pruning the fedora/rawhide/x86_64/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-20 20:39:54,143 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/rawhide/x86_64/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8588923
Would delete: 799802 objects, freeing 72.1 GB
2020-02-20 22:55:28,092 INFO fedora-ostree-pruner - Deleting the fedora/rawhide/x86_64/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-20 22:55:28,171 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-20 22:55:28,176 INFO fedora-ostree-pruner - Skipping ref fedora/32/x86_64/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-20 22:55:28,177 INFO fedora-ostree-pruner - Skipping ref fedora/31/x86_64/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-20 22:55:28,178 INFO fedora-ostree-pruner - Skipping ref fedora/31/x86_64/updates/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-20 22:55:28,179 INFO fedora-ostree-pruner - Pruning the fedora/31/x86_64/testing/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-20 22:55:28,183 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/31/x86_64/testing/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8605883
Would delete: 141097 objects, freeing 27.5 GB
2020-02-21 01:25:26,458 INFO fedora-ostree-pruner - Skipping ref fedora/30/x86_64/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 01:25:26,532 INFO fedora-ostree-pruner - Skipping ref fedora/30/x86_64/updates/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 01:25:26,536 INFO fedora-ostree-pruner - Pruning the fedora/30/x86_64/testing/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-21 01:25:26,547 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/30/x86_64/testing/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8608186
Would delete: 141340 objects, freeing 32.6 GB
2020-02-21 03:46:13,406 INFO fedora-ostree-pruner - Pruning the fedora/29/x86_64/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-21 03:46:13,462 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/29/x86_64/silverblue', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8624071
Would delete: 161783 objects, freeing 39.0 GB
2020-02-21 06:01:40,920 INFO fedora-ostree-pruner - Pruning the fedora/29/x86_64/updates/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-21 06:01:40,980 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/29/x86_64/updates/silverblue', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8624071
Would delete: 98824 objects, freeing 20.3 GB
2020-02-21 08:27:31,047 INFO fedora-ostree-pruner - Deleting the fedora/29/x86_64/testing/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,095 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,095 INFO fedora-ostree-pruner - Deleting the fedora/27/x86_64/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,096 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,096 INFO fedora-ostree-pruner - Deleting the fedora/27/x86_64/updates/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,096 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,096 INFO fedora-ostree-pruner - Deleting the fedora/27/x86_64/testing/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,097 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,097 INFO fedora-ostree-pruner - Deleting the fedora/28/x86_64/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,097 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,097 INFO fedora-ostree-pruner - Deleting the fedora/28/x86_64/updates/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,098 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,098 INFO fedora-ostree-pruner - Deleting the fedora/28/x86_64/testing/workstation ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 08:27:31,098 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 08:27:31,099 INFO fedora-ostree-pruner - Pruning the fedora/rawhide/aarch64/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-21 08:27:31,105 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/rawhide/aarch64/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8624071
Would delete: 512361 objects, freeing 46.2 GB
2020-02-21 10:49:02,410 INFO fedora-ostree-pruner - Skipping ref fedora/32/aarch64/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 10:49:02,459 INFO fedora-ostree-pruner - Skipping ref fedora/31/aarch64/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 10:49:02,460 INFO fedora-ostree-pruner - Skipping ref fedora/31/aarch64/updates/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 10:49:02,461 INFO fedora-ostree-pruner - Pruning the fedora/31/aarch64/testing/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-21 10:49:02,463 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/31/aarch64/testing/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8624071
Would delete: 103799 objects, freeing 21.9 GB
2020-02-21 13:07:50,295 INFO fedora-ostree-pruner - Pruning the fedora/rawhide/ppc64le/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-21 13:07:50,347 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/rawhide/ppc64le/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8624071
Would delete: 447962 objects, freeing 46.2 GB
2020-02-21 15:21:37,103 INFO fedora-ostree-pruner - Skipping ref fedora/32/ppc64le/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 15:21:37,179 INFO fedora-ostree-pruner - Skipping ref fedora/31/ppc64le/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 15:21:37,180 INFO fedora-ostree-pruner - Skipping ref fedora/31/ppc64le/updates/silverblue in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-21 15:21:37,182 INFO fedora-ostree-pruner - Pruning the fedora/31/ppc64le/testing/silverblue ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to time:90
2020-02-21 15:21:37,191 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/31/ppc64le/testing/silverblue', '--refs-only', '--keep-younger-than=90 days ago', '--no-prune']
Total objects: 8625236
Would delete: 104752 objects, freeing 22.2 GB
2020-02-21 17:43:10,533 INFO fedora-ostree-pruner - Deleting the fedora/rawhide/x86_64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 17:43:10,577 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 17:43:10,584 INFO fedora-ostree-pruner - Pruning the fedora/27/x86_64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-21 17:43:10,589 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/27/x86_64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8625236
Would delete: 98824 objects, freeing 20.3 GB
2020-02-21 20:00:25,043 INFO fedora-ostree-pruner - Pruning the fedora/28/x86_64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-21 20:00:25,088 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/28/x86_64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8625236
Would delete: 105048 objects, freeing 27.6 GB
2020-02-21 22:13:38,134 INFO fedora-ostree-pruner - Deleting the fedora/28/x86_64/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 22:13:38,207 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 22:13:38,208 INFO fedora-ostree-pruner - Deleting the fedora/28/x86_64/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-21 22:13:38,209 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-21 22:13:38,210 INFO fedora-ostree-pruner - Pruning the fedora/29/x86_64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-21 22:13:38,222 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/29/x86_64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8625236
Would delete: 98824 objects, freeing 20.3 GB
2020-02-22 00:30:56,997 INFO fedora-ostree-pruner - Deleting the fedora/29/x86_64/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 00:30:57,045 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 00:30:57,046 INFO fedora-ostree-pruner - Deleting the fedora/29/x86_64/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 00:30:57,046 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 00:30:57,046 INFO fedora-ostree-pruner - Deleting the fedora/rawhide/aarch64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 00:30:57,046 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 00:30:57,047 INFO fedora-ostree-pruner - Pruning the fedora/27/aarch64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 00:30:57,061 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/27/aarch64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 100236 objects, freeing 20.9 GB
2020-02-22 02:57:57,359 INFO fedora-ostree-pruner - Pruning the fedora/28/aarch64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 02:57:57,424 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/28/aarch64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 108919 objects, freeing 27.1 GB
2020-02-22 05:15:30,618 INFO fedora-ostree-pruner - Deleting the fedora/28/aarch64/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 05:15:30,671 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 05:15:30,671 INFO fedora-ostree-pruner - Deleting the fedora/28/aarch64/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 05:15:30,671 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 05:15:30,676 INFO fedora-ostree-pruner - Pruning the fedora/29/aarch64/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 05:15:30,686 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/29/aarch64/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 116433 objects, freeing 41.6 GB
2020-02-22 07:32:04,133 INFO fedora-ostree-pruner - Deleting the fedora/29/aarch64/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 07:32:04,206 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 07:32:04,206 INFO fedora-ostree-pruner - Deleting the fedora/29/aarch64/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 07:32:04,207 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 07:32:04,208 INFO fedora-ostree-pruner - Deleting the fedora/rawhide/ppc64le/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 07:32:04,208 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 07:32:04,210 INFO fedora-ostree-pruner - Pruning the fedora/27/ppc64le/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 07:32:04,213 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/27/ppc64le/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 98824 objects, freeing 20.3 GB
2020-02-22 09:56:07,133 INFO fedora-ostree-pruner - Pruning the fedora/28/ppc64le/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 09:56:07,191 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/28/ppc64le/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 98824 objects, freeing 20.3 GB
2020-02-22 12:13:49,769 INFO fedora-ostree-pruner - Deleting the fedora/28/ppc64le/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 12:13:49,825 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 12:13:49,826 INFO fedora-ostree-pruner - Deleting the fedora/28/ppc64le/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 12:13:49,826 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 12:13:49,827 INFO fedora-ostree-pruner - Pruning the fedora/29/ppc64le/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo to depth:0
2020-02-22 12:13:49,834 INFO fedora-ostree-pruner - Running command: ['ostree', 'prune', '--repo', '/mnt/fedora_koji_prod/koji/ostree/repo', '--only-branch', 'fedora/29/ppc64le/atomic-host', '--refs-only', '--depth=0', '--no-prune']
Total objects: 8626648
Would delete: 116059 objects, freeing 43.5 GB
2020-02-22 14:23:33,310 INFO fedora-ostree-pruner - Deleting the fedora/29/ppc64le/updates/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 14:23:33,364 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 14:23:33,364 INFO fedora-ostree-pruner - Deleting the fedora/29/ppc64le/testing/atomic-host ref in repo /mnt/fedora_koji_prod/koji/ostree/repo.
2020-02-22 14:23:33,364 INFO fedora-ostree-pruner - Skipping delete because of test mode
2020-02-22 14:23:33,365 INFO fedora-ostree-pruner - Skipping ref fedora/x86_64/coreos/testing in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits
2020-02-22 14:23:33,365 INFO fedora-ostree-pruner - Skipping ref fedora/x86_64/coreos/stable in repo /mnt/fedora_koji_prod/koji/ostree/repo. Policy is to keep all commits

@dustymabe
Copy link
Member Author

ok i added a commit which switches to a generic prune for content for deleted refs. This should be a little more efficient than running a prune for each deleted ref.

@dustymabe
Copy link
Member Author

Talked to @jlebon about the safety of running the pruner concurrently with other applications. Pasting here for posterity:

14:16:34 @dustymabe | jlebon: so you mentioned concerns about the pruner the other day
14:16:53 @dustymabe | can you reiterate what those were again. something about locking
14:18:14     jlebon | it's mostly a theoretical concern.  essentially, most of the apps interacting with
                    | the OSTree repo use the ostree CLI
14:18:55     jlebon | and while e.g. the API for pruning does take the lock, we'd be releasing it in
                    | between invocations each time
14:19:45     jlebon | so imagining one app querying information from the repo at the same time as the
                    | pruner is running can lead to a TOCTOU race
14:20:03 @dustymabe | jlebon: so let me talk about what the pruner does and see if there are corner cases
                    | where we do perceive a problem
14:20:23 @dustymabe | so the pruner has a policy for each ref in the repo
14:20:46 @dustymabe | the policy could be to delete a ref or to prune some number of commits from a ref
14:21:03 @dustymabe | we would only ever delete refs we're no longer updating
14:21:50 @dustymabe | and we'd never change the head of any ref we're not deleting
14:22:27 @dustymabe | all prune operations we do that are depth or time based are scoped to the ref
14:22:49 @dustymabe | there is one other prune operation we do, but it only deletes objects that are
                    | unreachable by any ref (i.e. we just deleted a ref)
14:26:18     jlebon | hmm ok, i think this should be fine
14:27:01          * | jlebon is thinking some more
14:27:50 @dustymabe | right. so can we think of any case where the pruner is going to update the repo in
                    | a way that would cause one of our other writers to get out of date information
14:29:28     jlebon | ok yeah, i think we can just rely on ostree's native locking assuming the refs
                    | don't overlap at all
14:30:00 @dustymabe | k
14:30:33 @dustymabe | jlebon: when ostree is called and something else has the lock, does it wait and
                    | then execuate after the lock is released, or does it throw an error?
14:31:17     jlebon | hmm, i think the default is blocking. let me double check
14:32:17     jlebon | ahh default is 30s actually
14:32:29     jlebon | you can override this with lock-timeout-secs=-1

@dustymabe
Copy link
Member Author

> 14:30:33 @dustymabe | jlebon: when ostree is called and something else has the lock, does it wait and
>                     | then execuate after the lock is released, or does it throw an error?
> 14:31:17     jlebon | hmm, i think the default is blocking. let me double check
> 14:32:17     jlebon | ahh default is 30s actually
> 14:32:29     jlebon | you can override this with lock-timeout-secs=-1

I guess the question is, should we implement that here? I think probbaly not and just let the default behavior apply.

Comment on lines +26 to +30
OSTREECOMPOSEREPO = '/mnt/koji/compose/ostree/repo'
OSTREEPRODREPO = '/mnt/koji/ostree/repo'
OSTREECOMPOSEREPO = '/mnt/fedora_koji_prod/koji/compose/ostree/repo'
OSTREEPRODREPO = '/mnt/fedora_koji_prod/koji/ostree/repo'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it's there because I can test this out on some composer staget machine (which has read-only mounts under /mnt/fedora_koji_prod/koji) will remove before final push.

# The # of commits to retain in the compose repo for each branch
COMPOSE_REPO_POLICY = 'depth:60'

# The policy for each ref in the prod repo. None means "keep all"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be good here to document the syntax.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still need to take a stab at this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok took a stab at updating the comment with the syntax

PROD_REF_POLICIES[f'fedora/{arch}/coreos/{stream}'] = None

# Beneath this is code, no config needed here
def runcmd(cmd: list, **kwargs: int) -> subprocess.CompletedProcess:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that int annotation correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Show resolved Hide resolved
Comment on lines 31 to 90
FEDORA_STABLE_LIST = [33, 34, 35]
FEDORA_EOL_LIST = [27, 28, 29, 30, 31, 32]

ATOMIC_HOST_ARCHES = ['x86_64', 'aarch64', 'ppc64le']
SILVERBLUE_ARCHES = ['x86_64', 'aarch64', 'ppc64le'] # Applies to Kinoite
FEDORA_COREOS_ARCHES = ['x86_64', 'aarch64']

# https://github.com/coreos/fedora-coreos-tracker/blob/master/stream-tooling.md#introduction
FEDORA_COREOS_PRODUCTION_STREAMS = ['next', 'testing', 'stable']

# The # of commits to retain in the compose repo for each branch
COMPOSE_REPO_POLICY = 'depth:60'

# The policy for each ref in the prod repo. None means "keep all"
PROD_REF_POLICIES = dict()
for arch in SILVERBLUE_ARCHES:
# Keep only the last 90 days of rawhide Silverblue/Kinoite
PROD_REF_POLICIES[f'fedora/rawhide/{arch}/silverblue'] = 'time:90'
PROD_REF_POLICIES[f'fedora/rawhide/{arch}/kinoite'] = 'time:90'
PROD_REF_POLICIES[f'fedora/rawhide/{arch}/workstation'] = 'delete'
# For Silverblue/Kinoite stable keep all stable/updates (they are aliased). For testing,
# keep just the last 90 days.
for release in FEDORA_STABLE_LIST:
PROD_REF_POLICIES[f'fedora/{release}/{arch}/silverblue'] = None
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/silverblue'] = None
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/silverblue'] = 'time:90'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/kinoite'] = None
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/kinoite'] = None
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/kinoite'] = 'time:90'
# For EOL Silverblue/Kinoite since the updates ref and stable ref are aliased
# we'll specify depth of 1 for both of those.
for release in FEDORA_EOL_LIST:
PROD_REF_POLICIES[f'fedora/{release}/{arch}/silverblue'] = 'depth:0'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/silverblue'] = 'depth:0'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/silverblue'] = 'delete'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/kinoite'] = 'depth:0'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/kinoite'] = 'depth:0'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/kinoite'] = 'delete'
# Delete any references to Atomic Workstation
for release in FEDORA_EOL_LIST:
PROD_REF_POLICIES[f'fedora/{release}/{arch}/workstation'] = 'delete'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/workstation'] = 'delete'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/workstation'] = 'delete'
for arch in ATOMIC_HOST_ARCHES:
# Delete all atomic host rawhide
PROD_REF_POLICIES[f'fedora/rawhide/{arch}/atomic-host'] = 'delete'
# For EOL ATOMIC HOST we keep only the last commit on the stable ref
for release in FEDORA_EOL_LIST:
PROD_REF_POLICIES[f'fedora/{release}/{arch}/atomic-host'] = 'depth:0'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/updates/atomic-host'] = 'delete'
PROD_REF_POLICIES[f'fedora/{release}/{arch}/testing/atomic-host'] = 'delete'
for arch in FEDORA_COREOS_ARCHES:
# For production Fedora CoreOS Streams we don't prune anything right now
for stream in FEDORA_COREOS_PRODUCTION_STREAMS:
PROD_REF_POLICIES[f'fedora/{arch}/coreos/{stream}'] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ideally all this would be a separate JSON or YAML file instead so we clearly delineate configuration from code. Then it could eventually be a configmap instead of just baked into the container.

OK to start like this too though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I started here because I couldn't find a compact way to represent all of what we're doing here in a config file.. Here I'm using for loops to apply configuration to many different branches. ideas on best way to expose this for yaml?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it would need some design work. Maybe we can circle back to this after the first prune since at least all the deleted refs will be gone.

fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
@cgwalters
Copy link
Member

It might be useful to set up something like coreos/fedora-coreos-pipeline#359 except run before the pruner, and run against all active commits so that we know we have those backed up as container images, and can rely on container registry GC.

@cgwalters
Copy link
Member

Ultimately, I think we should consider the ostree repos to be more like a cache. We should be able to rebuild and get "the same thing". (And really, we should think of RPMs in the same way, more like a cache of compiling the source code like Nix does)

I think a first problem here is getting to a repository of manageable size for which future GCs are more reasonable.

To that end, one approach might be to:

  • Create a new temporary repo
  • Import each ref to the depth we care about (as listed here)
  • Swap the temp repo with the live one
  • Delete the old repo at some later point

(This suggestion really parallels my one above to use rpm-ostree ex-container export on those refs)

Now, regarding questions around concurrency of writes and pruning - in general today, ostree has a very dumb "lock the world" approach to GC - it's not "mark and sweep". In general, if one has a prune running concurrently with writes, it is certainly possible for prune to delete an object which is expected to exist by an incoming write.

However, my instinct says that in practice this is going to be quite unlikely. It'd need to be something like trying to revert the tip of a ref to a package that existed before the prune cutoff.

In the end, if we detect that the tip of a ref somehow lost an object, well, we can just rebuild that thing right?

If the "GC via repo swap" approach works well enough, that also seems fine to me to continue as a SOP. I haven't done any benchmarking but my instinct says that when the amount of "dead objects" grows to 20% or more of live data (and here it's probably like 10x more dead objects than live, right?) it's better to make the operation O(live data) instead of O(all data) like ostree prune is.

We could probably make the "prune via swap" be a more first-class thing in ostree, like ostree prune --swap or so.

@dustymabe
Copy link
Member Author

Ultimately, I think we should consider the ostree repos to be more like a cache. We should be able to rebuild and get "the same thing".

I think this is true for CoreOS because we have the tarball (or OCI archive) for each build in our original build archive so we can rebuild and get a new set of Fedora CoreOS commits from scratch. For Silverblue and Kinoite, I think the only place the content lives is in the repo.

@cgwalters
Copy link
Member

cgwalters commented Oct 25, 2021

Yes, we have the OCI archive for FCOS, and but we can easily do so for other editions too - rpm-ostree ex-container export is explicitly designed for this use case. (Side note, I try to avoid using the bare term "Silverblue")

For Silverblue and Kinoite, I think the only place the content lives is in the repo.

It would be embarassing if we somehow lost the recent ostree commits for them, but hardly fatal. The client will happily upgrade to a new commit built from the same RPMs. (zincati and the upgrade graph I think care more about this though for FCOS)

But, I don't see any reason to put the tips of the refs at risk - that's why I am saying we should explicitly copy those out with rpm-ostree ex-container export first. Or, per above making a new repo and leaving the existing one untouched will also have this effect of not putting the existing data at risk (immediately).

@jlebon
Copy link
Member

jlebon commented Oct 25, 2021

Some more IRC discussions about this:

16:35:22 < jlebon> dustymabe: was the 24h just an example, or do you know roughly how long the first prune might take?
16:35:56 < jlebon> (roughly = magnitude, e.g. multiple hours or multiple days)
16:37:01 <@dustymabe> jlebon: i think ultimately it will run in a couple hours.. but the first run may take a long time
16:37:21 <@dustymabe> still probably only need/want to run it once a week
16:37:33 <@dustymabe> "long time" more than a weekend
16:39:33 < jlebon> i think as a first pass, we could have the pruner only delete commit objects and no data at all. then we can do the first megaprune in collaboration with releng to
                   make sure we have exclusive access
16:40:22 < jlebon> i vaguely recall we had discussions around locking over NFS in another context. are we doing that anywhere?
16:41:23 <@dustymabe> AFAIU we have 4 writers to these repos
16:41:38 <@dustymabe> 1. rpm-ostree compose (for silverblue/kinoite) into the compose repo
16:41:59 <@dustymabe> 2. new-updates-sync script, which does a pull-local to the prod repo from the compose repo
16:42:09 <@dustymabe> 3. coreos-ostree-importer
16:42:16 <@dustymabe> 4. soon: fedora-ostree-pruner
16:42:41 <@dustymabe> all of them pretty much access the share over NFS
16:43:46 < jlebon> walters: for the commit/pull-local side, even in the no transaction case, WDYT about a flag to at least respect exclusive locks?
16:43:50 <@dustymabe> when we added coreos-ostree-importer we also considered fedora-ostree-pruner so we set up our PV share in openshift so they could both access the content
16:46:08 <+walters> jlebon: yeah, makes sense

While I agree that races affecting commit content between importing and pruning objects will be rare, I think we should aim for stronger semantics for this. ISTM the core issue here is that we're not consistently using locks. I think repo swapping and saving/restoring content as SOP would still be subject to races without any locking.

The prune code already takes an exclusive lock. I think we need to support writing taking shared locks (and respecting exclusive locks) even if no transactions are in use. Then at least a compose can just fail if it tries to push at the same time the pruner is running, which is better than allowing it but possibly losing data. We could have it wait instead, but I'm not sure how viable that is depending on how long the pruner will take.

I like the idea of the repo swap for the initial prune (but e.g. done manually by releng running a script when we know it's safe to do).

jlebon added a commit to jlebon/rpm-ostree that referenced this pull request Oct 26, 2021
In the case we're using transactions, that's already done for us.
Otherwise, we should do it ourselves. Otherwise we run the risk of
racing with the pruner.

For more context, see:
ostreedev/ostree#2474
coreos/fedora-coreos-releng-automation#79
jlebon added a commit to jlebon/rpm-ostree that referenced this pull request Oct 26, 2021
In the case we're using transactions, that's already done for us.
Otherwise, we should do it ourselves. Otherwise we run the risk of
racing with e.g. a prune operation.

For more context, see:
ostreedev/ostree#2474
coreos/fedora-coreos-releng-automation#79
cgwalters pushed a commit to coreos/rpm-ostree that referenced this pull request Oct 26, 2021
In the case we're using transactions, that's already done for us.
Otherwise, we should do it ourselves. Otherwise we run the risk of
racing with e.g. a prune operation.

For more context, see:
ostreedev/ostree#2474
coreos/fedora-coreos-releng-automation#79
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
fedora-ostree-pruner/fedora-ostree-pruner Outdated Show resolved Hide resolved
@dustymabe
Copy link
Member Author

if everything looks good here now I'll proceed to squash the fixup commits and also run tests (which will probably take some time).

@dustymabe dustymabe force-pushed the dusty-ostree-pruner branch 2 times, most recently from 072ff1f to ecf80f9 Compare November 11, 2021 16:22
@dustymabe dustymabe force-pushed the dusty-ostree-pruner branch 2 times, most recently from a033f04 to a44b3b0 Compare April 21, 2022 20:39
@dustymabe
Copy link
Member Author

@jlebon - pushed up a new commit to take advantage of ostree prune --commit-only.

Copy link
Member

@jlebon jlebon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't do a full re-review, but the interdiffs look sane to me!

This will be run in Fedora's infrastructure and will prune out OSTree
repos that are used to serve content to Fedora users.
Just delete the ref first and then we'll do a generic prune for
unreachable objects after looping through all the refs in the repo.

This also means if more than one refs points to a commit we won't delete
it like we were doing with the `ostree prune --delete-commit=$commit`
approach.
When we prune commits the static deltas will get dropped too so this
function is not needed.
OStree just learned a new `--commit-only` option to `ostree prune` [1].
This allows us to just delete the commits in the history of a ref that
we desire to prune based on our policy, which is a fast operation. After
we've deleted all the commit objects we can then go back and do one
sweep over the repo to now clean up any unreachable objects.

Previously everytime we ran `ostree prune` and deleted a certain amount
of history from a ref the prune operation also computed reachability for
every object in the repo at that time, which is expensive. If you have
many refs this means the entire pruning process can take a long time.
With this new mode of operation deleting the commit objects should take
little time per run and then we can have one big expensive run at the
very end that does the expensive reachability determination for every
object in the repo.
This commit adds a decorator that will catch execptions and continue
execution so that we don't lose logs in the container we are running
in.
@dustymabe
Copy link
Member Author

@jlebon - made one more update here.

It's worth noting that the code here hardcodes us running in test mode, so theoretically no changes apply yet and we need a followup commit to get some pruning to actually take place.

This is no longer baked into COSA for some reason and config-bot
needs it.
@dustymabe dustymabe marked this pull request as ready for review December 13, 2022 21:25
runcmd(cmd)


def catch_exceptions_and_continue(func):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I'm not sure about is this. Right now we do:

prune_compose_repo(...)
prune_prod_repo(...)

Is it the right thing to do to continue with pruning the prod repo if an error occurred in the compose repo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they're mostly independent, but probably depends on the error. i.e. if it's some sort of I/O error the problem will probably apply to both operations.

@dustymabe dustymabe merged commit ae5e07f into coreos:main Dec 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants