Package buildbot :: Module ec2buildslave
[frames] | no frames]

Source Code for Module buildbot.ec2buildslave

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Portions Copyright Buildbot Team Members 
 15   
 16  from __future__ import with_statement 
 17  # Portions Copyright Canonical Ltd. 2009 
 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   
42 -class EC2LatentBuildSlave(AbstractLatentBuildSlave):
43 44 instance = image = None 45 _poll_resolution = 5 # hook point for tests 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 # verify that regex will compile 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 # Make the EC2 connection. 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 # Make a keypair 132 # 133 # We currently discard the keypair data because we don't need it. 134 # If we do need it in the future, we will always recreate the keypairs 135 # because there is no way to 136 # programmatically retrieve the private key component, unless we 137 # generate it and store it on the filesystem, which is an unnecessary 138 # usage requirement. 139 try: 140 key_pair = self.conn.get_all_key_pairs(keypair_name)[0] 141 assert key_pair 142 # key_pair.delete() # would be used to recreate 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 # make one; we would always do this, and stash the result, if we 153 # needed the key (for instance, to SSH to the box). We'd then 154 # use paramiko to use the key to connect. 155 self.conn.create_key_pair(keypair_name) 156 157 # create security group 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 # Authorize the master as necessary 167 # TODO this is where we'd open the hole to do the reverse pb 168 # connect to the buildbot 169 # ip = urllib.urlopen( 170 # 'http://checkip.amazonaws.com').read().strip() 171 # self.security_group.authorize('tcp', 22, 22, '%s/32' % ip) 172 # self.security_group.authorize('tcp', 80, 80, '%s/32' % ip) 173 else: 174 raise 175 176 # get the image 177 if self.ami is not None: 178 self.image = self.conn.get_image(self.ami) 179 else: 180 # verify we have access to at least one acceptable image 181 discard = self.get_image() 182 assert discard 183 184 # get the specified elastic IP, if any 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
189 - def get_image(self):
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 # gather sorting data 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
231 - def dns(self):
232 if self.instance is None: 233 return None 234 return self.instance.public_dns_name
235 dns = property(dns) 236
237 - def start_instance(self, build):
238 if self.instance is not None: 239 raise ValueError('instance active') 240 return threads.deferToThread(self._start_instance)
241
242 - def _start_instance(self):
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
281 - def stop_instance(self, fast=False):
282 if self.instance is None: 283 # be gentle. Something may just be trying to alert us that an 284 # instance never attached, and it's because, somehow, we never 285 # started. 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
292 - def _stop_instance(self, instance, fast):
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