~ntt-pf-lab/nova/monkey_patch_notification

« back to all changes in this revision

Viewing changes to vendor/boto/boto/manage/server.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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, BooleanProperty, 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_location='%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
 
 
147
    def get_name(self, params):
 
148
        if not params.get('name', None):
 
149
            prop = self.cls.find_property('name')
 
150
            params['name'] = propget.get(prop)
 
151
 
 
152
    def get_description(self, params):
 
153
        if not params.get('description', None):
 
154
            prop = self.cls.find_property('description')
 
155
            params['description'] = propget.get(prop)
 
156
 
 
157
    def get_instance_type(self, params):
 
158
        if not params.get('instance_type', None):
 
159
            prop = StringProperty(name='instance_type', verbose_name='Instance Type',
 
160
                                  choices=InstanceTypes)
 
161
            params['instance_type'] = propget.get(prop)
 
162
 
 
163
    def get_quantity(self, params):
 
164
        if not params.get('quantity', None):
 
165
            prop = IntegerProperty(name='quantity', verbose_name='Number of Instances')
 
166
            params['quantity'] = propget.get(prop)
 
167
 
 
168
    def get_zone(self, params):
 
169
        if not params.get('zone', None):
 
170
            prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone',
 
171
                                  choices=self.ec2.get_all_zones)
 
172
            params['zone'] = propget.get(prop)
 
173
            
 
174
    def get_ami_id(self, params):
 
175
        ami = params.get('ami', None)
 
176
        if isinstance(ami, str) or isinstance(ami, unicode):
 
177
            for a in self.ec2.get_all_images():
 
178
                if a.id == ami:
 
179
                    params['ami'] = a
 
180
        if not params.get('ami', None):
 
181
            prop = StringProperty(name='ami', verbose_name='AMI',
 
182
                                  choices=self.get_ami_list)
 
183
            params['ami'] = propget.get(prop)
 
184
 
 
185
    def get_group(self, params):
 
186
        group = params.get('group', None)
 
187
        if isinstance(group, str) or isinstance(group, unicode):
 
188
            group_list = self.ec2.get_all_security_groups()
 
189
            for g in group_list:
 
190
                if g.name == group:
 
191
                    group = g
 
192
                    params['group'] = g
 
193
        if not group:
 
194
            prop = StringProperty(name='group', verbose_name='EC2 Security Group',
 
195
                                  choices=self.ec2.get_all_security_groups)
 
196
            params['group'] = propget.get(prop)
 
197
 
 
198
    def get_key(self, params):
 
199
        keypair = params.get('keypair', None)
 
200
        if isinstance(keypair, str) or isinstance(keypair, unicode):
 
201
            key_list = self.ec2.get_all_key_pairs()
 
202
            for k in key_list:
 
203
                if k.name == keypair:
 
204
                    keypair = k.name
 
205
                    params['keypair'] = k.name
 
206
        if not keypair:
 
207
            prop = StringProperty(name='keypair', verbose_name='EC2 KeyPair',
 
208
                                  choices=self.ec2.get_all_key_pairs)
 
209
            params['keypair'] = propget.get(prop).name
 
210
 
 
211
    def get(self, cls, params):
 
212
        self.cls = cls
 
213
        self.get_region(params)
 
214
        self.ec2 = params['region'].connect()
 
215
        self.get_name(params)
 
216
        self.get_description(params)
 
217
        self.get_instance_type(params)
 
218
        self.get_zone(params)
 
219
        self.get_quantity(params)
 
220
        self.get_ami_id(params)
 
221
        self.get_group(params)
 
222
        self.get_key(params)
 
223
 
 
224
class Server(Model):
 
225
 
 
226
    #
 
227
    # The properties of this object consists of real properties for data that
 
228
    # is not already stored in EC2 somewhere (e.g. name, description) plus
 
229
    # calculated properties for all of the properties that are already in
 
230
    # EC2 (e.g. hostname, security groups, etc.)
 
231
    #
 
232
    name = StringProperty(unique=True, verbose_name="Name")
 
233
    description = StringProperty(verbose_name="Description")
 
234
    region_name = StringProperty(verbose_name="EC2 Region Name")
 
235
    instance_id = StringProperty(verbose_name="EC2 Instance ID")
 
236
    elastic_ip = StringProperty(verbose_name="EC2 Elastic IP Address")
 
237
    production = BooleanProperty(verbose_name="Is This Server Production", default=False)
 
238
    ami_id = CalculatedProperty(verbose_name="AMI ID", calculated_type=str, use_method=True)
 
239
    zone = CalculatedProperty(verbose_name="Availability Zone Name", calculated_type=str, use_method=True)
 
240
    hostname = CalculatedProperty(verbose_name="Public DNS Name", calculated_type=str, use_method=True)
 
241
    private_hostname = CalculatedProperty(verbose_name="Private DNS Name", calculated_type=str, use_method=True)
 
242
    groups = CalculatedProperty(verbose_name="Security Groups", calculated_type=list, use_method=True)
 
243
    security_group = CalculatedProperty(verbose_name="Primary Security Group Name", calculated_type=str, use_method=True)
 
244
    key_name = CalculatedProperty(verbose_name="Key Name", calculated_type=str, use_method=True)
 
245
    instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True)
 
246
    status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True)
 
247
    launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True)
 
248
    console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=file, use_method=True)
 
249
 
 
250
    packages = []
 
251
    plugins = []
 
252
 
 
253
    @classmethod
 
254
    def add_credentials(cls, cfg, aws_access_key_id, aws_secret_access_key):
 
255
        if not cfg.has_section('Credentials'):
 
256
            cfg.add_section('Credentials')
 
257
        cfg.set('Credentials', 'aws_access_key_id', aws_access_key_id)
 
258
        cfg.set('Credentials', 'aws_secret_access_key', aws_secret_access_key)
 
259
        if not cfg.has_section('DB_Server'):
 
260
            cfg.add_section('DB_Server')
 
261
        cfg.set('DB_Server', 'db_type', 'SimpleDB')
 
262
        cfg.set('DB_Server', 'db_name', cls._manager.domain.name)
 
263
 
 
264
    '''
 
265
    Create a new instance based on the specified configuration file or the specified
 
266
    configuration and the passed in parameters.
 
267
    
 
268
    If the config_file argument is not None, the configuration is read from there. 
 
269
    Otherwise, the cfg argument is used.
 
270
 
 
271
    The config file may include other config files with a #import reference. The included
 
272
    config files must reside in the same directory as the specified file. 
 
273
    
 
274
    The logical_volume argument, if supplied, will be used to get the current physical 
 
275
    volume ID and use that as an override of the value specified in the config file. This 
 
276
    may be useful for debugging purposes when you want to debug with a production config 
 
277
    file but a test Volume. 
 
278
    
 
279
    The dictionary argument may be used to override any EC2 configuration values in the 
 
280
    config file. 
 
281
    '''
 
282
    @classmethod
 
283
    def create(cls, config_file=None, logical_volume = None, cfg = None, **params):
 
284
        if config_file:
 
285
            cfg = Config(path=config_file)
 
286
        if cfg.has_section('EC2'):
 
287
            # include any EC2 configuration values that aren't specified in params:
 
288
            for option in cfg.options('EC2'):
 
289
                if option not in params:
 
290
                    params[option] = cfg.get('EC2', option)
 
291
        getter = CommandLineGetter()
 
292
        getter.get(cls, params)
 
293
        region = params.get('region')
 
294
        ec2 = region.connect()
 
295
        cls.add_credentials(cfg, ec2.aws_access_key_id, ec2.aws_secret_access_key)
 
296
        ami = params.get('ami')
 
297
        kp = params.get('keypair')
 
298
        group = params.get('group')
 
299
        zone = params.get('zone')
 
300
        # deal with possibly passed in logical volume:
 
301
        if logical_volume != None:
 
302
           cfg.set('EBS', 'logical_volume_name', logical_volume.name) 
 
303
        cfg_fp = StringIO.StringIO()
 
304
        cfg.write(cfg_fp)
 
305
        # deal with the possibility that zone and/or keypair are strings read from the config file:
 
306
        if isinstance(zone, Zone):
 
307
            zone = zone.name
 
308
        if isinstance(kp, KeyPair):
 
309
            kp = kp.name
 
310
        reservation = ami.run(min_count=1,
 
311
                              max_count=params.get('quantity', 1),
 
312
                              key_name=kp,
 
313
                              security_groups=[group],
 
314
                              instance_type=params.get('instance_type'),
 
315
                              placement = zone,
 
316
                              user_data = cfg_fp.getvalue())
 
317
        l = []
 
318
        i = 0
 
319
        elastic_ip = params.get('elastic_ip')
 
320
        instances = reservation.instances
 
321
        if elastic_ip != None and instances.__len__() > 0:
 
322
            instance = instances[0]
 
323
            print 'Waiting for instance to start so we can set its elastic IP address...'
 
324
            while instance.update() != 'running':
 
325
                time.sleep(1)
 
326
            instance.use_ip(elastic_ip)
 
327
            print 'set the elastic IP of the first instance to %s' % elastic_ip
 
328
        for instance in instances:
 
329
            s = cls()
 
330
            s.ec2 = ec2
 
331
            s.name = params.get('name') + '' if i==0 else str(i)
 
332
            s.description = params.get('description')
 
333
            s.region_name = region.name
 
334
            s.instance_id = instance.id
 
335
            if elastic_ip and i == 0:
 
336
                s.elastic_ip = elastic_ip
 
337
            s.put()
 
338
            l.append(s)
 
339
            i += 1
 
340
        return l
 
341
    
 
342
    @classmethod
 
343
    def create_from_instance_id(cls, instance_id, name, description=''):
 
344
        regions = boto.ec2.regions()
 
345
        for region in regions:
 
346
            ec2 = region.connect()
 
347
            try:
 
348
                rs = ec2.get_all_instances([instance_id])
 
349
            except:
 
350
                rs = []
 
351
            if len(rs) == 1:
 
352
                s = cls()
 
353
                s.ec2 = ec2
 
354
                s.name = name
 
355
                s.description = description
 
356
                s.region_name = region.name
 
357
                s.instance_id = instance_id
 
358
                s._reservation = rs[0]
 
359
                for instance in s._reservation.instances:
 
360
                    if instance.id == instance_id:
 
361
                        s._instance = instance
 
362
                s.put()
 
363
                return s
 
364
        return None
 
365
 
 
366
    @classmethod
 
367
    def create_from_current_instances(cls):
 
368
        servers = []
 
369
        regions = boto.ec2.regions()
 
370
        for region in regions:
 
371
            ec2 = region.connect()
 
372
            rs = ec2.get_all_instances()
 
373
            for reservation in rs:
 
374
                for instance in reservation.instances:
 
375
                    try:
 
376
                        Server.find(instance_id=instance.id).next()
 
377
                        boto.log.info('Server for %s already exists' % instance.id)
 
378
                    except StopIteration:
 
379
                        s = cls()
 
380
                        s.ec2 = ec2
 
381
                        s.name = instance.id
 
382
                        s.region_name = region.name
 
383
                        s.instance_id = instance.id
 
384
                        s._reservation = reservation
 
385
                        s.put()
 
386
                        servers.append(s)
 
387
        return servers
 
388
    
 
389
    def __init__(self, id=None, **kw):
 
390
        Model.__init__(self, id, **kw)
 
391
        self.ssh_key_file = None
 
392
        self.ec2 = None
 
393
        self._cmdshell = None
 
394
        self._reservation = None
 
395
        self._instance = None
 
396
        self._setup_ec2()
 
397
 
 
398
    def _setup_ec2(self):
 
399
        if self.ec2 and self._instance and self._reservation:
 
400
            return
 
401
        if self.id:
 
402
            if self.region_name:
 
403
                for region in boto.ec2.regions():
 
404
                    if region.name == self.region_name:
 
405
                        self.ec2 = region.connect()
 
406
                        if self.instance_id and not self._instance:
 
407
                            try:
 
408
                                rs = self.ec2.get_all_instances([self.instance_id])
 
409
                                if len(rs) >= 1:
 
410
                                    for instance in rs[0].instances:
 
411
                                        if instance.id == self.instance_id:
 
412
                                            self._reservation = rs[0]
 
413
                                            self._instance = instance
 
414
                            except EC2ResponseError:
 
415
                                pass
 
416
                            
 
417
    def _status(self):
 
418
        status = ''
 
419
        if self._instance:
 
420
            self._instance.update()
 
421
            status = self._instance.state
 
422
        return status
 
423
 
 
424
    def _hostname(self):
 
425
        hostname = ''
 
426
        if self._instance:
 
427
            hostname = self._instance.public_dns_name
 
428
        return hostname
 
429
 
 
430
    def _private_hostname(self):
 
431
        hostname = ''
 
432
        if self._instance:
 
433
            hostname = self._instance.private_dns_name
 
434
        return hostname
 
435
 
 
436
    def _instance_type(self):
 
437
        it = ''
 
438
        if self._instance:
 
439
            it = self._instance.instance_type
 
440
        return it
 
441
 
 
442
    def _launch_time(self):
 
443
        lt = ''
 
444
        if self._instance:
 
445
            lt = self._instance.launch_time
 
446
        return lt
 
447
 
 
448
    def _console_output(self):
 
449
        co = ''
 
450
        if self._instance:
 
451
            co = self._instance.get_console_output()
 
452
        return co
 
453
 
 
454
    def _groups(self):
 
455
        gn = []
 
456
        if self._reservation:
 
457
            gn = self._reservation.groups
 
458
        return gn
 
459
 
 
460
    def _security_group(self):
 
461
        groups = self._groups()
 
462
        if len(groups) >= 1:
 
463
            return groups[0].id
 
464
        return ""
 
465
 
 
466
    def _zone(self):
 
467
        zone = None
 
468
        if self._instance:
 
469
            zone = self._instance.placement
 
470
        return zone
 
471
 
 
472
    def _key_name(self):
 
473
        kn = None
 
474
        if self._instance:
 
475
            kn = self._instance.key_name
 
476
        return kn
 
477
 
 
478
    def put(self):
 
479
        Model.put(self)
 
480
        self._setup_ec2()
 
481
 
 
482
    def delete(self):
 
483
        if self.production:
 
484
            raise ValueError, "Can't delete a production server"
 
485
        #self.stop()
 
486
        Model.delete(self)
 
487
 
 
488
    def stop(self):
 
489
        if self.production:
 
490
            raise ValueError, "Can't delete a production server"
 
491
        if self._instance:
 
492
            self._instance.stop()
 
493
 
 
494
    def terminate(self):
 
495
        if self.production:
 
496
            raise ValueError, "Can't delete a production server"
 
497
        if self._instance:
 
498
            self._instance.terminate()
 
499
 
 
500
    def reboot(self):
 
501
        if self._instance:
 
502
            self._instance.reboot()
 
503
 
 
504
    def wait(self):
 
505
        while self.status != 'running':
 
506
            time.sleep(5)
 
507
 
 
508
    def get_ssh_key_file(self):
 
509
        if not self.ssh_key_file:
 
510
            ssh_dir = os.path.expanduser('~/.ssh')
 
511
            if os.path.isdir(ssh_dir):
 
512
                ssh_file = os.path.join(ssh_dir, '%s.pem' % self.key_name)
 
513
                if os.path.isfile(ssh_file):
 
514
                    self.ssh_key_file = ssh_file
 
515
            if not self.ssh_key_file:
 
516
                iobject = IObject()
 
517
                self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key file')
 
518
        return self.ssh_key_file
 
519
 
 
520
    def get_cmdshell(self):
 
521
        if not self._cmdshell:
 
522
            import cmdshell
 
523
            self.get_ssh_key_file()
 
524
            self._cmdshell = cmdshell.start(self)
 
525
        return self._cmdshell
 
526
 
 
527
    def reset_cmdshell(self):
 
528
        self._cmdshell = None
 
529
 
 
530
    def run(self, command):
 
531
        with closing(self.get_cmdshell()) as cmd:
 
532
            status = cmd.run(command)
 
533
        return status
 
534
 
 
535
    def get_bundler(self, uname='root'):
 
536
        self.get_ssh_key_file()
 
537
        return Bundler(self, uname)
 
538
 
 
539
    def get_ssh_client(self, uname='root'):
 
540
        from boto.manage.cmdshell import SSHClient
 
541
        self.get_ssh_key_file()
 
542
        return SSHClient(self, uname=uname)
 
543
 
 
544
    def install(self, pkg):
 
545
        return self.run('apt-get -y install %s' % pkg)
 
546
 
 
547
 
 
548