1
# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
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-
11
# The above copyright notice and this permission notice shall be included
12
# in all copies or substantial portions of the Software.
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
23
High-level abstraction of an EC2 server
27
from boto.mashups.iobject import IObject
28
from boto.pyami.config import Config, BotoConfigPath
29
from boto.mashups.interactive import interactive_shell
30
from boto.sdb.db.model import Model
31
from boto.sdb.db.property import StringProperty
36
class ServerSet(list):
38
def __getattr__(self, name):
43
val = getattr(server, name)
50
self.map_list = results
56
for fn in self.map_list:
57
results.append(fn(*args))
65
self._ec2 = boto.connect_ec2()
71
Returns a list of Server instances, one for each Server object
81
def Register(cls, name, instance_id, description=''):
84
s.instance_id = instance_id
85
s.description = description
89
def __init__(self, id=None, **kw):
90
Model.__init__(self, id, **kw)
91
self._reservation = None
93
self._ssh_client = None
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()
110
def setReadOnly(self, value):
113
def getInstance(self):
114
if not self._instance:
117
rs = self.ec2.get_all_instances([self.instance_id])
121
self._reservation = rs[0]
122
self._instance = self._reservation.instances[0]
123
return self._instance
125
instance = property(getInstance, setReadOnly, None, 'The Instance for the server')
129
return self.instance.image_id
131
ami = property(getAMI, setReadOnly, None, 'The AMI for the server')
135
self.instance.update()
136
return self.instance.state
138
status = property(getStatus, setReadOnly, None,
139
'The status of the server')
141
def getHostname(self):
143
return self.instance.public_dns_name
145
hostname = property(getHostname, setReadOnly, None,
146
'The public DNS name of the server')
148
def getPrivateHostname(self):
150
return self.instance.private_dns_name
152
private_hostname = property(getPrivateHostname, setReadOnly, None,
153
'The private DNS name of the server')
155
def getLaunchTime(self):
157
return self.instance.launch_time
159
launch_time = property(getLaunchTime, setReadOnly, None,
160
'The time the Server was started')
162
def getConsoleOutput(self):
164
return self.instance.get_console_output()
166
console_output = property(getConsoleOutput, setReadOnly, None,
167
'Retrieve the console output for server')
170
if self._reservation:
171
return self._reservation.groups
175
groups = property(getGroups, setReadOnly, None,
176
'The Security Groups controlling access to this server')
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)
186
def setConfig(self, config):
187
local_file = '%s.ini' % self.instance.id
188
fp = open(local_file)
191
self.put_file(local_file, BotoConfigPath)
192
self._config = config
194
config = property(getConfig, setConfig, None,
195
'The instance data for this server')
197
def set_config(self, config):
201
self._config = config
202
self._config.dump_to_sdb("botoConfigs", self.id)
204
def load_config(self):
205
self._config = Config(do_load=False)
206
self._config.load_from_sdb("botoConfigs", self.id)
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)])
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)
224
if not self._config.has_section("Pyami"):
225
self._config.add_section("Pyami")
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)
231
cfg = StringIO.StringIO()
232
self._config.write(cfg)
234
r = ami.run(min_count=1,
236
key_name=self.key_name,
237
security_groups = groups,
238
instance_type = self.instance_type,
239
placement = self.zone,
242
self.instance_id = i.id
245
ec2.associate_address(self.instance_id, self.elastic_ip)
249
self.instance.reboot()
251
def get_ssh_client(self, key_file=None, host_key_file='~/.ssh/known_hosts',
254
if not self.instance:
255
print 'No instance yet!'
257
if not self._ssh_client:
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
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)
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)
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)
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)
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
304
sftp_client.remove(BotoConfigPath)
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':
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()
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()
339
def create_image(self, bucket=None, prefix=None, key_file=None, cert_file=None, size=None):
342
bucket = iobject.get_string('Name of S3 bucket')
344
prefix = iobject.get_string('Prefix for AMI file')
346
key_file = iobject.get_filename('Path to RSA private key file')
348
cert_file = iobject.get_filename('Path to RSA public cert file')
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))
357
def attach_volume(self, volume, device="/dev/sdp"):
359
Attach an EBS volume to this server
361
:param volume: EBS Volume to attach
362
:type volume: boto.ec2.volume.Volume
364
:param device: Device to attach to (default to /dev/sdp)
367
if hasattr(volume, "id"):
368
volume_id = volume.id
371
return self.ec2.attach_volume(volume_id=volume_id, instance_id=self.instance_id, device=device)
373
def detach_volume(self, volume):
375
Detach an EBS volume from this server
377
:param volume: EBS Volume to detach
378
:type volume: boto.ec2.volume.Volume
380
if hasattr(volume, "id"):
381
volume_id = volume.id
384
return self.ec2.detach_volume(volume_id=volume_id, instance_id=self.instance_id)
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()