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  # Portions Copyright Canonical Ltd. 2009 
 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   
39 -class EC2LatentBuildSlave(AbstractLatentBuildSlave):
40 41 instance = image = None 42 _poll_resolution = 5 # hook point for tests 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 # verify that regex will compile 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 # Make the EC2 connection. 109 self.conn = boto.connect_ec2(identifier, secret_identifier) 110 111 # Make a keypair 112 # 113 # We currently discard the keypair data because we don't need it. 114 # If we do need it in the future, we will always recreate the keypairs 115 # because there is no way to 116 # programmatically retrieve the private key component, unless we 117 # generate it and store it on the filesystem, which is an unnecessary 118 # usage requirement. 119 try: 120 key_pair = self.conn.get_all_key_pairs(keypair_name)[0] 121 assert key_pair 122 # key_pair.delete() # would be used to recreate 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 # make one; we would always do this, and stash the result, if we 133 # needed the key (for instance, to SSH to the box). We'd then 134 # use paramiko to use the key to connect. 135 self.conn.create_key_pair(keypair_name) 136 137 # create security group 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 # Authorize the master as necessary 147 # TODO this is where we'd open the hole to do the reverse pb 148 # connect to the buildbot 149 # ip = urllib.urlopen( 150 # 'http://checkip.amazonaws.com').read().strip() 151 # self.security_group.authorize('tcp', 22, 22, '%s/32' % ip) 152 # self.security_group.authorize('tcp', 80, 80, '%s/32' % ip) 153 else: 154 raise 155 156 # get the image 157 if self.ami is not None: 158 self.image = self.conn.get_image(self.ami) 159 else: 160 # verify we have access to at least one acceptable image 161 discard = self.get_image() 162 assert discard 163 164 # get the specified elastic IP, if any 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
169 - def get_image(self):
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 # gather sorting data 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
211 - def dns(self):
212 if self.instance is None: 213 return None 214 return self.instance.public_dns_name
215 dns = property(dns) 216
217 - def start_instance(self):
218 if self.instance is not None: 219 raise ValueError('instance active') 220 return threads.deferToThread(self._start_instance)
221
222 - def _start_instance(self):
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
261 - def stop_instance(self, fast=False):
262 if self.instance is None: 263 # be gentle. Something may just be trying to alert us that an 264 # instance never attached, and it's because, somehow, we never 265 # started. 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
272 - def _stop_instance(self, instance, fast):
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