import subprocess
import time
import sys
import re
import argparse
import logging
import shlex
LOGFILE = "/var/log/clean_docker_images.log"
IMAGE_COMMAND_TEMPLATE = "docker image ls -a -q"
IMAGE_GREP_COMMAND_TEMPLATE = "docker image ls -a | grep %s"
IMAGE_CREATED_INFO_COMMAND_TEMPLATE = "docker inspect %s --format='{{ .Created }}'"
IMAGE_REMOVE_COMMAND_TEMPLATE = "docker rmi %s"
class CleanDockerImagesError(Exception):
"""Custom exception for clean docker images errors"""
pass
class Logger:
def __init__(self, logger_name):
self.logger = logging.getLogger()
self.logger.setLevel(logging.INFO)
logfile = logger_name
fh = logging.FileHandler(logfile, mode='a')
fh.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
self.logger.addHandler(fh)
self.logger.addHandler(ch)
def get_log(self):
return self.logger
logger = Logger(LOGFILE).get_log()
class Cmd(object):
def __init__(self):
self._return_code = None
self._message = None
def run(self, cmd_):
if not isinstance(cmd_, str):
msg = 'cmd is not available! expect Union[bytes, str] not %s' % type(cmd_)
logger.error(msg)
raise CleanDockerImagesError(msg)
cmd_list = shlex.split(cmd_)
result = subprocess.Popen(cmd_list, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result.wait()
self._return_code = result.returncode
self._message = result.stdout.read()
logger.info('execute cmd: %s success' % cmd_)
@property
def return_code(self):
return self._return_code
@property
def message(self):
if isinstance(self._message, bytes):
return self._message.decode(self, encoding='utf-8')
return self._message
cmd = Cmd()
def command_rmi(remove_ids):
for remove_id in remove_ids:
cmd.run("docker image ls -a")
if cmd.return_code == 0:
output_lines = cmd.message.split('\n')
matching_lines = [line for line in output_lines if remove_id in line]
if matching_lines:
cmd.run(IMAGE_REMOVE_COMMAND_TEMPLATE % remove_id)
if cmd.return_code == 0:
logger.info("%s" % cmd.message)
def transfer_timedelta(timedelta):
if not re.match('^\d', timedelta):
msg = "error format ! timedelta: %s " % timedelta
logger.error(msg)
raise CleanDockerImagesError(msg)
if timedelta.endswith('S'):
return int(timedelta.split('S')[0])
elif timedelta.endswith('M'):
return int(timedelta.split('M')[0]) * 60
elif timedelta.endswith('H'):
return int(timedelta.split('H')[0]) * 60 * 60
elif timedelta.endswith('d'):
return int(timedelta.split('d')[0]) * 24 * 60 * 60
elif timedelta.endswith('m'):
return int(timedelta.split('m')[0]) * 30 * 24 * 60 * 60
elif timedelta.endswith('y'):
return int(timedelta.split('y')[0]) * 12 * 30 * 24 * 60 * 60
else:
msg = "error format ! timedelta: %s " % timedelta
logger.error(msg)
raise CleanDockerImagesError(msg)
class DockerImage(object):
def __init__(self, ignore_ids=None):
if ignore_ids is None:
ignore_ids = []
if not isinstance(ignore_ids, list):
msg = "[ERROR] expect list!"
logger.error(msg)
raise CleanDockerImagesError(msg)
self.ignore_ids = ignore_ids
self.image_ids = []
self.remove_ids = []
self._get_image_ids()
def _get_image_ids(self):
cmd.run(IMAGE_COMMAND_TEMPLATE)
if cmd.return_code != 0:
msg = ('[ERROR] can not get docker image info! cmd: %s . '
'please check docker status on your host' % IMAGE_COMMAND_TEMPLATE)
logger.error(msg)
raise CleanDockerImagesError(msg)
self.image_ids = cmd.message.split('\n')[:-1]
return self.image_ids
def _check(self):
pass
class DataParser(object):
def parse(self, docker_image, timedelta):
remove_ids = []
image_id_string = ' '.join(docker_image.image_ids)
timedelta = transfer_timedelta(timedelta)
created_timestamp_list = self._get_created_timestamp_list(image_id_string)
if len(docker_image.image_ids) != len(created_timestamp_list):
msg = ('Data inconsistency: image_ids count (%d) does not match '
'created_timestamp_list count (%d)' % (
len(docker_image.image_ids), len(created_timestamp_list)))
logger.error(msg)
raise CleanDockerImagesError(msg)
for image_id, created_timestamp in zip(docker_image.image_ids,
created_timestamp_list):
if image_id in docker_image.ignore_ids:
continue
if created_timestamp <= time.time() - timedelta:
remove_ids.append(image_id)
docker_image.remove_ids = remove_ids
def _get_created_timestamp_list(self, image_id_string):
cmd.run(IMAGE_CREATED_INFO_COMMAND_TEMPLATE % image_id_string)
if cmd.return_code != 0:
msg = ('[ERROR] can not get docker image id info! cmd: %s . '
'please check docker status on your host' % IMAGE_CREATED_INFO_COMMAND_TEMPLATE)
logger.error(msg)
raise CleanDockerImagesError(msg)
return [self._transfer_timestring_to_timestamp(x) for x in cmd.message.split('\n')[:-1]]
def _transfer_timestring_to_timestamp(self, timestring):
return time.mktime(time.strptime(timestring.split(r'.')[0], "%Y-%m-%dT%H:%M:%S"))
data_parser = DataParser()
if __name__ == '__main__':
try:
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("-t", "--timedelta", default='3d', help="timedelta example 1S, 1M, 1H, 1d, 1m, 1y")
arg_parser.add_argument("-g", "--ignore", nargs='*', help="ids in ignore_id_list will not be deleted")
args = arg_parser.parse_args()
docker_image = DockerImage(ignore_ids=args.ignore)
data_parser.parse(docker_image, timedelta=args.timedelta)
if len(docker_image.remove_ids) == 0:
logger.info("finish clean")
command_rmi(docker_image.remove_ids)
except CleanDockerImagesError as e:
logger.error(str(e))
sys.exit(1)
except Exception as e:
logger.error("Unexpected error: %s" % str(e))
sys.exit(1)