PageRenderTime 37ms CodeModel.GetById 13ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/boto-2.5.2/boto/mashups/server.py

#
Python | 395 lines | 359 code | 9 blank | 27 comment | 15 complexity | 939e15bc9a119331b3c1afa4c41558de MD5 | raw file
  1# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
  2#
  3# Permission is hereby granted, free of charge, to any person obtaining a
  4# copy of this software and associated documentation files (the
  5# "Software"), to deal in the Software without restriction, including
  6# without limitation the rights to use, copy, modify, merge, publish, dis-
  7# tribute, sublicense, and/or sell copies of the Software, and to permit
  8# persons to whom the Software is furnished to do so, subject to the fol-
  9# lowing conditions:
 10#
 11# The above copyright notice and this permission notice shall be included
 12# in all copies or substantial portions of the Software.
 13#
 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
 16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
 17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 20# IN THE SOFTWARE.
 21
 22"""
 23High-level abstraction of an EC2 server
 24"""
 25import boto
 26import boto.utils
 27from boto.mashups.iobject import IObject
 28from boto.pyami.config import Config, BotoConfigPath
 29from boto.mashups.interactive import interactive_shell
 30from boto.sdb.db.model import Model
 31from boto.sdb.db.property import StringProperty
 32import os
 33import StringIO
 34
 35
 36class ServerSet(list):
 37
 38    def __getattr__(self, name):
 39        results = []
 40        is_callable = False
 41        for server in self:
 42            try:
 43                val = getattr(server, name)
 44                if callable(val):
 45                    is_callable = True
 46                results.append(val)
 47            except:
 48                results.append(None)
 49        if is_callable:
 50            self.map_list = results
 51            return self.map
 52        return results
 53
 54    def map(self, *args):
 55        results = []
 56        for fn in self.map_list:
 57            results.append(fn(*args))
 58        return results
 59
 60class Server(Model):
 61
 62    @property
 63    def ec2(self):
 64        if self._ec2 is None:
 65            self._ec2 = boto.connect_ec2()
 66        return self._ec2
 67
 68    @classmethod
 69    def Inventory(cls):
 70        """
 71        Returns a list of Server instances, one for each Server object
 72        persisted in the db
 73        """
 74        l = ServerSet()
 75        rs = cls.find()
 76        for server in rs:
 77            l.append(server)
 78        return l
 79
 80    @classmethod
 81    def Register(cls, name, instance_id, description=''):
 82        s = cls()
 83        s.name = name
 84        s.instance_id = instance_id
 85        s.description = description
 86        s.save()
 87        return s
 88
 89    def __init__(self, id=None, **kw):
 90        Model.__init__(self, id, **kw)
 91        self._reservation = None
 92        self._instance = None
 93        self._ssh_client = None
 94        self._pkey = None
 95        self._config = None
 96        self._ec2 = None
 97
 98    name = StringProperty(unique=True, verbose_name="Name")
 99    instance_id = StringProperty(verbose_name="Instance ID")
100    config_uri = StringProperty()
101    ami_id = StringProperty(verbose_name="AMI ID")
102    zone = StringProperty(verbose_name="Availability Zone")
103    security_group = StringProperty(verbose_name="Security Group", default="default")
104    key_name = StringProperty(verbose_name="Key Name")
105    elastic_ip = StringProperty(verbose_name="Elastic IP")
106    instance_type = StringProperty(verbose_name="Instance Type")
107    description = StringProperty(verbose_name="Description")
108    log = StringProperty()
109
110    def setReadOnly(self, value):
111        raise AttributeError
112
113    def getInstance(self):
114        if not self._instance:
115            if self.instance_id:
116                try:
117                    rs = self.ec2.get_all_instances([self.instance_id])
118                except:
119                    return None
120                if len(rs) > 0:
121                    self._reservation = rs[0]
122                    self._instance = self._reservation.instances[0]
123        return self._instance
124
125    instance = property(getInstance, setReadOnly, None, 'The Instance for the server')
126    
127    def getAMI(self):
128        if self.instance:
129            return self.instance.image_id
130
131    ami = property(getAMI, setReadOnly, None, 'The AMI for the server')
132    
133    def getStatus(self):
134        if self.instance:
135            self.instance.update()
136            return self.instance.state
137
138    status = property(getStatus, setReadOnly, None,
139                      'The status of the server')
140    
141    def getHostname(self):
142        if self.instance:
143            return self.instance.public_dns_name
144
145    hostname = property(getHostname, setReadOnly, None,
146                        'The public DNS name of the server')
147
148    def getPrivateHostname(self):
149        if self.instance:
150            return self.instance.private_dns_name
151
152    private_hostname = property(getPrivateHostname, setReadOnly, None,
153                                'The private DNS name of the server')
154
155    def getLaunchTime(self):
156        if self.instance:
157            return self.instance.launch_time
158
159    launch_time = property(getLaunchTime, setReadOnly, None,
160                           'The time the Server was started')
161
162    def getConsoleOutput(self):
163        if self.instance:
164            return self.instance.get_console_output()
165
166    console_output = property(getConsoleOutput, setReadOnly, None,
167                              'Retrieve the console output for server')
168
169    def getGroups(self):
170        if self._reservation:
171            return self._reservation.groups
172        else:
173            return None
174
175    groups = property(getGroups, setReadOnly, None,
176                      'The Security Groups controlling access to this server')
177
178    def getConfig(self):
179        if not self._config:
180            remote_file = BotoConfigPath
181            local_file = '%s.ini' % self.instance.id
182            self.get_file(remote_file, local_file)
183            self._config = Config(local_file)
184        return self._config
185
186    def setConfig(self, config):
187        local_file = '%s.ini' % self.instance.id
188        fp = open(local_file)
189        config.write(fp)
190        fp.close()
191        self.put_file(local_file, BotoConfigPath)
192        self._config = config
193
194    config = property(getConfig, setConfig, None,
195                      'The instance data for this server')
196
197    def set_config(self, config):
198        """
199        Set SDB based config
200        """
201        self._config = config
202        self._config.dump_to_sdb("botoConfigs", self.id)
203
204    def load_config(self):
205        self._config = Config(do_load=False)
206        self._config.load_from_sdb("botoConfigs", self.id)
207
208    def stop(self):
209        if self.instance:
210            self.instance.stop()
211
212    def start(self):
213        self.stop()
214        ec2 = boto.connect_ec2()
215        ami = ec2.get_all_images(image_ids = [str(self.ami_id)])[0]
216        groups = ec2.get_all_security_groups(groupnames=[str(self.security_group)])
217        if not self._config:
218            self.load_config()
219        if not self._config.has_section("Credentials"):
220            self._config.add_section("Credentials")
221            self._config.set("Credentials", "aws_access_key_id", ec2.aws_access_key_id)
222            self._config.set("Credentials", "aws_secret_access_key", ec2.aws_secret_access_key)
223
224        if not self._config.has_section("Pyami"):
225            self._config.add_section("Pyami")
226
227        if self._manager.domain:
228            self._config.set('Pyami', 'server_sdb_domain', self._manager.domain.name)
229            self._config.set("Pyami", 'server_sdb_name', self.name)
230
231        cfg = StringIO.StringIO()
232        self._config.write(cfg)
233        cfg = cfg.getvalue()
234        r = ami.run(min_count=1,
235                    max_count=1,
236                    key_name=self.key_name,
237                    security_groups = groups,
238                    instance_type = self.instance_type,
239                    placement = self.zone,
240                    user_data = cfg)
241        i = r.instances[0]
242        self.instance_id = i.id
243        self.put()
244        if self.elastic_ip:
245            ec2.associate_address(self.instance_id, self.elastic_ip)
246
247    def reboot(self):
248        if self.instance:
249            self.instance.reboot()
250
251    def get_ssh_client(self, key_file=None, host_key_file='~/.ssh/known_hosts',
252                       uname='root'):
253        import paramiko
254        if not self.instance:
255            print 'No instance yet!'
256            return
257        if not self._ssh_client:
258            if not key_file:
259                iobject = IObject()
260                key_file = iobject.get_filename('Path to OpenSSH Key file')
261            self._pkey = paramiko.RSAKey.from_private_key_file(key_file)
262            self._ssh_client = paramiko.SSHClient()
263            self._ssh_client.load_system_host_keys()
264            self._ssh_client.load_host_keys(os.path.expanduser(host_key_file))
265            self._ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
266            self._ssh_client.connect(self.instance.public_dns_name,
267                                     username=uname, pkey=self._pkey)
268        return self._ssh_client
269
270    def get_file(self, remotepath, localpath):
271        ssh_client = self.get_ssh_client()
272        sftp_client = ssh_client.open_sftp()
273        sftp_client.get(remotepath, localpath)
274
275    def put_file(self, localpath, remotepath):
276        ssh_client = self.get_ssh_client()
277        sftp_client = ssh_client.open_sftp()
278        sftp_client.put(localpath, remotepath)
279
280    def listdir(self, remotepath):
281        ssh_client = self.get_ssh_client()
282        sftp_client = ssh_client.open_sftp()
283        return sftp_client.listdir(remotepath)
284
285    def shell(self, key_file=None):
286        ssh_client = self.get_ssh_client(key_file)
287        channel = ssh_client.invoke_shell()
288        interactive_shell(channel)
289
290    def bundle_image(self, prefix, key_file, cert_file, size):
291        print 'bundling image...'
292        print '\tcopying cert and pk over to /mnt directory on server'
293        ssh_client = self.get_ssh_client()
294        sftp_client = ssh_client.open_sftp()
295        path, name = os.path.split(key_file)
296        remote_key_file = '/mnt/%s' % name
297        self.put_file(key_file, remote_key_file)
298        path, name = os.path.split(cert_file)
299        remote_cert_file = '/mnt/%s' % name
300        self.put_file(cert_file, remote_cert_file)
301        print '\tdeleting %s' % BotoConfigPath
302        # delete the metadata.ini file if it exists
303        try:
304            sftp_client.remove(BotoConfigPath)
305        except:
306            pass
307        command = 'sudo ec2-bundle-vol '
308        command += '-c %s -k %s ' % (remote_cert_file, remote_key_file)
309        command += '-u %s ' % self._reservation.owner_id
310        command += '-p %s ' % prefix
311        command += '-s %d ' % size
312        command += '-d /mnt '
313        if self.instance.instance_type == 'm1.small' or self.instance_type == 'c1.medium':
314            command += '-r i386'
315        else:
316            command += '-r x86_64'
317        print '\t%s' % command
318        t = ssh_client.exec_command(command)
319        response = t[1].read()
320        print '\t%s' % response
321        print '\t%s' % t[2].read()
322        print '...complete!'
323
324    def upload_bundle(self, bucket, prefix):
325        print 'uploading bundle...'
326        command = 'ec2-upload-bundle '
327        command += '-m /mnt/%s.manifest.xml ' % prefix
328        command += '-b %s ' % bucket
329        command += '-a %s ' % self.ec2.aws_access_key_id
330        command += '-s %s ' % self.ec2.aws_secret_access_key
331        print '\t%s' % command
332        ssh_client = self.get_ssh_client()
333        t = ssh_client.exec_command(command)
334        response = t[1].read()
335        print '\t%s' % response
336        print '\t%s' % t[2].read()
337        print '...complete!'
338
339    def create_image(self, bucket=None, prefix=None, key_file=None, cert_file=None, size=None):
340        iobject = IObject()
341        if not bucket:
342            bucket = iobject.get_string('Name of S3 bucket')
343        if not prefix:
344            prefix = iobject.get_string('Prefix for AMI file')
345        if not key_file:
346            key_file = iobject.get_filename('Path to RSA private key file')
347        if not cert_file:
348            cert_file = iobject.get_filename('Path to RSA public cert file')
349        if not size:
350            size = iobject.get_int('Size (in MB) of bundled image')
351        self.bundle_image(prefix, key_file, cert_file, size)
352        self.upload_bundle(bucket, prefix)
353        print 'registering image...'
354        self.image_id = self.ec2.register_image('%s/%s.manifest.xml' % (bucket, prefix))
355        return self.image_id
356
357    def attach_volume(self, volume, device="/dev/sdp"):
358        """
359        Attach an EBS volume to this server
360
361        :param volume: EBS Volume to attach
362        :type volume: boto.ec2.volume.Volume
363
364        :param device: Device to attach to (default to /dev/sdp)
365        :type device: string
366        """
367        if hasattr(volume, "id"):
368            volume_id = volume.id
369        else:
370            volume_id = volume
371        return self.ec2.attach_volume(volume_id=volume_id, instance_id=self.instance_id, device=device)
372
373    def detach_volume(self, volume):
374        """
375        Detach an EBS volume from this server
376
377        :param volume: EBS Volume to detach
378        :type volume: boto.ec2.volume.Volume
379        """
380        if hasattr(volume, "id"):
381            volume_id = volume.id
382        else:
383            volume_id = volume
384        return self.ec2.detach_volume(volume_id=volume_id, instance_id=self.instance_id)
385
386    def install_package(self, package_name):
387        print 'installing %s...' % package_name
388        command = 'yum -y install %s' % package_name
389        print '\t%s' % command
390        ssh_client = self.get_ssh_client()
391        t = ssh_client.exec_command(command)
392        response = t[1].read()
393        print '\t%s' % response
394        print '\t%s' % t[2].read()
395        print '...complete!'