Add the missing template file clean-amis.py
This commit is contained in:
parent
3e20f35339
commit
ba71e878c2
1 changed files with 404 additions and 0 deletions
404
roles/fedimg/templates/clean-amis.py
Normal file
404
roles/fedimg/templates/clean-amis.py
Normal file
|
@ -0,0 +1,404 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# clean-amis.py - A utility to remove the nightly AMIs every 5 days.
|
||||
#
|
||||
#
|
||||
# Authors:
|
||||
# Sayan Chowdhury <sayanchowdhury@fedoraproject.org>
|
||||
# Copyright (C) 2016 Red Hat Inc,
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
#
|
||||
# The script runs as a cron job within the Fedora Infrastructure to delete
|
||||
# the old AMIs. The permission of the selected AMIs are changed to private.
|
||||
# This is to make sure that if someone from the community raises an issue
|
||||
# we have the option to get the AMI back to public.
|
||||
# After 10 days, if no complaints are raised the AMIs are deleted permanently.
|
||||
#
|
||||
# The complete process can be divided in couple of parts:
|
||||
#
|
||||
# - Fetching the data from datagrepper.
|
||||
# Based on the `--days` param, the script starts fetching the fedmsg messages
|
||||
# from datagrepper for the specified timeframe i.e. for lasts `n` days, where
|
||||
# `n` is the value of `--days` param. The queried fedmsg
|
||||
# topic `fedimg.image.upload`.
|
||||
#
|
||||
# - Selection of the AMIs:
|
||||
# After the AMIs are parsed from datagrepper. The AMIs are filtered to remove
|
||||
# Beta, Two-week Atomic Host and GA released AMIs.
|
||||
# Composes with `compose_type` set to `nightly` are picked up for deletion.
|
||||
# Composes which contain date in the `compose label` are also picked up for
|
||||
# deletion.
|
||||
# GA composes also have the compose_type set to production. So to distinguish
|
||||
# then we filter them if the compose_label have date in them. The GA
|
||||
# composes dont have date whereas they have the version in format of X.Y
|
||||
#
|
||||
# - Updated permissions of AMIs
|
||||
# The permissions of the selected AMIs are changed to private.
|
||||
#
|
||||
# - Deletion of AMIs
|
||||
# After 10 days, the private AMIs are deleted.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
import boto3
|
||||
import functools
|
||||
import fedfind
|
||||
import fedfind.release
|
||||
import requests
|
||||
|
||||
from datetime import datetime, timedelta, date
|
||||
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger()
|
||||
|
||||
env = os.environ
|
||||
aws_access_key_id = os.environ.get("AWS_ACCESS_KEY", '{{ ec2_image_delete_access_key_id }}')
|
||||
aws_secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY", '{{ ec2_image_delete_access_key }}')
|
||||
|
||||
DATAGREPPER_URL = "https://apps.fedoraproject.org/datagrepper/"
|
||||
NIGHTLY = "nightly"
|
||||
|
||||
REGIONS = (
|
||||
"us-east-1",
|
||||
"us-east-2",
|
||||
"us-west-2",
|
||||
"us-west-1",
|
||||
"eu-west-1",
|
||||
"eu-central-1",
|
||||
"ap-south-1",
|
||||
"ap-southeast-1",
|
||||
"ap-northeast-1",
|
||||
"ap-northeast-2",
|
||||
"ap-southeast-2",
|
||||
"sa-east-1",
|
||||
"ca-central-1",
|
||||
"eu-west-2",
|
||||
)
|
||||
|
||||
|
||||
def _is_timestamp_newer(timestamp1, timestamp2):
|
||||
""" Return true if timestamp1 is newer than timestamp2
|
||||
"""
|
||||
timestamp1_f = datetime.strptime(timestamp1, "%d%m%Y")
|
||||
timestamp2_f = datetime.strptime(timestamp2, "%d%m%Y")
|
||||
|
||||
return timestamp1_f > timestamp2_f
|
||||
|
||||
|
||||
def _get_raw_url():
|
||||
""" Get the datagrepper raw URL to fetch the message from
|
||||
"""
|
||||
return DATAGREPPER_URL + "/raw"
|
||||
|
||||
|
||||
def get_page(page, delta, topic, start=None, end=None):
|
||||
|
||||
params = {
|
||||
"topic": topic,
|
||||
"delta": delta,
|
||||
"rows_per_page": 100,
|
||||
"page": page,
|
||||
}
|
||||
|
||||
if start:
|
||||
params.update({"start": start})
|
||||
|
||||
if end:
|
||||
params.update({"end": end})
|
||||
|
||||
resp = requests.get(_get_raw_url(), params=params)
|
||||
|
||||
return resp.json()
|
||||
|
||||
|
||||
def _get_two_week_released_atomic_compose_id(delta, start=None, end=None):
|
||||
""" Returns the release compose ids for last n days """
|
||||
|
||||
topic = "org.fedoraproject.prod.releng.atomic.twoweek.complete"
|
||||
data = get_page(1, delta, topic, start, end)
|
||||
|
||||
messages = data.get("raw_messages", [])
|
||||
|
||||
for page in range(1, data["pages"]):
|
||||
data = get_page(
|
||||
topic=topic, page=page + 1, delta=delta, start=start, end=end
|
||||
)
|
||||
messages.extend(data["raw_messages"])
|
||||
|
||||
messages = [msg["msg"] for msg in messages]
|
||||
|
||||
released_atomic_compose_ids = []
|
||||
for msg in messages:
|
||||
# This is to support the older-format fedmsg messages
|
||||
if "atomic_raw" in msg:
|
||||
released_atomic_compose_ids.append(msg["atomic_raw"]["compose_id"])
|
||||
# We are just trying here multiple archs to get the compose id
|
||||
elif "aarch64" in msg:
|
||||
released_atomic_compose_ids.append(
|
||||
msg["aarch64"]["atomic_raw"]["compose_id"]
|
||||
)
|
||||
elif "x86_64" in msg:
|
||||
released_atomic_compose_ids.append(
|
||||
msg["x86_64"]["atomic_raw"]["compose_id"]
|
||||
)
|
||||
elif "ppc64le" in msg:
|
||||
released_atomic_compose_ids.append(
|
||||
msg["ppc64le"]["atomic_raw"]["compose_id"]
|
||||
)
|
||||
|
||||
return set(released_atomic_compose_ids)
|
||||
|
||||
|
||||
def _get_nightly_amis_nd(delta, start=None, end=None):
|
||||
""" Returns the nightly AMIs for the last n days
|
||||
|
||||
:args delta: last delta seconds
|
||||
"""
|
||||
amis = []
|
||||
released_atomic_compose_ids = _get_two_week_released_atomic_compose_id(
|
||||
delta=delta, start=start, end=end
|
||||
)
|
||||
|
||||
topic = "org.fedoraproject.prod.fedimg.image.publish"
|
||||
data = get_page(1, delta, topic, start, end)
|
||||
messages = data.get("raw_messages", [])
|
||||
|
||||
for page in range(1, data["pages"]):
|
||||
data = get_page(
|
||||
topic=topic, page=page + 1, delta=delta, start=start, end=end
|
||||
)
|
||||
messages.extend(data["raw_messages"])
|
||||
|
||||
for message in messages:
|
||||
msg = message.get("msg")
|
||||
ami_id = msg["extra"]["id"]
|
||||
region = msg["destination"]
|
||||
|
||||
compose_id = msg["compose"]
|
||||
compose_info = fedfind.release.get_release(cid=compose_id)
|
||||
compose_type = compose_info.type
|
||||
compose_label = compose_info.label
|
||||
|
||||
# Sometimes the compose label is None
|
||||
# and they can be blindly put in for deletion
|
||||
if not compose_label:
|
||||
amis.append((compose_id, ami_id, region))
|
||||
|
||||
if compose_id in released_atomic_compose_ids:
|
||||
continue
|
||||
|
||||
# Include the nightly composes
|
||||
if compose_type == NIGHTLY:
|
||||
amis.append((compose_id, ami_id, region))
|
||||
else:
|
||||
# Include AMIs that have date in them
|
||||
# These are the production compose type but not GA
|
||||
result = re.search("-(\d{8}).", compose_label)
|
||||
if result is None:
|
||||
continue
|
||||
amis.append((compose_id, ami_id, region))
|
||||
|
||||
return amis
|
||||
|
||||
|
||||
def delete_amis_nd(deletetimestamp, dry_run=False):
|
||||
""" Delete the give list of nightly AMIs
|
||||
|
||||
:args deletetimestamp: the timestamp for the delete
|
||||
:args dry_run: dry run the flow
|
||||
"""
|
||||
log.info("Deleting AMIs")
|
||||
for region in REGIONS:
|
||||
log.info("%s Starting" % region)
|
||||
# Create a connection to an AWS region
|
||||
conn = boto3.client(
|
||||
"ec2",
|
||||
region,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
)
|
||||
log.info("%s: Connected" % region)
|
||||
|
||||
response = conn.describe_images(
|
||||
Filters=[{"Name": "tag-key", "Values": ["LaunchPermissionRevoked"]}]
|
||||
)
|
||||
amis = response.get("Images", [])
|
||||
|
||||
for ami in amis:
|
||||
try:
|
||||
ami_id = ami["ImageId"]
|
||||
is_launch_permitted = ami["Public"]
|
||||
_index = len(ami["BlockDeviceMappings"])
|
||||
snapshot_id = ami["BlockDeviceMappings"][0]["Ebs"]["SnapshotId"]
|
||||
tags = ami["Tags"]
|
||||
|
||||
revoketimestamp = ""
|
||||
for tag in tags:
|
||||
if "LaunchPermissionRevoked" in tag.values():
|
||||
revoketimestamp = tag["Value"]
|
||||
|
||||
if not revoketimestamp:
|
||||
log.warn(
|
||||
"%s ami has LaunchPermissionRevoked tag but no value"
|
||||
% ami_id
|
||||
)
|
||||
continue
|
||||
|
||||
if is_launch_permitted:
|
||||
log.warn(
|
||||
"%s ami has LaunchPermissionRevoked tag "
|
||||
"but launch permission is still enabled" % ami_id
|
||||
)
|
||||
continue
|
||||
|
||||
# The revoke timestamp allows us to tell how long ago an image
|
||||
# had permissions removed. If the permissions have been removed
|
||||
# for shorter than the waiting period then we can't delete it yet.
|
||||
if _is_timestamp_newer(revoketimestamp, deletetimestamp):
|
||||
continue
|
||||
|
||||
if not dry_run:
|
||||
conn.deregister_image(ImageId=ami_id)
|
||||
conn.delete_snapshot(SnapshotId=snapshot_id)
|
||||
else:
|
||||
print(ami_id)
|
||||
except Exception as ex:
|
||||
log.error("%s: %s failed\n%s" % (region, ami_id, ex))
|
||||
|
||||
|
||||
def change_amis_permission_nd(amis, dry_run=False):
|
||||
""" Change the launch permissions of the AMIs to private.
|
||||
|
||||
The permission of the AMIs are changed to private first and then delete
|
||||
after 5 days.
|
||||
|
||||
:args amis: list of AMIs
|
||||
:args dry_run: dry run the flow
|
||||
"""
|
||||
log.info("Changing permission for AMIs")
|
||||
todaystimestamp = date.today().strftime("%d%m%Y")
|
||||
|
||||
for region in REGIONS:
|
||||
log.info("%s: Starting" % region)
|
||||
# Create a connection to an AWS region
|
||||
conn = boto3.client(
|
||||
"ec2",
|
||||
region,
|
||||
aws_access_key_id=aws_access_key_id,
|
||||
aws_secret_access_key=aws_secret_access_key,
|
||||
)
|
||||
log.info("%s: Connected" % region)
|
||||
|
||||
# Filter all the nightly AMIs belonging to this region
|
||||
r_amis = [(c, a, r) for c, a, r in amis if r == region]
|
||||
|
||||
# Loop through the AMIs change the permissions
|
||||
for _, ami_id, region in r_amis:
|
||||
try:
|
||||
if not dry_run:
|
||||
conn.modify_image_attribute(
|
||||
ImageId=ami_id,
|
||||
LaunchPermission={"Remove": [{"Group": "all"}]},
|
||||
)
|
||||
conn.create_tags(
|
||||
Resources=[ami_id],
|
||||
Tags=[
|
||||
{
|
||||
"Key": "LaunchPermissionRevoked",
|
||||
"Value": todaystimestamp,
|
||||
}
|
||||
],
|
||||
)
|
||||
else:
|
||||
print(ami_id)
|
||||
except Exception as ex:
|
||||
log.error("%s: %s failed \n %s" % (region, ami_id, ex))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
argument_parser = argparse.ArgumentParser()
|
||||
argument_parser.add_argument(
|
||||
"--delete",
|
||||
help="Delete the AMIs whose launch permissions have been removed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
argument_parser.add_argument(
|
||||
"--days",
|
||||
help="Specify the number of days worth of AMI fedmsg information to fetch from datagrepper.",
|
||||
type=int,
|
||||
)
|
||||
argument_parser.add_argument(
|
||||
"--deletewaitperiod",
|
||||
help="Specify the number of days to wait after removing launch perms before deleting",
|
||||
type=int,
|
||||
default=10,
|
||||
)
|
||||
argument_parser.add_argument(
|
||||
"--permswaitperiod",
|
||||
help="Specify the number of days to wait before removing launch perms",
|
||||
type=int,
|
||||
default=10,
|
||||
)
|
||||
argument_parser.add_argument(
|
||||
"--change-perms",
|
||||
help="Change the launch permissions of the AMIs to private",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
argument_parser.add_argument(
|
||||
"--dry-run",
|
||||
help="Dry run the action to be performed",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
args = argument_parser.parse_args()
|
||||
|
||||
if not args.delete and not args.change_perms:
|
||||
raise Exception(
|
||||
"Either of the argument, delete or change permission is required"
|
||||
)
|
||||
|
||||
if args.delete and args.change_perms:
|
||||
raise Exception(
|
||||
"Both the argument delete and change permission is not allowed"
|
||||
)
|
||||
|
||||
# Ideally, we could search through all the AMIs that ever were created but this
|
||||
# this would create huge load on datagrepper.
|
||||
# default to 4 weeks/ 28 days
|
||||
days = 28
|
||||
if args.days:
|
||||
days = args.days
|
||||
|
||||
permswaitperiod = args.permswaitperiod
|
||||
deletewaitperiod = args.deletewaitperiod
|
||||
|
||||
# The AMIs deleted are the nightly AMIs that are uploaded via fedimg everyday.
|
||||
# The clean up of the AMIs happens through a cron job.
|
||||
# The steps followed while deleting the AMIs:
|
||||
# - The selected AMIs are made private, so that if people report issue we can make it
|
||||
# public again.
|
||||
# - If no issues are reported in 10 days, the AMIs are deleted permanently.
|
||||
|
||||
if args.change_perms:
|
||||
if days < permswaitperiod:
|
||||
raise Exception(
|
||||
"permswaitperiod param cannot be more than days param"
|
||||
)
|
||||
end = (datetime.now() - timedelta(days=permswaitperiod)).strftime("%s")
|
||||
amis = _get_nightly_amis_nd(
|
||||
delta=86400 * (days - permswaitperiod), end=int(end)
|
||||
)
|
||||
change_amis_permission_nd(amis, dry_run=args.dry_run)
|
||||
|
||||
if args.delete:
|
||||
deletetimestamp = (
|
||||
datetime.now() - timedelta(days=deletewaitperiod)
|
||||
).strftime("%d%m%Y")
|
||||
delete_amis_nd(deletetimestamp, dry_run=args.dry_run)
|
Loading…
Add table
Add a link
Reference in a new issue