OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ |
| 2 # Copyright (c) 2010 Chris Moyer http://coredumped.org/ |
| 3 # |
| 4 # Permission is hereby granted, free of charge, to any person obtaining a |
| 5 # copy of this software and associated documentation files (the |
| 6 # "Software"), to deal in the Software without restriction, including |
| 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 9 # persons to whom the Software is furnished to do so, subject to the fol- |
| 10 # lowing conditions: |
| 11 # |
| 12 # The above copyright notice and this permission notice shall be included |
| 13 # in all copies or substantial portions of the Software. |
| 14 # |
| 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 21 # IN THE SOFTWARE. |
| 22 |
| 23 """ |
| 24 High-level abstraction of an EC2 server |
| 25 """ |
| 26 from __future__ import with_statement |
| 27 import boto.ec2 |
| 28 from boto.mashups.iobject import IObject |
| 29 from boto.pyami.config import BotoConfigPath, Config |
| 30 from boto.sdb.db.model import Model |
| 31 from boto.sdb.db.property import StringProperty, IntegerProperty, BooleanPropert
y, CalculatedProperty |
| 32 from boto.manage import propget |
| 33 from boto.ec2.zone import Zone |
| 34 from boto.ec2.keypair import KeyPair |
| 35 import os, time, StringIO |
| 36 from contextlib import closing |
| 37 from boto.exception import EC2ResponseError |
| 38 |
| 39 InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge', |
| 40 'c1.medium', 'c1.xlarge', |
| 41 'm2.2xlarge', 'm2.4xlarge'] |
| 42 |
| 43 class Bundler(object): |
| 44 |
| 45 def __init__(self, server, uname='root'): |
| 46 from boto.manage.cmdshell import SSHClient |
| 47 self.server = server |
| 48 self.uname = uname |
| 49 self.ssh_client = SSHClient(server, uname=uname) |
| 50 |
| 51 def copy_x509(self, key_file, cert_file): |
| 52 print '\tcopying cert and pk over to /mnt directory on server' |
| 53 self.ssh_client.open_sftp() |
| 54 path, name = os.path.split(key_file) |
| 55 self.remote_key_file = '/mnt/%s' % name |
| 56 self.ssh_client.put_file(key_file, self.remote_key_file) |
| 57 path, name = os.path.split(cert_file) |
| 58 self.remote_cert_file = '/mnt/%s' % name |
| 59 self.ssh_client.put_file(cert_file, self.remote_cert_file) |
| 60 print '...complete!' |
| 61 |
| 62 def bundle_image(self, prefix, size, ssh_key): |
| 63 command = "" |
| 64 if self.uname != 'root': |
| 65 command = "sudo " |
| 66 command += 'ec2-bundle-vol ' |
| 67 command += '-c %s -k %s ' % (self.remote_cert_file, self.remote_key_file
) |
| 68 command += '-u %s ' % self.server._reservation.owner_id |
| 69 command += '-p %s ' % prefix |
| 70 command += '-s %d ' % size |
| 71 command += '-d /mnt ' |
| 72 if self.server.instance_type == 'm1.small' or self.server.instance_type
== 'c1.medium': |
| 73 command += '-r i386' |
| 74 else: |
| 75 command += '-r x86_64' |
| 76 return command |
| 77 |
| 78 def upload_bundle(self, bucket, prefix, ssh_key): |
| 79 command = "" |
| 80 if self.uname != 'root': |
| 81 command = "sudo " |
| 82 command += 'ec2-upload-bundle ' |
| 83 command += '-m /mnt/%s.manifest.xml ' % prefix |
| 84 command += '-b %s ' % bucket |
| 85 command += '-a %s ' % self.server.ec2.aws_access_key_id |
| 86 command += '-s %s ' % self.server.ec2.aws_secret_access_key |
| 87 return command |
| 88 |
| 89 def bundle(self, bucket=None, prefix=None, key_file=None, cert_file=None, |
| 90 size=None, ssh_key=None, fp=None, clear_history=True): |
| 91 iobject = IObject() |
| 92 if not bucket: |
| 93 bucket = iobject.get_string('Name of S3 bucket') |
| 94 if not prefix: |
| 95 prefix = iobject.get_string('Prefix for AMI file') |
| 96 if not key_file: |
| 97 key_file = iobject.get_filename('Path to RSA private key file') |
| 98 if not cert_file: |
| 99 cert_file = iobject.get_filename('Path to RSA public cert file') |
| 100 if not size: |
| 101 size = iobject.get_int('Size (in MB) of bundled image') |
| 102 if not ssh_key: |
| 103 ssh_key = self.server.get_ssh_key_file() |
| 104 self.copy_x509(key_file, cert_file) |
| 105 if not fp: |
| 106 fp = StringIO.StringIO() |
| 107 fp.write('sudo mv %s /mnt/boto.cfg; ' % BotoConfigPath) |
| 108 fp.write('mv ~/.ssh/authorized_keys /mnt/authorized_keys; ') |
| 109 if clear_history: |
| 110 fp.write('history -c; ') |
| 111 fp.write(self.bundle_image(prefix, size, ssh_key)) |
| 112 fp.write('; ') |
| 113 fp.write(self.upload_bundle(bucket, prefix, ssh_key)) |
| 114 fp.write('; ') |
| 115 fp.write('sudo mv /mnt/boto.cfg %s; ' % BotoConfigPath) |
| 116 fp.write('mv /mnt/authorized_keys ~/.ssh/authorized_keys') |
| 117 command = fp.getvalue() |
| 118 print 'running the following command on the remote server:' |
| 119 print command |
| 120 t = self.ssh_client.run(command) |
| 121 print '\t%s' % t[0] |
| 122 print '\t%s' % t[1] |
| 123 print '...complete!' |
| 124 print 'registering image...' |
| 125 self.image_id = self.server.ec2.register_image(name=prefix, image_locati
on='%s/%s.manifest.xml' % (bucket, prefix)) |
| 126 return self.image_id |
| 127 |
| 128 class CommandLineGetter(object): |
| 129 |
| 130 def get_ami_list(self): |
| 131 my_amis = [] |
| 132 for ami in self.ec2.get_all_images(): |
| 133 # hack alert, need a better way to do this! |
| 134 if ami.location.find('pyami') >= 0: |
| 135 my_amis.append((ami.location, ami)) |
| 136 return my_amis |
| 137 |
| 138 def get_region(self, params): |
| 139 region = params.get('region', None) |
| 140 if isinstance(region, str) or isinstance(region, unicode): |
| 141 region = boto.ec2.get_region(region) |
| 142 params['region'] = region |
| 143 if not region: |
| 144 prop = self.cls.find_property('region_name') |
| 145 params['region'] = propget.get(prop, choices=boto.ec2.regions) |
| 146 self.ec2 = params['region'].connect() |
| 147 |
| 148 def get_name(self, params): |
| 149 if not params.get('name', None): |
| 150 prop = self.cls.find_property('name') |
| 151 params['name'] = propget.get(prop) |
| 152 |
| 153 def get_description(self, params): |
| 154 if not params.get('description', None): |
| 155 prop = self.cls.find_property('description') |
| 156 params['description'] = propget.get(prop) |
| 157 |
| 158 def get_instance_type(self, params): |
| 159 if not params.get('instance_type', None): |
| 160 prop = StringProperty(name='instance_type', verbose_name='Instance T
ype', |
| 161 choices=InstanceTypes) |
| 162 params['instance_type'] = propget.get(prop) |
| 163 |
| 164 def get_quantity(self, params): |
| 165 if not params.get('quantity', None): |
| 166 prop = IntegerProperty(name='quantity', verbose_name='Number of Inst
ances') |
| 167 params['quantity'] = propget.get(prop) |
| 168 |
| 169 def get_zone(self, params): |
| 170 if not params.get('zone', None): |
| 171 prop = StringProperty(name='zone', verbose_name='EC2 Availability Zo
ne', |
| 172 choices=self.ec2.get_all_zones) |
| 173 params['zone'] = propget.get(prop) |
| 174 |
| 175 def get_ami_id(self, params): |
| 176 valid = False |
| 177 while not valid: |
| 178 ami = params.get('ami', None) |
| 179 if not ami: |
| 180 prop = StringProperty(name='ami', verbose_name='AMI') |
| 181 ami = propget.get(prop) |
| 182 try: |
| 183 rs = self.ec2.get_all_images([ami]) |
| 184 if len(rs) == 1: |
| 185 valid = True |
| 186 params['ami'] = rs[0] |
| 187 except EC2ResponseError: |
| 188 pass |
| 189 |
| 190 def get_group(self, params): |
| 191 group = params.get('group', None) |
| 192 if isinstance(group, str) or isinstance(group, unicode): |
| 193 group_list = self.ec2.get_all_security_groups() |
| 194 for g in group_list: |
| 195 if g.name == group: |
| 196 group = g |
| 197 params['group'] = g |
| 198 if not group: |
| 199 prop = StringProperty(name='group', verbose_name='EC2 Security Group
', |
| 200 choices=self.ec2.get_all_security_groups) |
| 201 params['group'] = propget.get(prop) |
| 202 |
| 203 def get_key(self, params): |
| 204 keypair = params.get('keypair', None) |
| 205 if isinstance(keypair, str) or isinstance(keypair, unicode): |
| 206 key_list = self.ec2.get_all_key_pairs() |
| 207 for k in key_list: |
| 208 if k.name == keypair: |
| 209 keypair = k.name |
| 210 params['keypair'] = k.name |
| 211 if not keypair: |
| 212 prop = StringProperty(name='keypair', verbose_name='EC2 KeyPair', |
| 213 choices=self.ec2.get_all_key_pairs) |
| 214 params['keypair'] = propget.get(prop).name |
| 215 |
| 216 def get(self, cls, params): |
| 217 self.cls = cls |
| 218 self.get_region(params) |
| 219 self.ec2 = params['region'].connect() |
| 220 self.get_name(params) |
| 221 self.get_description(params) |
| 222 self.get_instance_type(params) |
| 223 self.get_zone(params) |
| 224 self.get_quantity(params) |
| 225 self.get_ami_id(params) |
| 226 self.get_group(params) |
| 227 self.get_key(params) |
| 228 |
| 229 class Server(Model): |
| 230 |
| 231 # |
| 232 # The properties of this object consists of real properties for data that |
| 233 # is not already stored in EC2 somewhere (e.g. name, description) plus |
| 234 # calculated properties for all of the properties that are already in |
| 235 # EC2 (e.g. hostname, security groups, etc.) |
| 236 # |
| 237 name = StringProperty(unique=True, verbose_name="Name") |
| 238 description = StringProperty(verbose_name="Description") |
| 239 region_name = StringProperty(verbose_name="EC2 Region Name") |
| 240 instance_id = StringProperty(verbose_name="EC2 Instance ID") |
| 241 elastic_ip = StringProperty(verbose_name="EC2 Elastic IP Address") |
| 242 production = BooleanProperty(verbose_name="Is This Server Production", defau
lt=False) |
| 243 ami_id = CalculatedProperty(verbose_name="AMI ID", calculated_type=str, use_
method=True) |
| 244 zone = CalculatedProperty(verbose_name="Availability Zone Name", calculated_
type=str, use_method=True) |
| 245 hostname = CalculatedProperty(verbose_name="Public DNS Name", calculated_typ
e=str, use_method=True) |
| 246 private_hostname = CalculatedProperty(verbose_name="Private DNS Name", calcu
lated_type=str, use_method=True) |
| 247 groups = CalculatedProperty(verbose_name="Security Groups", calculated_type=
list, use_method=True) |
| 248 security_group = CalculatedProperty(verbose_name="Primary Security Group Nam
e", calculated_type=str, use_method=True) |
| 249 key_name = CalculatedProperty(verbose_name="Key Name", calculated_type=str,
use_method=True) |
| 250 instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_
type=str, use_method=True) |
| 251 status = CalculatedProperty(verbose_name="Current Status", calculated_type=s
tr, use_method=True) |
| 252 launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculat
ed_type=str, use_method=True) |
| 253 console_output = CalculatedProperty(verbose_name="Console Output", calculate
d_type=file, use_method=True) |
| 254 |
| 255 packages = [] |
| 256 plugins = [] |
| 257 |
| 258 @classmethod |
| 259 def add_credentials(cls, cfg, aws_access_key_id, aws_secret_access_key): |
| 260 if not cfg.has_section('Credentials'): |
| 261 cfg.add_section('Credentials') |
| 262 cfg.set('Credentials', 'aws_access_key_id', aws_access_key_id) |
| 263 cfg.set('Credentials', 'aws_secret_access_key', aws_secret_access_key) |
| 264 if not cfg.has_section('DB_Server'): |
| 265 cfg.add_section('DB_Server') |
| 266 cfg.set('DB_Server', 'db_type', 'SimpleDB') |
| 267 cfg.set('DB_Server', 'db_name', cls._manager.domain.name) |
| 268 |
| 269 @classmethod |
| 270 def create(cls, config_file=None, logical_volume = None, cfg = None, **param
s): |
| 271 """ |
| 272 Create a new instance based on the specified configuration file or the s
pecified |
| 273 configuration and the passed in parameters. |
| 274 |
| 275 If the config_file argument is not None, the configuration is read from
there. |
| 276 Otherwise, the cfg argument is used. |
| 277 |
| 278 The config file may include other config files with a #import reference.
The included |
| 279 config files must reside in the same directory as the specified file. |
| 280 |
| 281 The logical_volume argument, if supplied, will be used to get the curren
t physical |
| 282 volume ID and use that as an override of the value specified in the conf
ig file. This |
| 283 may be useful for debugging purposes when you want to debug with a produ
ction config |
| 284 file but a test Volume. |
| 285 |
| 286 The dictionary argument may be used to override any EC2 configuration va
lues in the |
| 287 config file. |
| 288 """ |
| 289 if config_file: |
| 290 cfg = Config(path=config_file) |
| 291 if cfg.has_section('EC2'): |
| 292 # include any EC2 configuration values that aren't specified in para
ms: |
| 293 for option in cfg.options('EC2'): |
| 294 if option not in params: |
| 295 params[option] = cfg.get('EC2', option) |
| 296 getter = CommandLineGetter() |
| 297 getter.get(cls, params) |
| 298 region = params.get('region') |
| 299 ec2 = region.connect() |
| 300 cls.add_credentials(cfg, ec2.aws_access_key_id, ec2.aws_secret_access_ke
y) |
| 301 ami = params.get('ami') |
| 302 kp = params.get('keypair') |
| 303 group = params.get('group') |
| 304 zone = params.get('zone') |
| 305 # deal with possibly passed in logical volume: |
| 306 if logical_volume != None: |
| 307 cfg.set('EBS', 'logical_volume_name', logical_volume.name) |
| 308 cfg_fp = StringIO.StringIO() |
| 309 cfg.write(cfg_fp) |
| 310 # deal with the possibility that zone and/or keypair are strings read fr
om the config file: |
| 311 if isinstance(zone, Zone): |
| 312 zone = zone.name |
| 313 if isinstance(kp, KeyPair): |
| 314 kp = kp.name |
| 315 reservation = ami.run(min_count=1, |
| 316 max_count=params.get('quantity', 1), |
| 317 key_name=kp, |
| 318 security_groups=[group], |
| 319 instance_type=params.get('instance_type'), |
| 320 placement = zone, |
| 321 user_data = cfg_fp.getvalue()) |
| 322 l = [] |
| 323 i = 0 |
| 324 elastic_ip = params.get('elastic_ip') |
| 325 instances = reservation.instances |
| 326 if elastic_ip != None and instances.__len__() > 0: |
| 327 instance = instances[0] |
| 328 print 'Waiting for instance to start so we can set its elastic IP ad
dress...' |
| 329 # Sometimes we get a message from ec2 that says that the instance do
es not exist. |
| 330 # Hopefully the following delay will giv eec2 enough time to get to
a stable state: |
| 331 time.sleep(5) |
| 332 while instance.update() != 'running': |
| 333 time.sleep(1) |
| 334 instance.use_ip(elastic_ip) |
| 335 print 'set the elastic IP of the first instance to %s' % elastic_ip |
| 336 for instance in instances: |
| 337 s = cls() |
| 338 s.ec2 = ec2 |
| 339 s.name = params.get('name') + '' if i==0 else str(i) |
| 340 s.description = params.get('description') |
| 341 s.region_name = region.name |
| 342 s.instance_id = instance.id |
| 343 if elastic_ip and i == 0: |
| 344 s.elastic_ip = elastic_ip |
| 345 s.put() |
| 346 l.append(s) |
| 347 i += 1 |
| 348 return l |
| 349 |
| 350 @classmethod |
| 351 def create_from_instance_id(cls, instance_id, name, description=''): |
| 352 regions = boto.ec2.regions() |
| 353 for region in regions: |
| 354 ec2 = region.connect() |
| 355 try: |
| 356 rs = ec2.get_all_instances([instance_id]) |
| 357 except: |
| 358 rs = [] |
| 359 if len(rs) == 1: |
| 360 s = cls() |
| 361 s.ec2 = ec2 |
| 362 s.name = name |
| 363 s.description = description |
| 364 s.region_name = region.name |
| 365 s.instance_id = instance_id |
| 366 s._reservation = rs[0] |
| 367 for instance in s._reservation.instances: |
| 368 if instance.id == instance_id: |
| 369 s._instance = instance |
| 370 s.put() |
| 371 return s |
| 372 return None |
| 373 |
| 374 @classmethod |
| 375 def create_from_current_instances(cls): |
| 376 servers = [] |
| 377 regions = boto.ec2.regions() |
| 378 for region in regions: |
| 379 ec2 = region.connect() |
| 380 rs = ec2.get_all_instances() |
| 381 for reservation in rs: |
| 382 for instance in reservation.instances: |
| 383 try: |
| 384 Server.find(instance_id=instance.id).next() |
| 385 boto.log.info('Server for %s already exists' % instance.
id) |
| 386 except StopIteration: |
| 387 s = cls() |
| 388 s.ec2 = ec2 |
| 389 s.name = instance.id |
| 390 s.region_name = region.name |
| 391 s.instance_id = instance.id |
| 392 s._reservation = reservation |
| 393 s.put() |
| 394 servers.append(s) |
| 395 return servers |
| 396 |
| 397 def __init__(self, id=None, **kw): |
| 398 Model.__init__(self, id, **kw) |
| 399 self.ssh_key_file = None |
| 400 self.ec2 = None |
| 401 self._cmdshell = None |
| 402 self._reservation = None |
| 403 self._instance = None |
| 404 self._setup_ec2() |
| 405 |
| 406 def _setup_ec2(self): |
| 407 if self.ec2 and self._instance and self._reservation: |
| 408 return |
| 409 if self.id: |
| 410 if self.region_name: |
| 411 for region in boto.ec2.regions(): |
| 412 if region.name == self.region_name: |
| 413 self.ec2 = region.connect() |
| 414 if self.instance_id and not self._instance: |
| 415 try: |
| 416 rs = self.ec2.get_all_instances([self.instance_i
d]) |
| 417 if len(rs) >= 1: |
| 418 for instance in rs[0].instances: |
| 419 if instance.id == self.instance_id: |
| 420 self._reservation = rs[0] |
| 421 self._instance = instance |
| 422 except EC2ResponseError: |
| 423 pass |
| 424 |
| 425 def _status(self): |
| 426 status = '' |
| 427 if self._instance: |
| 428 self._instance.update() |
| 429 status = self._instance.state |
| 430 return status |
| 431 |
| 432 def _hostname(self): |
| 433 hostname = '' |
| 434 if self._instance: |
| 435 hostname = self._instance.public_dns_name |
| 436 return hostname |
| 437 |
| 438 def _private_hostname(self): |
| 439 hostname = '' |
| 440 if self._instance: |
| 441 hostname = self._instance.private_dns_name |
| 442 return hostname |
| 443 |
| 444 def _instance_type(self): |
| 445 it = '' |
| 446 if self._instance: |
| 447 it = self._instance.instance_type |
| 448 return it |
| 449 |
| 450 def _launch_time(self): |
| 451 lt = '' |
| 452 if self._instance: |
| 453 lt = self._instance.launch_time |
| 454 return lt |
| 455 |
| 456 def _console_output(self): |
| 457 co = '' |
| 458 if self._instance: |
| 459 co = self._instance.get_console_output() |
| 460 return co |
| 461 |
| 462 def _groups(self): |
| 463 gn = [] |
| 464 if self._reservation: |
| 465 gn = self._reservation.groups |
| 466 return gn |
| 467 |
| 468 def _security_group(self): |
| 469 groups = self._groups() |
| 470 if len(groups) >= 1: |
| 471 return groups[0].id |
| 472 return "" |
| 473 |
| 474 def _zone(self): |
| 475 zone = None |
| 476 if self._instance: |
| 477 zone = self._instance.placement |
| 478 return zone |
| 479 |
| 480 def _key_name(self): |
| 481 kn = None |
| 482 if self._instance: |
| 483 kn = self._instance.key_name |
| 484 return kn |
| 485 |
| 486 def put(self): |
| 487 Model.put(self) |
| 488 self._setup_ec2() |
| 489 |
| 490 def delete(self): |
| 491 if self.production: |
| 492 raise ValueError("Can't delete a production server") |
| 493 #self.stop() |
| 494 Model.delete(self) |
| 495 |
| 496 def stop(self): |
| 497 if self.production: |
| 498 raise ValueError("Can't delete a production server") |
| 499 if self._instance: |
| 500 self._instance.stop() |
| 501 |
| 502 def terminate(self): |
| 503 if self.production: |
| 504 raise ValueError("Can't delete a production server") |
| 505 if self._instance: |
| 506 self._instance.terminate() |
| 507 |
| 508 def reboot(self): |
| 509 if self._instance: |
| 510 self._instance.reboot() |
| 511 |
| 512 def wait(self): |
| 513 while self.status != 'running': |
| 514 time.sleep(5) |
| 515 |
| 516 def get_ssh_key_file(self): |
| 517 if not self.ssh_key_file: |
| 518 ssh_dir = os.path.expanduser('~/.ssh') |
| 519 if os.path.isdir(ssh_dir): |
| 520 ssh_file = os.path.join(ssh_dir, '%s.pem' % self.key_name) |
| 521 if os.path.isfile(ssh_file): |
| 522 self.ssh_key_file = ssh_file |
| 523 if not self.ssh_key_file: |
| 524 iobject = IObject() |
| 525 self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key fi
le') |
| 526 return self.ssh_key_file |
| 527 |
| 528 def get_cmdshell(self): |
| 529 if not self._cmdshell: |
| 530 import cmdshell |
| 531 self.get_ssh_key_file() |
| 532 self._cmdshell = cmdshell.start(self) |
| 533 return self._cmdshell |
| 534 |
| 535 def reset_cmdshell(self): |
| 536 self._cmdshell = None |
| 537 |
| 538 def run(self, command): |
| 539 with closing(self.get_cmdshell()) as cmd: |
| 540 status = cmd.run(command) |
| 541 return status |
| 542 |
| 543 def get_bundler(self, uname='root'): |
| 544 self.get_ssh_key_file() |
| 545 return Bundler(self, uname) |
| 546 |
| 547 def get_ssh_client(self, uname='root', ssh_pwd=None): |
| 548 from boto.manage.cmdshell import SSHClient |
| 549 self.get_ssh_key_file() |
| 550 return SSHClient(self, uname=uname, ssh_pwd=ssh_pwd) |
| 551 |
| 552 def install(self, pkg): |
| 553 return self.run('apt-get -y install %s' % pkg) |
| 554 |
| 555 |
| 556 |
OLD | NEW |