1
# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
2
# Copyright (c) 2010 Chris Moyer http://coredumped.org/
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-
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
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
24
High-level abstraction of an EC2 server
26
from __future__ import with_statement
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
39
InstanceTypes = ['m1.small', 'm1.large', 'm1.xlarge',
40
'c1.medium', 'c1.xlarge',
41
'm2.2xlarge', 'm2.4xlarge']
43
class Bundler(object):
45
def __init__(self, server, uname='root'):
46
from boto.manage.cmdshell import SSHClient
49
self.ssh_client = SSHClient(server, uname=uname)
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)
62
def bundle_image(self, prefix, size, ssh_key):
64
if self.uname != 'root':
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
72
if self.server.instance_type == 'm1.small' or self.server.instance_type == 'c1.medium':
75
command += '-r x86_64'
78
def upload_bundle(self, bucket, prefix, ssh_key):
80
if self.uname != 'root':
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
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):
93
bucket = iobject.get_string('Name of S3 bucket')
95
prefix = iobject.get_string('Prefix for AMI file')
97
key_file = iobject.get_filename('Path to RSA private key file')
99
cert_file = iobject.get_filename('Path to RSA public cert file')
101
size = iobject.get_int('Size (in MB) of bundled image')
103
ssh_key = self.server.get_ssh_key_file()
104
self.copy_x509(key_file, cert_file)
106
fp = StringIO.StringIO()
107
fp.write('sudo mv %s /mnt/boto.cfg; ' % BotoConfigPath)
108
fp.write('mv ~/.ssh/authorized_keys /mnt/authorized_keys; ')
110
fp.write('history -c; ')
111
fp.write(self.bundle_image(prefix, size, ssh_key))
113
fp.write(self.upload_bundle(bucket, prefix, ssh_key))
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:'
120
t = self.ssh_client.run(command)
124
print 'registering image...'
125
self.image_id = self.server.ec2.register_image(name=prefix, image_location='%s/%s.manifest.xml' % (bucket, prefix))
128
class CommandLineGetter(object):
130
def get_ami_list(self):
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))
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
144
prop = self.cls.find_property('region_name')
145
params['region'] = propget.get(prop, choices=boto.ec2.regions)
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)
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)
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)
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)
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)
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():
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)
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()
194
prop = StringProperty(name='group', verbose_name='EC2 Security Group',
195
choices=self.ec2.get_all_security_groups)
196
params['group'] = propget.get(prop)
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()
203
if k.name == keypair:
205
params['keypair'] = k.name
207
prop = StringProperty(name='keypair', verbose_name='EC2 KeyPair',
208
choices=self.ec2.get_all_key_pairs)
209
params['keypair'] = propget.get(prop).name
211
def get(self, cls, params):
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)
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.)
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)
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)
265
Create a new instance based on the specified configuration file or the specified
266
configuration and the passed in parameters.
268
If the config_file argument is not None, the configuration is read from there.
269
Otherwise, the cfg argument is used.
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.
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.
279
The dictionary argument may be used to override any EC2 configuration values in the
283
def create(cls, config_file=None, logical_volume = None, cfg = None, **params):
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()
305
# deal with the possibility that zone and/or keypair are strings read from the config file:
306
if isinstance(zone, Zone):
308
if isinstance(kp, KeyPair):
310
reservation = ami.run(min_count=1,
311
max_count=params.get('quantity', 1),
313
security_groups=[group],
314
instance_type=params.get('instance_type'),
316
user_data = cfg_fp.getvalue())
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':
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:
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
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()
348
rs = ec2.get_all_instances([instance_id])
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
367
def create_from_current_instances(cls):
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:
376
Server.find(instance_id=instance.id).next()
377
boto.log.info('Server for %s already exists' % instance.id)
378
except StopIteration:
382
s.region_name = region.name
383
s.instance_id = instance.id
384
s._reservation = reservation
389
def __init__(self, id=None, **kw):
390
Model.__init__(self, id, **kw)
391
self.ssh_key_file = None
393
self._cmdshell = None
394
self._reservation = None
395
self._instance = None
398
def _setup_ec2(self):
399
if self.ec2 and self._instance and self._reservation:
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:
408
rs = self.ec2.get_all_instances([self.instance_id])
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:
420
self._instance.update()
421
status = self._instance.state
427
hostname = self._instance.public_dns_name
430
def _private_hostname(self):
433
hostname = self._instance.private_dns_name
436
def _instance_type(self):
439
it = self._instance.instance_type
442
def _launch_time(self):
445
lt = self._instance.launch_time
448
def _console_output(self):
451
co = self._instance.get_console_output()
456
if self._reservation:
457
gn = self._reservation.groups
460
def _security_group(self):
461
groups = self._groups()
469
zone = self._instance.placement
475
kn = self._instance.key_name
484
raise ValueError, "Can't delete a production server"
490
raise ValueError, "Can't delete a production server"
492
self._instance.stop()
496
raise ValueError, "Can't delete a production server"
498
self._instance.terminate()
502
self._instance.reboot()
505
while self.status != 'running':
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:
517
self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key file')
518
return self.ssh_key_file
520
def get_cmdshell(self):
521
if not self._cmdshell:
523
self.get_ssh_key_file()
524
self._cmdshell = cmdshell.start(self)
525
return self._cmdshell
527
def reset_cmdshell(self):
528
self._cmdshell = None
530
def run(self, command):
531
with closing(self.get_cmdshell()) as cmd:
532
status = cmd.run(command)
535
def get_bundler(self, uname='root'):
536
self.get_ssh_key_file()
537
return Bundler(self, uname)
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)
544
def install(self, pkg):
545
return self.run('apt-get -y install %s' % pkg)