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