diff options
author | Mike Crute <mike@crute.us> | 2020-08-22 22:32:02 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2020-08-28 00:47:43 +0000 |
commit | 7dea4f392e2fe8271d5764f19683cb5384688875 (patch) | |
tree | a49d11cd97a140d0e2acfd8dfce998964cda0b60 | |
parent | 1bcb4a0b0d1a676943e08cd3de08ebf029350ea2 (diff) | |
download | alpine-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-x | scripts/builder.py | 201 |
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 | |||
54 | import pyhocon | 54 | import 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. | ||
59 | def region_from_client(client): | ||
60 | return client._client_config.region_name | ||
61 | |||
62 | |||
57 | class EC2Architecture(Enum): | 63 | class 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 | ||
902 | class 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 | |||
896 | class ConvertPackerJSON: | 1097 | class ConvertPackerJSON: |
897 | """Convert packer.conf to packer.json | 1098 | """Convert packer.conf to packer.json |
898 | """ | 1099 | """ |