aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-08-22 22:32:02 +0000
committerMike Crute <mike@crute.us>2020-08-28 00:47:43 +0000
commit7dea4f392e2fe8271d5764f19683cb5384688875 (patch)
treea49d11cd97a140d0e2acfd8dfce998964cda0b60
parent1bcb4a0b0d1a676943e08cd3de08ebf029350ea2 (diff)
downloadalpine-ec2-ami-release-tool.tar.bz2
alpine-ec2-ami-release-tool.tar.xz
alpine-ec2-ami-release-tool.zip
Add release commandrelease-tool
-rwxr-xr-xscripts/builder.py201
1 files changed, 201 insertions, 0 deletions
diff --git a/scripts/builder.py b/scripts/builder.py
index 00241ae..0501eaa 100755
--- a/scripts/builder.py
+++ b/scripts/builder.py
@@ -54,6 +54,12 @@ import boto3
54import pyhocon 54import pyhocon
55 55
56 56
57# This is an ugly hack. We occasionally need the region name but it's not
58# attached to anything publicly exposed on the client objects. Hide this here.
59def region_from_client(client):
60 return client._client_config.region_name
61
62
57class EC2Architecture(Enum): 63class EC2Architecture(Enum):
58 64
59 I386 = "i386" 65 I386 = "i386"
@@ -893,6 +899,201 @@ class UpdateReleases:
893 yaml.dump(releases, data, sort_keys=False) 899 yaml.dump(releases, data, sort_keys=False)
894 900
895 901
902class ReleaseAMIs:
903 """Copy AMIs to other regions and optionally make them public.
904
905 Copies an AMI from a source region to destination regions. If the AMI
906 exists in some regions but not others it will copy only to the new regions.
907 This copy will add tags to the destination AMIs to link them to the source
908 AMI.
909
910 By default does not make the AMIs public. Running the command a second time
911 with the --public flag will make the already copied AMIs public. If some
912 AMIs are public and others are not, will make them all public.
913
914 This command will fill in missing regions and synchronized public settings
915 if it's re-run with the same AMI ID as new regions are added.
916 """
917
918 command_name = "release"
919
920 @staticmethod
921 def add_args(parser):
922 parser.add_argument("--use-broker", action="store_true",
923 help="use identity broker to obtain per-region credentials")
924 parser.add_argument("--public", action="store_true",
925 help="make all copied images public, even previously copied ones")
926 parser.add_argument("--source-region", default="us-west-2",
927 help="source region hosting ami to copy")
928 parser.add_argument("--region", "-r", action="append",
929 help="destination regions for copy, may be specified multiple "
930 "times")
931 parser.add_argument("--allow-accounts", action="append",
932 help="add permissions for other accounts to non-public images, "
933 "may be specified multiple times")
934 parser.add_argument("--out-file", "-o",
935 help="output file for JSON AMI map, otherwise stdout")
936 parser.add_argument("ami", help="ami id to copy")
937
938 @staticmethod
939 def check_args(args):
940 if not args.use_broker and not args.region:
941 return ["Use broker or region must be specified"]
942
943 if args.use_broker and args.region:
944 return ["Broker and region flags are mutually exclusive."]
945
946 if args.out_file and os.path.exists(args.out_file):
947 return ["Output file already exists"]
948
949 def get_source_region_client(self, use_broker, source_region):
950 if use_broker:
951 return IdentityBrokerClient().boto3_session_for_region(
952 source_region).client("ec2")
953 else:
954 return boto3.session.Session(region_name=source_region).client(
955 "ec2")
956
957 def iter_regions(self, use_broker, regions):
958 if use_broker:
959 for region in IdentityBrokerClient().iter_regions():
960 yield region.client("ec2")
961 return
962
963 for region in regions:
964 yield boto3.session.Session(region_name=region).client("ec2")
965
966 def get_image(self, client, image_id):
967 images = client.describe_images(ImageIds=[image_id], Owners=["self"])
968 perms = client.describe_image_attribute(
969 Attribute="launchPermission", ImageId=image_id)
970
971 ami = AMI.from_aws_model(
972 images["Images"][0], region_from_client(client))
973 ami.aws_permissions = perms["LaunchPermissions"]
974
975 return ami
976
977 def get_image_with_tags(self, client, **tags):
978 images = self.get_images_with_tags(client, **tags)
979 if len(images) > 1:
980 raise Exception(f"Too many images for query {tags!r}")
981 elif len(images) == 0:
982 return None
983 else:
984 return images[0]
985
986 def get_images_with_tags(self, client, **tags):
987 images = []
988
989 res = client.describe_images(Owners=["self"], Filters=[
990 {"Name": f"tag:{k}", "Values": [v]} for k, v in tags.items()])
991
992 for image in res["Images"]:
993 ami = AMI.from_aws_model(image, region_from_client(client))
994 perms = client.describe_image_attribute(
995 Attribute="launchPermission", ImageId=ami.image_id)
996 ami.aws_permissions = perms["LaunchPermissions"]
997 images.append(ami)
998
999 return images
1000
1001 def copy_image(self, from_client, to_client, image_id):
1002 source = self.get_image(from_client, image_id)
1003
1004 res = to_client.copy_image(
1005 Name=source.name, Description=source.description,
1006 SourceImageId=source.image_id, SourceRegion=source.region)
1007
1008 tags = [{
1009 "Key": "source_ami",
1010 "Value": source.image_id,
1011 }]
1012 tags.extend(source.aws_tags)
1013
1014 to_client.create_tags(Resources=[res["ImageId"]], Tags=tags)
1015
1016 return self.get_image(to_client, res["ImageId"])
1017
1018 def has_incorrect_perms(self, ami, accounts, public):
1019 if accounts and set(ami.allowed_users) != set(accounts):
1020 return True
1021
1022 if public and not ami.public:
1023 return True
1024
1025 def update_image_permissions(self, client, ami):
1026 client.modify_image_attribute(
1027 Attribute="launchPermission", ImageId=ami.image_id,
1028 LaunchPermission={"Add": ami.aws_permissions})
1029
1030 def run(self, args, root, log):
1031 released = {}
1032 pending_copy = []
1033 pending_perms = []
1034
1035 source_client = self.get_source_region_client(
1036 args.use_broker, args.source_region)
1037
1038 # Copy image to regions where it is missing, catalog images that need
1039 # permission fixes
1040 for client in self.iter_regions(args.use_broker, args.region):
1041 region_name = region_from_client(client) # For logging
1042
1043 # Don't copy to source region
1044 if region_name == region_from_client(source_client):
1045 continue
1046
1047 log.info(f"Considering region {region_name}")
1048 image = self.get_image_with_tags(client, source_ami=args.ami)
1049 if not image:
1050 log.info(f"Copying ami {args.ami} from {args.source_region} "
1051 f"to {region_name}")
1052 ami = self.copy_image(source_client, client, args.ami)
1053 pending_copy.append((client, ami.image_id))
1054 elif self.has_incorrect_perms(
1055 image, args.allow_accounts, args.public):
1056 log.info(f"Incorrect permissions for ami {args.ami} in region "
1057 f"{region_name}")
1058 pending_perms.append((client, image.image_id))
1059
1060 # Wait for images to copy
1061 while pending_copy:
1062 client, id = pending_copy.pop(0) # emulate a FIFO queue
1063 region_name = region_from_client(client) # For logging
1064 image = self.get_image(client, id)
1065 if image.state != AMIState.AVAILABLE:
1066 log.info(f"Waiting for image copy for {id} to complete "
1067 f"in {region_name}")
1068 pending_copy.append((client, id))
1069 else:
1070 pending_perms.append((client, id))
1071 released[region_name] = id
1072
1073 time.sleep(30)
1074
1075 # Update all permissions
1076 for client, id in pending_perms:
1077 region_name = region_from_client(client) # For logging
1078
1079 log.info(f"Updating permissions on ami {id} in "
1080 f"{region_name}")
1081 image = self.get_image(client, id)
1082
1083 if args.public:
1084 image.allowed_groups = ["all"]
1085 elif args.allow_accounts:
1086 image.allowed_users = args.allow_accounts
1087
1088 self.update_image_permissions(client, image)
1089
1090 if args.out_file:
1091 with open(args.out_file, "w") as fp:
1092 json.dump(released, fp, indent=4)
1093 else:
1094 json.dump(released, sys.stdout, indent=4)
1095
1096
896class ConvertPackerJSON: 1097class ConvertPackerJSON:
897 """Convert packer.conf to packer.json 1098 """Convert packer.conf to packer.json
898 """ 1099 """