diff options
author | Mike Crute <mike@crute.us> | 2020-08-22 21:04:13 +0000 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2020-08-28 00:42:31 +0000 |
commit | 1bcb4a0b0d1a676943e08cd3de08ebf029350ea2 (patch) | |
tree | 47203a87245133df992c97baf63ee52d86fdd3b1 | |
parent | 27491bcb200e78ce114e2452524ffd834bc0f022 (diff) | |
download | alpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.tar.bz2 alpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.tar.xz alpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.zip |
Add EC2 data types
-rwxr-xr-x | scripts/builder.py | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/scripts/builder.py b/scripts/builder.py index 2107474..00241ae 100755 --- a/scripts/builder.py +++ b/scripts/builder.py | |||
@@ -43,6 +43,7 @@ import textwrap | |||
43 | import subprocess | 43 | import subprocess |
44 | import urllib.error | 44 | import urllib.error |
45 | 45 | ||
46 | from enum import Enum | ||
46 | from collections import defaultdict | 47 | from collections import defaultdict |
47 | from datetime import datetime, timedelta | 48 | from datetime import datetime, timedelta |
48 | from distutils.version import StrictVersion | 49 | from distutils.version import StrictVersion |
@@ -53,6 +54,205 @@ import boto3 | |||
53 | import pyhocon | 54 | import pyhocon |
54 | 55 | ||
55 | 56 | ||
57 | class EC2Architecture(Enum): | ||
58 | |||
59 | I386 = "i386" | ||
60 | X86_64 = "x86_64" | ||
61 | ARM64 = "arm64" | ||
62 | |||
63 | |||
64 | class AMIState(Enum): | ||
65 | |||
66 | PENDING = "pending" | ||
67 | AVAILABLE = "available" | ||
68 | INVALID = "invalid" | ||
69 | DEREGISTERED = "deregistered" | ||
70 | TRANSIENT = "transient" | ||
71 | FAILED = "failed" | ||
72 | ERROR = "error" | ||
73 | |||
74 | |||
75 | class EC2SnapshotState(Enum): | ||
76 | |||
77 | PENDING = "pending" | ||
78 | COMPLETED = "completed" | ||
79 | ERROR = "error" | ||
80 | |||
81 | |||
82 | class TaggedAWSObject: | ||
83 | """Base class for AWS API models that support tagging | ||
84 | """ | ||
85 | |||
86 | EDGE = StrictVersion("0.0") | ||
87 | |||
88 | missing_known_tags = None | ||
89 | |||
90 | _identity = lambda x: x | ||
91 | _known_tags = { | ||
92 | "Name": _identity, | ||
93 | "profile": _identity, | ||
94 | "revision": _identity, | ||
95 | "profile_build": _identity, | ||
96 | "source_ami": _identity, | ||
97 | "arch": lambda x: EC2Architecture(x), | ||
98 | "end_of_life": lambda x: datetime.fromisoformat(x), | ||
99 | "release": lambda v: EDGE if v == "edge" else StrictVersion(v), | ||
100 | "version": lambda v: EDGE if v == "edge" else StrictVersion(v), | ||
101 | } | ||
102 | |||
103 | def __repr__(self): | ||
104 | attrs = [] | ||
105 | for k, v in self.__dict__.items(): | ||
106 | if isinstance(v, TaggedAWSObject): | ||
107 | attrs.append(f"{k}=" + object.__repr__(v)) | ||
108 | elif not k.startswith("_"): | ||
109 | attrs.append(f"{k}={v!r}") | ||
110 | attrs = ", ".join(attrs) | ||
111 | |||
112 | return f"{self.__class__.__name__}({attrs})" | ||
113 | |||
114 | __str__ = __repr__ | ||
115 | |||
116 | @property | ||
117 | def aws_tags(self): | ||
118 | """Convert python tags to AWS API tags | ||
119 | |||
120 | See AMI.aws_permissions for rationale. | ||
121 | """ | ||
122 | for key, values in self.tags.items(): | ||
123 | for value in values: | ||
124 | yield { "Key": key, "Value": value } | ||
125 | |||
126 | @aws_tags.setter | ||
127 | def aws_tags(self, values): | ||
128 | """Convert AWS API tags to python tags | ||
129 | |||
130 | See AMI.aws_permissions for rationale. | ||
131 | """ | ||
132 | if not getattr(self, "tags", None): | ||
133 | self.tags = {} | ||
134 | |||
135 | tags = defaultdict(list) | ||
136 | |||
137 | for tag in values: | ||
138 | tags[tag["Key"]].append(tag["Value"]) | ||
139 | |||
140 | self.tags.update(tags) | ||
141 | self._transform_known_tags() | ||
142 | |||
143 | # XXX(mcrute): The second paragraph might be considered a bug and worth | ||
144 | # fixing at some point. For now those are all read-only attributes though. | ||
145 | def _transform_known_tags(self): | ||
146 | """Convert well known tags into python attributes | ||
147 | |||
148 | Some tags have special meanings for the model objects that they're | ||
149 | attached to. This copies those tags, transforms them, then sets them in | ||
150 | the model attributes. | ||
151 | |||
152 | It doesn't touch the tag itself so if that | ||
153 | attribute needs updated and re-saved the tag must be updated in | ||
154 | addition to the model. | ||
155 | """ | ||
156 | self.missing_known_tags = [] | ||
157 | |||
158 | for k, tf in self._known_tags.items(): | ||
159 | v = self.tags.get(k, []) | ||
160 | if not v: | ||
161 | self.missing_known_tags.append(k) | ||
162 | continue | ||
163 | |||
164 | if len(v) > 1: | ||
165 | raise Exception(f"multiple instances of tag {k}") | ||
166 | |||
167 | setattr(self, k, v[0]) | ||
168 | |||
169 | |||
170 | class AMI(TaggedAWSObject): | ||
171 | |||
172 | @property | ||
173 | def aws_permissions(self): | ||
174 | """Convert python permissions to AWS API permissions | ||
175 | |||
176 | The permissions model for the API makes more sense for a web service | ||
177 | but is overly verbose for working with in Python. This and the setter | ||
178 | allow transforming to/from the API syntax. The python code should | ||
179 | consume the allowed_groups and allowed_users lists directly. | ||
180 | """ | ||
181 | perms = [] | ||
182 | for g in self.allowed_groups: | ||
183 | perms.append({"Group": g}) | ||
184 | |||
185 | for i in self.allowed_users: | ||
186 | perms.append({"UserId": i}) | ||
187 | |||
188 | return perms | ||
189 | |||
190 | @aws_permissions.setter | ||
191 | def aws_permissions(self, perms): | ||
192 | """Convert AWS API permissions to python permissions | ||
193 | """ | ||
194 | for perm in perms: | ||
195 | group = perm.get("Group") | ||
196 | if group: | ||
197 | self.allowed_groups.append(group) | ||
198 | |||
199 | user = perm.get("UserId") | ||
200 | if user: | ||
201 | self.allowed_users.append(user) | ||
202 | |||
203 | @classmethod | ||
204 | def from_aws_model(cls, ob, region): | ||
205 | self = cls() | ||
206 | |||
207 | self.linked_snapshot = None | ||
208 | self.allowed_groups = [] | ||
209 | self.allowed_users = [] | ||
210 | self.region = region | ||
211 | self.architecture = EC2Architecture(ob["Architecture"]) | ||
212 | self.creation_date = ob["CreationDate"] | ||
213 | self.description = ob.get("Description", None) | ||
214 | self.image_id = ob["ImageId"] | ||
215 | self.name = ob.get("Name") | ||
216 | self.owner_id = int(ob["OwnerId"]) | ||
217 | self.public = ob["Public"] | ||
218 | self.state = AMIState(ob["State"]) | ||
219 | self.virtualization_type = ob["VirtualizationType"] | ||
220 | self.state_reason = ob.get("StateReason", {}).get("Message", None) | ||
221 | self.aws_tags = ob.get("Tags", []) | ||
222 | |||
223 | # XXX(mcrute): Assumes we only ever have one device mapping, which is | ||
224 | # valid for Alpine AMIs but not a good general assumption. | ||
225 | # | ||
226 | # This should always resolve for AVAILABLE images but any part of the | ||
227 | # data structure may not yet exist for images that are still in the | ||
228 | # process of copying. | ||
229 | if ob.get("BlockDeviceMappings"): | ||
230 | self.snapshot_id = \ | ||
231 | ob["BlockDeviceMappings"][0]["Ebs"].get("SnapshotId") | ||
232 | |||
233 | return self | ||
234 | |||
235 | |||
236 | class EC2Snapshot(TaggedAWSObject): | ||
237 | |||
238 | @classmethod | ||
239 | def from_aws_model(cls, ob, region): | ||
240 | self = cls() | ||
241 | |||
242 | self.linked_ami = None | ||
243 | self.region = region | ||
244 | self.snapshot_id = ob["SnapshotId"] | ||
245 | self.description = ob.get("Description", None) | ||
246 | self.owner_id = int(ob["OwnerId"]) | ||
247 | self.progress = int(ob["Progress"].rstrip("%")) / 100 | ||
248 | self.start_time = ob["StartTime"] | ||
249 | self.state = EC2SnapshotState(ob["State"]) | ||
250 | self.volume_size = ob["VolumeSize"] | ||
251 | self.aws_tags = ob.get("Tags", []) | ||
252 | |||
253 | return self | ||
254 | |||
255 | |||
56 | class ColoredFormatter(logging.Formatter): | 256 | class ColoredFormatter(logging.Formatter): |
57 | """Log formatter that colors output based on level | 257 | """Log formatter that colors output based on level |
58 | """ | 258 | """ |