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