diff --git a/awx/main/management/commands/cleanup_images.py b/awx/main/management/commands/cleanup_images.py deleted file mode 100644 index 107209d0cb..0000000000 --- a/awx/main/management/commands/cleanup_images.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import subprocess -import logging -import json - -# Django -from django.core.management.base import BaseCommand, CommandError -from django.conf import settings - -# AWX -from awx.main.models import ExecutionEnvironment - - -class Command(BaseCommand): - """ - Management command to cleanup unused execution environment images. - """ - - help = 'Remove unused execution environment images' - - def init_logging(self): - log_levels = dict(enumerate([logging.ERROR, logging.INFO, logging.DEBUG, 0])) - self.logger = logging.getLogger('awx.main.commands.cleanup_images') - self.logger.setLevel(log_levels.get(self.verbosity, 0)) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter('%(message)s')) - self.logger.addHandler(handler) - self.logger.propagate = False - - def add_arguments(self, parser): - parser.add_argument('--dry-run', dest='dry_run', action='store_true', default=False, help='Dry run mode (show items that would ' 'be removed)') - - def delete_images(self, images_json): - if self.dry_run: - delete_prefix = "Would delete" - else: - delete_prefix = "Deleting" - for e in images_json: - if 'Names' in e: - image_names = e['Names'] - else: - image_names = [e["Id"]] - image_size = e['Size'] / 1e6 - for i in image_names: - if i not in self.images_in_use and i not in self.deleted: - self.deleted.append(i) - self.logger.info(f"{delete_prefix} {i}: {image_size:.0f} MB") - if not self.dry_run: - subprocess.run(['podman', 'rmi', i, '-f'], stdout=subprocess.DEVNULL) - - def cleanup_images(self): - self.images_in_use = [ee.image for ee in ExecutionEnvironment.objects.all()] - if self.images_in_use: - self.logger.info("Execution environment images in use:") - for i in self.images_in_use: - self.logger.info(f"\t{i}") - self.deleted = [] - # find and remove unused images - images_system = subprocess.run("podman images -a --format json".split(" "), capture_output=True) - if len(images_system.stdout) > 0: - images_system = json.loads(images_system.stdout) - - self.delete_images(images_system) - # find and remove dangling images - images_system = subprocess.run('podman images -a --filter "dangling=true" --format json'.split(" "), capture_output=True) - if len(images_system.stdout) > 0: - images_system = json.loads(images_system.stdout) - self.delete_images(images_system) - if not self.deleted: - self.logger.info("Did not find unused images to remove") - - def handle(self, *args, **options): - self.verbosity = int(options.get('verbosity', 1)) - self.init_logging() - self.dry_run = bool(options.get('dry_run', False)) - if self.dry_run: - self.logger.info("Dry run enabled, images will not be deleted") - if settings.IS_K8S: - raise CommandError("Cannot run cleanup tool on k8s installations") - self.cleanup_images() diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 7c0dd854d0..2b4961fcc2 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -27,6 +27,7 @@ import socket import threading import concurrent.futures from base64 import b64encode +import subprocess # Django from django.conf import settings @@ -59,6 +60,7 @@ from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_ from awx.main.access import access_registry from awx.main.redact import UriCleaner from awx.main.models import ( + ExecutionEnvironment, Schedule, TowerScheduleState, Instance, @@ -396,6 +398,23 @@ def purge_old_stdout_files(): logger.debug("Removing {}".format(os.path.join(settings.JOBOUTPUT_ROOT, f))) +@task(queue=get_local_queuename) +def cleanup_execution_environment_images(): + images_in_use = [ee.image for ee in ExecutionEnvironment.objects.all()] + images_system = subprocess.run("podman images -a --format json".split(" "), capture_output=True) + if len(images_system.stdout) > 0: + images_system = json.loads(images_system.stdout) + for e in images_system: + if 'Names' in e: + image_name = e['Names'][0] + else: + image_name = e["Id"] + image_size = e['Size'] / 1e6 + if image_name not in images_in_use: + logger.debug(f"Cleanup execution environment images: deleting {image_name}, {image_size:.0f} MB") + subprocess.run(['podman', 'rmi', image_name, '-f'], stdout=subprocess.DEVNULL) + + @task(queue=get_local_queuename) def cluster_node_heartbeat(): logger.debug("Cluster node heartbeat task.") diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 39426fbc43..e1b497f1b0 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -439,6 +439,7 @@ CELERYBEAT_SCHEDULE = { 'task_manager': {'task': 'awx.main.scheduler.tasks.run_task_manager', 'schedule': timedelta(seconds=20), 'options': {'expires': 20}}, 'k8s_reaper': {'task': 'awx.main.tasks.awx_k8s_reaper', 'schedule': timedelta(seconds=60), 'options': {'expires': 50}}, 'send_subsystem_metrics': {'task': 'awx.main.analytics.analytics_tasks.send_subsystem_metrics', 'schedule': timedelta(seconds=20)}, + 'cleanup_images': {'task': 'awx.main.tasks.cleanup_execution_environment_images', 'schedule': timedelta(hours=8)}, # 'isolated_heartbeat': set up at the end of production.py and development.py }