aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2020-08-22 21:04:13 +0000
committerMike Crute <mike@crute.us>2020-08-28 00:42:31 +0000
commit1bcb4a0b0d1a676943e08cd3de08ebf029350ea2 (patch)
tree47203a87245133df992c97baf63ee52d86fdd3b1
parent27491bcb200e78ce114e2452524ffd834bc0f022 (diff)
downloadalpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.tar.bz2
alpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.tar.xz
alpine-ec2-ami-1bcb4a0b0d1a676943e08cd3de08ebf029350ea2.zip
Add EC2 data types
-rwxr-xr-xscripts/builder.py200
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
43import subprocess 43import subprocess
44import urllib.error 44import urllib.error
45 45
46from enum import Enum
46from collections import defaultdict 47from collections import defaultdict
47from datetime import datetime, timedelta 48from datetime import datetime, timedelta
48from distutils.version import StrictVersion 49from distutils.version import StrictVersion
@@ -53,6 +54,205 @@ import boto3
53import pyhocon 54import pyhocon
54 55
55 56
57class EC2Architecture(Enum):
58
59 I386 = "i386"
60 X86_64 = "x86_64"
61 ARM64 = "arm64"
62
63
64class 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
75class EC2SnapshotState(Enum):
76
77 PENDING = "pending"
78 COMPLETED = "completed"
79 ERROR = "error"
80
81
82class 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
170class 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
236class 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
56class ColoredFormatter(logging.Formatter): 256class ColoredFormatter(logging.Formatter):
57 """Log formatter that colors output based on level 257 """Log formatter that colors output based on level
58 """ 258 """