OLD | NEW |
(Empty) | |
| 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 """ |
| 23 High-level abstraction of an EC2 server |
| 24 """ |
| 25 import boto |
| 26 import boto.utils |
| 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 |
| 32 import os |
| 33 import StringIO |
| 34 |
| 35 |
| 36 class 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 |
| 60 class 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="defa
ult") |
| 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 se
rver') |
| 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_sec
ret_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 == 'c
1.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=No
ne, 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.inst
ance_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.inst
ance_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!' |
OLD | NEW |