1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """A LatentSlave that uses EC2 to instantiate the slaves on demand.
18
19 Tested with Python boto 1.5c
20 """
21
22 import os
23 import re
24 import time
25
26 import boto
27 import boto.ec2
28 import boto.exception
29 from twisted.internet import defer, threads
30 from twisted.python import log
31
32 from buildbot.buildslave import AbstractLatentBuildSlave
33 from buildbot import interfaces
34
35 PENDING = 'pending'
36 RUNNING = 'running'
37 SHUTTINGDOWN = 'shutting-down'
38 TERMINATED = 'terminated'
39
41
42 instance = image = None
43 _poll_resolution = 5
44
45 - def __init__(self, name, password, instance_type, ami=None,
46 valid_ami_owners=None, valid_ami_location_regex=None,
47 elastic_ip=None, identifier=None, secret_identifier=None,
48 aws_id_file_path=None, user_data=None, region=None,
49 keypair_name='latent_buildbot_slave',
50 security_name='latent_buildbot_slave',
51 max_builds=None, notify_on_missing=[], missing_timeout=60*20,
52 build_wait_timeout=60*10, properties={}, locks=None):
53
54 AbstractLatentBuildSlave.__init__(
55 self, name, password, max_builds, notify_on_missing,
56 missing_timeout, build_wait_timeout, properties, locks)
57 if not ((ami is not None) ^
58 (valid_ami_owners is not None or
59 valid_ami_location_regex is not None)):
60 raise ValueError(
61 'You must provide either a specific ami, or one or both of '
62 'valid_ami_location_regex and valid_ami_owners')
63 self.ami = ami
64 if valid_ami_owners is not None:
65 if isinstance(valid_ami_owners, (int, long)):
66 valid_ami_owners = (valid_ami_owners,)
67 else:
68 for element in valid_ami_owners:
69 if not isinstance(element, (int, long)):
70 raise ValueError(
71 'valid_ami_owners should be int or iterable '
72 'of ints', element)
73 if valid_ami_location_regex is not None:
74 if not isinstance(valid_ami_location_regex, basestring):
75 raise ValueError(
76 'valid_ami_location_regex should be a string')
77 else:
78
79 re.compile(valid_ami_location_regex)
80 self.valid_ami_owners = valid_ami_owners
81 self.valid_ami_location_regex = valid_ami_location_regex
82 self.instance_type = instance_type
83 self.keypair_name = keypair_name
84 self.security_name = security_name
85 self.user_data = user_data
86 if identifier is None:
87 assert secret_identifier is None, (
88 'supply both or neither of identifier, secret_identifier')
89 if aws_id_file_path is None:
90 home = os.environ['HOME']
91 aws_id_file_path = os.path.join(home, '.ec2', 'aws_id')
92 if not os.path.exists(aws_id_file_path):
93 raise ValueError(
94 "Please supply your AWS access key identifier and secret "
95 "access key identifier either when instantiating this %s "
96 "or in the %s file (on two lines).\n" %
97 (self.__class__.__name__, aws_id_file_path))
98 aws_file = open(aws_id_file_path, 'r')
99 try:
100 identifier = aws_file.readline().strip()
101 secret_identifier = aws_file.readline().strip()
102 finally:
103 aws_file.close()
104 else:
105 assert aws_id_file_path is None, \
106 'if you supply the identifier and secret_identifier, ' \
107 'do not specify the aws_id_file_path'
108 assert secret_identifier is not None, \
109 'supply both or neither of identifier, secret_identifier'
110
111 region_found = None
112
113
114 if region is not None:
115 for r in boto.ec2.regions(aws_access_key_id=identifier,
116 aws_secret_access_key=secret_identifier):
117
118 if r.name == region:
119 region_found = r
120
121
122 if region_found is not None:
123 self.conn = boto.ec2.connect_to_region(region,
124 aws_access_key_id=identifier,
125 aws_secret_access_key=secret_identifier)
126 else:
127 raise ValueError('The specified region does not exist: {0}'.format(region))
128
129 else:
130 self.conn = boto.connect_ec2(identifier, secret_identifier)
131
132
133
134
135
136
137
138
139
140 try:
141 key_pair = self.conn.get_all_key_pairs(keypair_name)[0]
142 assert key_pair
143
144 except boto.exception.EC2ResponseError, e:
145 if 'InvalidKeyPair.NotFound' not in e.body:
146 if 'AuthFailure' in e.body:
147 print ('POSSIBLE CAUSES OF ERROR:\n'
148 ' Did you sign up for EC2?\n'
149 ' Did you put a credit card number in your AWS '
150 'account?\n'
151 'Please doublecheck before reporting a problem.\n')
152 raise
153
154
155
156 self.conn.create_key_pair(keypair_name)
157
158
159 try:
160 group = self.conn.get_all_security_groups(security_name)[0]
161 assert group
162 except boto.exception.EC2ResponseError, e:
163 if 'InvalidGroup.NotFound' in e.body:
164 self.security_group = self.conn.create_security_group(
165 security_name,
166 'Authorization to access the buildbot instance.')
167
168
169
170
171
172
173
174 else:
175 raise
176
177
178 if self.ami is not None:
179 self.image = self.conn.get_image(self.ami)
180 else:
181
182 discard = self.get_image()
183 assert discard
184
185
186 if elastic_ip is not None:
187 elastic_ip = self.conn.get_all_addresses([elastic_ip])[0]
188 self.elastic_ip = elastic_ip
189
191 if self.image is not None:
192 return self.image
193 if self.valid_ami_location_regex:
194 level = 0
195 options = []
196 get_match = re.compile(self.valid_ami_location_regex).match
197 for image in self.conn.get_all_images(
198 owners=self.valid_ami_owners):
199
200 match = get_match(image.location)
201 if match:
202 alpha_sort = int_sort = None
203 if level < 2:
204 try:
205 alpha_sort = match.group(1)
206 except IndexError:
207 level = 2
208 else:
209 if level == 0:
210 try:
211 int_sort = int(alpha_sort)
212 except ValueError:
213 level = 1
214 options.append([int_sort, alpha_sort,
215 image.location, image.id, image])
216 if level:
217 log.msg('sorting images at level %d' % level)
218 options = [candidate[level:] for candidate in options]
219 else:
220 options = [(image.location, image.id, image) for image
221 in self.conn.get_all_images(
222 owners=self.valid_ami_owners)]
223 options.sort()
224 log.msg('sorted images (last is chosen): %s' %
225 (', '.join(
226 ['%s (%s)' % (candidate[-1].id, candidate[-1].location)
227 for candidate in options])))
228 if not options:
229 raise ValueError('no available images match constraints')
230 return options[-1][-1]
231
233 if self.instance is None:
234 return None
235 return self.instance.public_dns_name
236 dns = property(dns)
237
239 if self.instance is not None:
240 raise ValueError('instance active')
241 return threads.deferToThread(self._start_instance)
242
244 image = self.get_image()
245 reservation = image.run(
246 key_name=self.keypair_name, security_groups=[self.security_name],
247 instance_type=self.instance_type, user_data=self.user_data)
248 self.instance = reservation.instances[0]
249 log.msg('%s %s starting instance %s' %
250 (self.__class__.__name__, self.slavename, self.instance.id))
251 duration = 0
252 interval = self._poll_resolution
253 while self.instance.state == PENDING:
254 time.sleep(interval)
255 duration += interval
256 if duration % 60 == 0:
257 log.msg('%s %s has waited %d minutes for instance %s' %
258 (self.__class__.__name__, self.slavename, duration//60,
259 self.instance.id))
260 self.instance.update()
261 if self.instance.state == RUNNING:
262 self.output = self.instance.get_console_output()
263 minutes = duration//60
264 seconds = duration%60
265 log.msg('%s %s instance %s started on %s '
266 'in about %d minutes %d seconds (%s)' %
267 (self.__class__.__name__, self.slavename,
268 self.instance.id, self.dns, minutes, seconds,
269 self.output.output))
270 if self.elastic_ip is not None:
271 self.instance.use_ip(self.elastic_ip)
272 return [self.instance.id,
273 image.id,
274 '%02d:%02d:%02d' % (minutes//60, minutes%60, seconds)]
275 else:
276 log.msg('%s %s failed to start instance %s (%s)' %
277 (self.__class__.__name__, self.slavename,
278 self.instance.id, self.instance.state))
279 raise interfaces.LatentBuildSlaveFailedToSubstantiate(
280 self.instance.id, self.instance.state)
281
283 if self.instance is None:
284
285
286
287 return defer.succeed(None)
288 instance = self.instance
289 self.output = self.instance = None
290 return threads.deferToThread(
291 self._stop_instance, instance, fast)
292
294 if self.elastic_ip is not None:
295 self.conn.disassociate_address(self.elastic_ip.public_ip)
296 instance.update()
297 if instance.state not in (SHUTTINGDOWN, TERMINATED):
298 instance.terminate()
299 log.msg('%s %s terminating instance %s' %
300 (self.__class__.__name__, self.slavename, instance.id))
301 duration = 0
302 interval = self._poll_resolution
303 if fast:
304 goal = (SHUTTINGDOWN, TERMINATED)
305 instance.update()
306 else:
307 goal = (TERMINATED,)
308 while instance.state not in goal:
309 time.sleep(interval)
310 duration += interval
311 if duration % 60 == 0:
312 log.msg(
313 '%s %s has waited %d minutes for instance %s to end' %
314 (self.__class__.__name__, self.slavename, duration//60,
315 instance.id))
316 instance.update()
317 log.msg('%s %s instance %s %s '
318 'after about %d minutes %d seconds' %
319 (self.__class__.__name__, self.slavename,
320 instance.id, goal, duration//60, duration%60))
321