Index: third_party/boto/boto/manage/server.py |
diff --git a/third_party/boto/boto/manage/server.py b/third_party/boto/boto/manage/server.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2a2b1f1634bfeb889ea62067fa6e1c3a97a0bd53 |
--- /dev/null |
+++ b/third_party/boto/boto/manage/server.py |
@@ -0,0 +1,556 @@ |
+# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ |
+# Copyright (c) 2010 Chris Moyer http://coredumped.org/ |
+# |
+# Permission is hereby granted, free of charge, to any person obtaining a |
+# copy of this software and associated documentation files (the |
+# "Software"), to deal in the Software without restriction, including |
+# without limitation the rights to use, copy, modify, merge, publish, dis- |
+# tribute, sublicense, and/or sell copies of the Software, and to permit |
+# persons to whom the Software is furnished to do so, subject to the fol- |
+# lowing conditions: |
+# |
+# The above copyright notice and this permission notice shall be included |
+# in all copies or substantial portions of the Software. |
+# |
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
+# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
+# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
+# IN THE SOFTWARE. |
+ |
+""" |
+High-level abstraction of an EC2 server |
+""" |
+from __future__ import with_statement |
+import boto.ec2 |
+from boto.mashups.iobject import IObject |
+from boto.pyami.config import BotoConfigPath, Config |
+from boto.sdb.db.model import Model |
+from boto.sdb.db.property import StringProperty, IntegerProperty, BooleanProperty, CalculatedProperty |
+from boto.manage import propget |
+from boto.ec2.zone import Zone |
+from boto.ec2.keypair import KeyPair |
+import os, time, StringIO |
+from contextlib import closing |
+from boto.exception import EC2ResponseError |
+ |
+InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge', |
+ 'c1.medium', 'c1.xlarge', |
+ 'm2.2xlarge', 'm2.4xlarge'] |
+ |
+class Bundler(object): |
+ |
+ def __init__(self, server, uname='root'): |
+ from boto.manage.cmdshell import SSHClient |
+ self.server = server |
+ self.uname = uname |
+ self.ssh_client = SSHClient(server, uname=uname) |
+ |
+ def copy_x509(self, key_file, cert_file): |
+ print '\tcopying cert and pk over to /mnt directory on server' |
+ self.ssh_client.open_sftp() |
+ path, name = os.path.split(key_file) |
+ self.remote_key_file = '/mnt/%s' % name |
+ self.ssh_client.put_file(key_file, self.remote_key_file) |
+ path, name = os.path.split(cert_file) |
+ self.remote_cert_file = '/mnt/%s' % name |
+ self.ssh_client.put_file(cert_file, self.remote_cert_file) |
+ print '...complete!' |
+ |
+ def bundle_image(self, prefix, size, ssh_key): |
+ command = "" |
+ if self.uname != 'root': |
+ command = "sudo " |
+ command += 'ec2-bundle-vol ' |
+ command += '-c %s -k %s ' % (self.remote_cert_file, self.remote_key_file) |
+ command += '-u %s ' % self.server._reservation.owner_id |
+ command += '-p %s ' % prefix |
+ command += '-s %d ' % size |
+ command += '-d /mnt ' |
+ if self.server.instance_type == 'm1.small' or self.server.instance_type == 'c1.medium': |
+ command += '-r i386' |
+ else: |
+ command += '-r x86_64' |
+ return command |
+ |
+ def upload_bundle(self, bucket, prefix, ssh_key): |
+ command = "" |
+ if self.uname != 'root': |
+ command = "sudo " |
+ command += 'ec2-upload-bundle ' |
+ command += '-m /mnt/%s.manifest.xml ' % prefix |
+ command += '-b %s ' % bucket |
+ command += '-a %s ' % self.server.ec2.aws_access_key_id |
+ command += '-s %s ' % self.server.ec2.aws_secret_access_key |
+ return command |
+ |
+ def bundle(self, bucket=None, prefix=None, key_file=None, cert_file=None, |
+ size=None, ssh_key=None, fp=None, clear_history=True): |
+ iobject = IObject() |
+ if not bucket: |
+ bucket = iobject.get_string('Name of S3 bucket') |
+ if not prefix: |
+ prefix = iobject.get_string('Prefix for AMI file') |
+ if not key_file: |
+ key_file = iobject.get_filename('Path to RSA private key file') |
+ if not cert_file: |
+ cert_file = iobject.get_filename('Path to RSA public cert file') |
+ if not size: |
+ size = iobject.get_int('Size (in MB) of bundled image') |
+ if not ssh_key: |
+ ssh_key = self.server.get_ssh_key_file() |
+ self.copy_x509(key_file, cert_file) |
+ if not fp: |
+ fp = StringIO.StringIO() |
+ fp.write('sudo mv %s /mnt/boto.cfg; ' % BotoConfigPath) |
+ fp.write('mv ~/.ssh/authorized_keys /mnt/authorized_keys; ') |
+ if clear_history: |
+ fp.write('history -c; ') |
+ fp.write(self.bundle_image(prefix, size, ssh_key)) |
+ fp.write('; ') |
+ fp.write(self.upload_bundle(bucket, prefix, ssh_key)) |
+ fp.write('; ') |
+ fp.write('sudo mv /mnt/boto.cfg %s; ' % BotoConfigPath) |
+ fp.write('mv /mnt/authorized_keys ~/.ssh/authorized_keys') |
+ command = fp.getvalue() |
+ print 'running the following command on the remote server:' |
+ print command |
+ t = self.ssh_client.run(command) |
+ print '\t%s' % t[0] |
+ print '\t%s' % t[1] |
+ print '...complete!' |
+ print 'registering image...' |
+ self.image_id = self.server.ec2.register_image(name=prefix, image_location='%s/%s.manifest.xml' % (bucket, prefix)) |
+ return self.image_id |
+ |
+class CommandLineGetter(object): |
+ |
+ def get_ami_list(self): |
+ my_amis = [] |
+ for ami in self.ec2.get_all_images(): |
+ # hack alert, need a better way to do this! |
+ if ami.location.find('pyami') >= 0: |
+ my_amis.append((ami.location, ami)) |
+ return my_amis |
+ |
+ def get_region(self, params): |
+ region = params.get('region', None) |
+ if isinstance(region, str) or isinstance(region, unicode): |
+ region = boto.ec2.get_region(region) |
+ params['region'] = region |
+ if not region: |
+ prop = self.cls.find_property('region_name') |
+ params['region'] = propget.get(prop, choices=boto.ec2.regions) |
+ self.ec2 = params['region'].connect() |
+ |
+ def get_name(self, params): |
+ if not params.get('name', None): |
+ prop = self.cls.find_property('name') |
+ params['name'] = propget.get(prop) |
+ |
+ def get_description(self, params): |
+ if not params.get('description', None): |
+ prop = self.cls.find_property('description') |
+ params['description'] = propget.get(prop) |
+ |
+ def get_instance_type(self, params): |
+ if not params.get('instance_type', None): |
+ prop = StringProperty(name='instance_type', verbose_name='Instance Type', |
+ choices=InstanceTypes) |
+ params['instance_type'] = propget.get(prop) |
+ |
+ def get_quantity(self, params): |
+ if not params.get('quantity', None): |
+ prop = IntegerProperty(name='quantity', verbose_name='Number of Instances') |
+ params['quantity'] = propget.get(prop) |
+ |
+ def get_zone(self, params): |
+ if not params.get('zone', None): |
+ prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone', |
+ choices=self.ec2.get_all_zones) |
+ params['zone'] = propget.get(prop) |
+ |
+ def get_ami_id(self, params): |
+ valid = False |
+ while not valid: |
+ ami = params.get('ami', None) |
+ if not ami: |
+ prop = StringProperty(name='ami', verbose_name='AMI') |
+ ami = propget.get(prop) |
+ try: |
+ rs = self.ec2.get_all_images([ami]) |
+ if len(rs) == 1: |
+ valid = True |
+ params['ami'] = rs[0] |
+ except EC2ResponseError: |
+ pass |
+ |
+ def get_group(self, params): |
+ group = params.get('group', None) |
+ if isinstance(group, str) or isinstance(group, unicode): |
+ group_list = self.ec2.get_all_security_groups() |
+ for g in group_list: |
+ if g.name == group: |
+ group = g |
+ params['group'] = g |
+ if not group: |
+ prop = StringProperty(name='group', verbose_name='EC2 Security Group', |
+ choices=self.ec2.get_all_security_groups) |
+ params['group'] = propget.get(prop) |
+ |
+ def get_key(self, params): |
+ keypair = params.get('keypair', None) |
+ if isinstance(keypair, str) or isinstance(keypair, unicode): |
+ key_list = self.ec2.get_all_key_pairs() |
+ for k in key_list: |
+ if k.name == keypair: |
+ keypair = k.name |
+ params['keypair'] = k.name |
+ if not keypair: |
+ prop = StringProperty(name='keypair', verbose_name='EC2 KeyPair', |
+ choices=self.ec2.get_all_key_pairs) |
+ params['keypair'] = propget.get(prop).name |
+ |
+ def get(self, cls, params): |
+ self.cls = cls |
+ self.get_region(params) |
+ self.ec2 = params['region'].connect() |
+ self.get_name(params) |
+ self.get_description(params) |
+ self.get_instance_type(params) |
+ self.get_zone(params) |
+ self.get_quantity(params) |
+ self.get_ami_id(params) |
+ self.get_group(params) |
+ self.get_key(params) |
+ |
+class Server(Model): |
+ |
+ # |
+ # The properties of this object consists of real properties for data that |
+ # is not already stored in EC2 somewhere (e.g. name, description) plus |
+ # calculated properties for all of the properties that are already in |
+ # EC2 (e.g. hostname, security groups, etc.) |
+ # |
+ name = StringProperty(unique=True, verbose_name="Name") |
+ description = StringProperty(verbose_name="Description") |
+ region_name = StringProperty(verbose_name="EC2 Region Name") |
+ instance_id = StringProperty(verbose_name="EC2 Instance ID") |
+ elastic_ip = StringProperty(verbose_name="EC2 Elastic IP Address") |
+ production = BooleanProperty(verbose_name="Is This Server Production", default=False) |
+ ami_id = CalculatedProperty(verbose_name="AMI ID", calculated_type=str, use_method=True) |
+ zone = CalculatedProperty(verbose_name="Availability Zone Name", calculated_type=str, use_method=True) |
+ hostname = CalculatedProperty(verbose_name="Public DNS Name", calculated_type=str, use_method=True) |
+ private_hostname = CalculatedProperty(verbose_name="Private DNS Name", calculated_type=str, use_method=True) |
+ groups = CalculatedProperty(verbose_name="Security Groups", calculated_type=list, use_method=True) |
+ security_group = CalculatedProperty(verbose_name="Primary Security Group Name", calculated_type=str, use_method=True) |
+ key_name = CalculatedProperty(verbose_name="Key Name", calculated_type=str, use_method=True) |
+ instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True) |
+ status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True) |
+ launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True) |
+ console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=file, use_method=True) |
+ |
+ packages = [] |
+ plugins = [] |
+ |
+ @classmethod |
+ def add_credentials(cls, cfg, aws_access_key_id, aws_secret_access_key): |
+ if not cfg.has_section('Credentials'): |
+ cfg.add_section('Credentials') |
+ cfg.set('Credentials', 'aws_access_key_id', aws_access_key_id) |
+ cfg.set('Credentials', 'aws_secret_access_key', aws_secret_access_key) |
+ if not cfg.has_section('DB_Server'): |
+ cfg.add_section('DB_Server') |
+ cfg.set('DB_Server', 'db_type', 'SimpleDB') |
+ cfg.set('DB_Server', 'db_name', cls._manager.domain.name) |
+ |
+ @classmethod |
+ def create(cls, config_file=None, logical_volume = None, cfg = None, **params): |
+ """ |
+ Create a new instance based on the specified configuration file or the specified |
+ configuration and the passed in parameters. |
+ |
+ If the config_file argument is not None, the configuration is read from there. |
+ Otherwise, the cfg argument is used. |
+ |
+ The config file may include other config files with a #import reference. The included |
+ config files must reside in the same directory as the specified file. |
+ |
+ The logical_volume argument, if supplied, will be used to get the current physical |
+ volume ID and use that as an override of the value specified in the config file. This |
+ may be useful for debugging purposes when you want to debug with a production config |
+ file but a test Volume. |
+ |
+ The dictionary argument may be used to override any EC2 configuration values in the |
+ config file. |
+ """ |
+ if config_file: |
+ cfg = Config(path=config_file) |
+ if cfg.has_section('EC2'): |
+ # include any EC2 configuration values that aren't specified in params: |
+ for option in cfg.options('EC2'): |
+ if option not in params: |
+ params[option] = cfg.get('EC2', option) |
+ getter = CommandLineGetter() |
+ getter.get(cls, params) |
+ region = params.get('region') |
+ ec2 = region.connect() |
+ cls.add_credentials(cfg, ec2.aws_access_key_id, ec2.aws_secret_access_key) |
+ ami = params.get('ami') |
+ kp = params.get('keypair') |
+ group = params.get('group') |
+ zone = params.get('zone') |
+ # deal with possibly passed in logical volume: |
+ if logical_volume != None: |
+ cfg.set('EBS', 'logical_volume_name', logical_volume.name) |
+ cfg_fp = StringIO.StringIO() |
+ cfg.write(cfg_fp) |
+ # deal with the possibility that zone and/or keypair are strings read from the config file: |
+ if isinstance(zone, Zone): |
+ zone = zone.name |
+ if isinstance(kp, KeyPair): |
+ kp = kp.name |
+ reservation = ami.run(min_count=1, |
+ max_count=params.get('quantity', 1), |
+ key_name=kp, |
+ security_groups=[group], |
+ instance_type=params.get('instance_type'), |
+ placement = zone, |
+ user_data = cfg_fp.getvalue()) |
+ l = [] |
+ i = 0 |
+ elastic_ip = params.get('elastic_ip') |
+ instances = reservation.instances |
+ if elastic_ip != None and instances.__len__() > 0: |
+ instance = instances[0] |
+ print 'Waiting for instance to start so we can set its elastic IP address...' |
+ # Sometimes we get a message from ec2 that says that the instance does not exist. |
+ # Hopefully the following delay will giv eec2 enough time to get to a stable state: |
+ time.sleep(5) |
+ while instance.update() != 'running': |
+ time.sleep(1) |
+ instance.use_ip(elastic_ip) |
+ print 'set the elastic IP of the first instance to %s' % elastic_ip |
+ for instance in instances: |
+ s = cls() |
+ s.ec2 = ec2 |
+ s.name = params.get('name') + '' if i==0 else str(i) |
+ s.description = params.get('description') |
+ s.region_name = region.name |
+ s.instance_id = instance.id |
+ if elastic_ip and i == 0: |
+ s.elastic_ip = elastic_ip |
+ s.put() |
+ l.append(s) |
+ i += 1 |
+ return l |
+ |
+ @classmethod |
+ def create_from_instance_id(cls, instance_id, name, description=''): |
+ regions = boto.ec2.regions() |
+ for region in regions: |
+ ec2 = region.connect() |
+ try: |
+ rs = ec2.get_all_instances([instance_id]) |
+ except: |
+ rs = [] |
+ if len(rs) == 1: |
+ s = cls() |
+ s.ec2 = ec2 |
+ s.name = name |
+ s.description = description |
+ s.region_name = region.name |
+ s.instance_id = instance_id |
+ s._reservation = rs[0] |
+ for instance in s._reservation.instances: |
+ if instance.id == instance_id: |
+ s._instance = instance |
+ s.put() |
+ return s |
+ return None |
+ |
+ @classmethod |
+ def create_from_current_instances(cls): |
+ servers = [] |
+ regions = boto.ec2.regions() |
+ for region in regions: |
+ ec2 = region.connect() |
+ rs = ec2.get_all_instances() |
+ for reservation in rs: |
+ for instance in reservation.instances: |
+ try: |
+ Server.find(instance_id=instance.id).next() |
+ boto.log.info('Server for %s already exists' % instance.id) |
+ except StopIteration: |
+ s = cls() |
+ s.ec2 = ec2 |
+ s.name = instance.id |
+ s.region_name = region.name |
+ s.instance_id = instance.id |
+ s._reservation = reservation |
+ s.put() |
+ servers.append(s) |
+ return servers |
+ |
+ def __init__(self, id=None, **kw): |
+ Model.__init__(self, id, **kw) |
+ self.ssh_key_file = None |
+ self.ec2 = None |
+ self._cmdshell = None |
+ self._reservation = None |
+ self._instance = None |
+ self._setup_ec2() |
+ |
+ def _setup_ec2(self): |
+ if self.ec2 and self._instance and self._reservation: |
+ return |
+ if self.id: |
+ if self.region_name: |
+ for region in boto.ec2.regions(): |
+ if region.name == self.region_name: |
+ self.ec2 = region.connect() |
+ if self.instance_id and not self._instance: |
+ try: |
+ rs = self.ec2.get_all_instances([self.instance_id]) |
+ if len(rs) >= 1: |
+ for instance in rs[0].instances: |
+ if instance.id == self.instance_id: |
+ self._reservation = rs[0] |
+ self._instance = instance |
+ except EC2ResponseError: |
+ pass |
+ |
+ def _status(self): |
+ status = '' |
+ if self._instance: |
+ self._instance.update() |
+ status = self._instance.state |
+ return status |
+ |
+ def _hostname(self): |
+ hostname = '' |
+ if self._instance: |
+ hostname = self._instance.public_dns_name |
+ return hostname |
+ |
+ def _private_hostname(self): |
+ hostname = '' |
+ if self._instance: |
+ hostname = self._instance.private_dns_name |
+ return hostname |
+ |
+ def _instance_type(self): |
+ it = '' |
+ if self._instance: |
+ it = self._instance.instance_type |
+ return it |
+ |
+ def _launch_time(self): |
+ lt = '' |
+ if self._instance: |
+ lt = self._instance.launch_time |
+ return lt |
+ |
+ def _console_output(self): |
+ co = '' |
+ if self._instance: |
+ co = self._instance.get_console_output() |
+ return co |
+ |
+ def _groups(self): |
+ gn = [] |
+ if self._reservation: |
+ gn = self._reservation.groups |
+ return gn |
+ |
+ def _security_group(self): |
+ groups = self._groups() |
+ if len(groups) >= 1: |
+ return groups[0].id |
+ return "" |
+ |
+ def _zone(self): |
+ zone = None |
+ if self._instance: |
+ zone = self._instance.placement |
+ return zone |
+ |
+ def _key_name(self): |
+ kn = None |
+ if self._instance: |
+ kn = self._instance.key_name |
+ return kn |
+ |
+ def put(self): |
+ Model.put(self) |
+ self._setup_ec2() |
+ |
+ def delete(self): |
+ if self.production: |
+ raise ValueError("Can't delete a production server") |
+ #self.stop() |
+ Model.delete(self) |
+ |
+ def stop(self): |
+ if self.production: |
+ raise ValueError("Can't delete a production server") |
+ if self._instance: |
+ self._instance.stop() |
+ |
+ def terminate(self): |
+ if self.production: |
+ raise ValueError("Can't delete a production server") |
+ if self._instance: |
+ self._instance.terminate() |
+ |
+ def reboot(self): |
+ if self._instance: |
+ self._instance.reboot() |
+ |
+ def wait(self): |
+ while self.status != 'running': |
+ time.sleep(5) |
+ |
+ def get_ssh_key_file(self): |
+ if not self.ssh_key_file: |
+ ssh_dir = os.path.expanduser('~/.ssh') |
+ if os.path.isdir(ssh_dir): |
+ ssh_file = os.path.join(ssh_dir, '%s.pem' % self.key_name) |
+ if os.path.isfile(ssh_file): |
+ self.ssh_key_file = ssh_file |
+ if not self.ssh_key_file: |
+ iobject = IObject() |
+ self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key file') |
+ return self.ssh_key_file |
+ |
+ def get_cmdshell(self): |
+ if not self._cmdshell: |
+ import cmdshell |
+ self.get_ssh_key_file() |
+ self._cmdshell = cmdshell.start(self) |
+ return self._cmdshell |
+ |
+ def reset_cmdshell(self): |
+ self._cmdshell = None |
+ |
+ def run(self, command): |
+ with closing(self.get_cmdshell()) as cmd: |
+ status = cmd.run(command) |
+ return status |
+ |
+ def get_bundler(self, uname='root'): |
+ self.get_ssh_key_file() |
+ return Bundler(self, uname) |
+ |
+ def get_ssh_client(self, uname='root', ssh_pwd=None): |
+ from boto.manage.cmdshell import SSHClient |
+ self.get_ssh_key_file() |
+ return SSHClient(self, uname=uname, ssh_pwd=ssh_pwd) |
+ |
+ def install(self, pkg): |
+ return self.run('apt-get -y install %s' % pkg) |
+ |
+ |
+ |