class Server(Model): # # The properties of this object consists of real properties for data that # is not already stored in EC2 somewhere (e.g. name, description) plus # calculated properties for all of the properties that are already in # EC2 (e.g. hostname, security groups, etc.) # name = StringProperty(unique=True, verbose_name="Name") description = StringProperty(verbose_name="Description") region_name = StringProperty(verbose_name="EC2 Region Name") instance_id = StringProperty(verbose_name="EC2 Instance ID") elastic_ip = StringProperty(verbose_name="EC2 Elastic IP Address") production = BooleanProperty(verbose_name="Is This Server Production", default=False) ami_id = CalculatedProperty(verbose_name="AMI ID", calculated_type=str, use_method=True) zone = CalculatedProperty(verbose_name="Availability Zone Name", calculated_type=str, use_method=True) hostname = CalculatedProperty(verbose_name="Public DNS Name", calculated_type=str, use_method=True) private_hostname = CalculatedProperty(verbose_name="Private DNS Name", calculated_type=str, use_method=True) groups = CalculatedProperty(verbose_name="Security Groups", calculated_type=list, use_method=True) security_group = CalculatedProperty(verbose_name="Primary Security Group Name", calculated_type=str, use_method=True) key_name = CalculatedProperty(verbose_name="Key Name", calculated_type=str, use_method=True) instance_type = CalculatedProperty(verbose_name="Instance Type", calculated_type=str, use_method=True) status = CalculatedProperty(verbose_name="Current Status", calculated_type=str, use_method=True) launch_time = CalculatedProperty(verbose_name="Server Launch Time", calculated_type=str, use_method=True) console_output = CalculatedProperty(verbose_name="Console Output", calculated_type=file, use_method=True) packages = [] plugins = [] @classmethod def add_credentials(cls, cfg, aws_access_key_id, aws_secret_access_key): if not cfg.has_section('Credentials'): cfg.add_section('Credentials') cfg.set('Credentials', 'aws_access_key_id', aws_access_key_id) cfg.set('Credentials', 'aws_secret_access_key', aws_secret_access_key) if not cfg.has_section('DB_Server'): cfg.add_section('DB_Server') cfg.set('DB_Server', 'db_type', 'SimpleDB') cfg.set('DB_Server', 'db_name', cls._manager.domain.name) @classmethod def create(cls, config_file=None, logical_volume = None, cfg = None, **params): """ Create a new instance based on the specified configuration file or the specified configuration and the passed in parameters. If the config_file argument is not None, the configuration is read from there. Otherwise, the cfg argument is used. The config file may include other config files with a #import reference. The included config files must reside in the same directory as the specified file. The logical_volume argument, if supplied, will be used to get the current physical volume ID and use that as an override of the value specified in the config file. This may be useful for debugging purposes when you want to debug with a production config file but a test Volume. The dictionary argument may be used to override any EC2 configuration values in the config file. """ if config_file: cfg = Config(path=config_file) if cfg.has_section('EC2'): # include any EC2 configuration values that aren't specified in params: for option in cfg.options('EC2'): if option not in params: params[option] = cfg.get('EC2', option) getter = CommandLineGetter() getter.get(cls, params) region = params.get('region') ec2 = region.connect() cls.add_credentials(cfg, ec2.aws_access_key_id, ec2.aws_secret_access_key) ami = params.get('ami') kp = params.get('keypair') group = params.get('group') zone = params.get('zone') # deal with possibly passed in logical volume: if logical_volume != None: cfg.set('EBS', 'logical_volume_name', logical_volume.name) cfg_fp = StringIO() cfg.write(cfg_fp) # deal with the possibility that zone and/or keypair are strings read from the config file: if isinstance(zone, Zone): zone = zone.name if isinstance(kp, KeyPair): kp = kp.name reservation = ami.run(min_count=1, max_count=params.get('quantity', 1), key_name=kp, security_groups=[group], instance_type=params.get('instance_type'), placement = zone, user_data = cfg_fp.getvalue()) l = [] i = 0 elastic_ip = params.get('elastic_ip') instances = reservation.instances if elastic_ip is not None and instances.__len__() > 0: instance = instances[0] print('Waiting for instance to start so we can set its elastic IP address...') # Sometimes we get a message from ec2 that says that the instance does not exist. # Hopefully the following delay will giv eec2 enough time to get to a stable state: time.sleep(5) while instance.update() != 'running': time.sleep(1) instance.use_ip(elastic_ip) print('set the elastic IP of the first instance to %s' % elastic_ip) for instance in instances: s = cls() s.ec2 = ec2 s.name = params.get('name') + '' if i==0 else str(i) s.description = params.get('description') s.region_name = region.name s.instance_id = instance.id if elastic_ip and i == 0: s.elastic_ip = elastic_ip s.put() l.append(s) i += 1 return l @classmethod def create_from_instance_id(cls, instance_id, name, description=''): regions = boto.ec2.regions() for region in regions: ec2 = region.connect() try: rs = ec2.get_all_reservations([instance_id]) except: rs = [] if len(rs) == 1: s = cls() s.ec2 = ec2 s.name = name s.description = description s.region_name = region.name s.instance_id = instance_id s._reservation = rs[0] for instance in s._reservation.instances: if instance.id == instance_id: s._instance = instance s.put() return s return None @classmethod def create_from_current_instances(cls): servers = [] regions = boto.ec2.regions() for region in regions: ec2 = region.connect() rs = ec2.get_all_reservations() for reservation in rs: for instance in reservation.instances: try: next(Server.find(instance_id=instance.id)) boto.log.info('Server for %s already exists' % instance.id) except StopIteration: s = cls() s.ec2 = ec2 s.name = instance.id s.region_name = region.name s.instance_id = instance.id s._reservation = reservation s.put() servers.append(s) return servers def __init__(self, id=None, **kw): super(Server, self).__init__(id, **kw) self.ssh_key_file = None self.ec2 = None self._cmdshell = None self._reservation = None self._instance = None self._setup_ec2() def _setup_ec2(self): if self.ec2 and self._instance and self._reservation: return if self.id: if self.region_name: for region in boto.ec2.regions(): if region.name == self.region_name: self.ec2 = region.connect() if self.instance_id and not self._instance: try: rs = self.ec2.get_all_reservations([self.instance_id]) if len(rs) >= 1: for instance in rs[0].instances: if instance.id == self.instance_id: self._reservation = rs[0] self._instance = instance except EC2ResponseError: pass def _status(self): status = '' if self._instance: self._instance.update() status = self._instance.state return status def _hostname(self): hostname = '' if self._instance: hostname = self._instance.public_dns_name return hostname def _private_hostname(self): hostname = '' if self._instance: hostname = self._instance.private_dns_name return hostname def _instance_type(self): it = '' if self._instance: it = self._instance.instance_type return it def _launch_time(self): lt = '' if self._instance: lt = self._instance.launch_time return lt def _console_output(self): co = '' if self._instance: co = self._instance.get_console_output() return co def _groups(self): gn = [] if self._reservation: gn = self._reservation.groups return gn def _security_group(self): groups = self._groups() if len(groups) >= 1: return groups[0].id return "" def _zone(self): zone = None if self._instance: zone = self._instance.placement return zone def _key_name(self): kn = None if self._instance: kn = self._instance.key_name return kn def put(self): super(Server, self).put() self._setup_ec2() def delete(self): if self.production: raise ValueError("Can't delete a production server") #self.stop() super(Server, self).delete() def stop(self): if self.production: raise ValueError("Can't delete a production server") if self._instance: self._instance.stop() def terminate(self): if self.production: raise ValueError("Can't delete a production server") if self._instance: self._instance.terminate() def reboot(self): if self._instance: self._instance.reboot() def wait(self): while self.status != 'running': time.sleep(5) def get_ssh_key_file(self): if not self.ssh_key_file: ssh_dir = os.path.expanduser('~/.ssh') if os.path.isdir(ssh_dir): ssh_file = os.path.join(ssh_dir, '%s.pem' % self.key_name) if os.path.isfile(ssh_file): self.ssh_key_file = ssh_file if not self.ssh_key_file: iobject = IObject() self.ssh_key_file = iobject.get_filename('Path to OpenSSH Key file') return self.ssh_key_file def get_cmdshell(self): if not self._cmdshell: from boto.manage import cmdshell self.get_ssh_key_file() self._cmdshell = cmdshell.start(self) return self._cmdshell def reset_cmdshell(self): self._cmdshell = None def run(self, command): with closing(self.get_cmdshell()) as cmd: status = cmd.run(command) return status def get_bundler(self, uname='root'): self.get_ssh_key_file() return Bundler(self, uname) def get_ssh_client(self, uname='root', ssh_pwd=None): from boto.manage.cmdshell import SSHClient self.get_ssh_key_file() return SSHClient(self, uname=uname, ssh_pwd=ssh_pwd) def install(self, pkg): return self.run('apt-get -y install %s' % pkg)
class Volume(Model): name = StringProperty(required=True, unique=True, verbose_name='Name') region_name = StringProperty(required=True, verbose_name='EC2 Region') zone_name = StringProperty(required=True, verbose_name='EC2 Zone') mount_point = StringProperty(verbose_name='Mount Point') device = StringProperty(verbose_name="Device Name", default='/dev/sdp') volume_id = StringProperty(required=True) past_volume_ids = ListProperty(item_type=str) server = ReferenceProperty(Server, collection_name='volumes', verbose_name='Server Attached To') volume_state = CalculatedProperty(verbose_name="Volume State", calculated_type=str, use_method=True) attachment_state = CalculatedProperty(verbose_name="Attachment State", calculated_type=str, use_method=True) size = CalculatedProperty(verbose_name="Size (GB)", calculated_type=int, use_method=True) @classmethod def create(cls, **params): getter = CommandLineGetter() getter.get(cls, params) region = params.get('region') ec2 = region.connect() zone = params.get('zone') size = params.get('size') ebs_volume = ec2.create_volume(size, zone.name) v = cls() v.ec2 = ec2 v.volume_id = ebs_volume.id v.name = params.get('name') v.mount_point = params.get('mount_point') v.device = params.get('device') v.region_name = region.name v.zone_name = zone.name v.put() return v @classmethod def create_from_volume_id(cls, region_name, volume_id, name): vol = None ec2 = boto.ec2.connect_to_region(region_name) rs = ec2.get_all_volumes([volume_id]) if len(rs) == 1: v = rs[0] vol = cls() vol.volume_id = v.id vol.name = name vol.region_name = v.region.name vol.zone_name = v.zone vol.put() return vol def create_from_latest_snapshot(self, name, size=None): snapshot = self.get_snapshots()[-1] return self.create_from_snapshot(name, snapshot, size) def create_from_snapshot(self, name, snapshot, size=None): if size < self.size: size = self.size ec2 = self.get_ec2_connection() if self.zone_name == None or self.zone_name == '': # deal with the migration case where the zone is not set in the logical volume: current_volume = ec2.get_all_volumes([self.volume_id])[0] self.zone_name = current_volume.zone ebs_volume = ec2.create_volume(size, self.zone_name, snapshot) v = Volume() v.ec2 = self.ec2 v.volume_id = ebs_volume.id v.name = name v.mount_point = self.mount_point v.device = self.device v.region_name = self.region_name v.zone_name = self.zone_name v.put() return v def get_ec2_connection(self): if self.server: return self.server.ec2 if not hasattr(self, 'ec2') or self.ec2 == None: self.ec2 = boto.ec2.connect_to_region(self.region_name) return self.ec2 def _volume_state(self): ec2 = self.get_ec2_connection() rs = ec2.get_all_volumes([self.volume_id]) return rs[0].volume_state() def _attachment_state(self): ec2 = self.get_ec2_connection() rs = ec2.get_all_volumes([self.volume_id]) return rs[0].attachment_state() def _size(self): if not hasattr(self, '__size'): ec2 = self.get_ec2_connection() rs = ec2.get_all_volumes([self.volume_id]) self.__size = rs[0].size return self.__size def install_xfs(self): if self.server: self.server.install('xfsprogs xfsdump') def get_snapshots(self): """ Returns a list of all completed snapshots for this volume ID. """ ec2 = self.get_ec2_connection() rs = ec2.get_all_snapshots() all_vols = [self.volume_id] + self.past_volume_ids snaps = [] for snapshot in rs: if snapshot.volume_id in all_vols: if snapshot.progress == '100%': snapshot.date = dateutil.parser.parse(snapshot.start_time) snapshot.keep = True snaps.append(snapshot) snaps.sort(cmp=lambda x, y: cmp(x.date, y.date)) return snaps def attach(self, server=None): if self.attachment_state == 'attached': print 'already attached' return None if server: self.server = server self.put() ec2 = self.get_ec2_connection() ec2.attach_volume(self.volume_id, self.server.instance_id, self.device) def detach(self, force=False): state = self.attachment_state if state == 'available' or state == None or state == 'detaching': print 'already detached' return None ec2 = self.get_ec2_connection() ec2.detach_volume(self.volume_id, self.server.instance_id, self.device, force) self.server = None self.put() def checkfs(self, use_cmd=None): if self.server == None: raise ValueError, 'server attribute must be set to run this command' # detemine state of file system on volume, only works if attached if use_cmd: cmd = use_cmd else: cmd = self.server.get_cmdshell() status = cmd.run('xfs_check %s' % self.device) if not use_cmd: cmd.close() if status[1].startswith('bad superblock magic number 0'): return False return True def wait(self): if self.server == None: raise ValueError, 'server attribute must be set to run this command' with closing(self.server.get_cmdshell()) as cmd: # wait for the volume device to appear cmd = self.server.get_cmdshell() while not cmd.exists(self.device): boto.log.info('%s still does not exist, waiting 10 seconds' % self.device) time.sleep(10) def format(self): if self.server == None: raise ValueError, 'server attribute must be set to run this command' status = None with closing(self.server.get_cmdshell()) as cmd: if not self.checkfs(cmd): boto.log.info('make_fs...') status = cmd.run('mkfs -t xfs %s' % self.device) return status def mount(self): if self.server == None: raise ValueError, 'server attribute must be set to run this command' boto.log.info('handle_mount_point') with closing(self.server.get_cmdshell()) as cmd: cmd = self.server.get_cmdshell() if not cmd.isdir(self.mount_point): boto.log.info('making directory') # mount directory doesn't exist so create it cmd.run("mkdir %s" % self.mount_point) else: boto.log.info('directory exists already') status = cmd.run('mount -l') lines = status[1].split('\n') for line in lines: t = line.split() if t and t[2] == self.mount_point: # something is already mounted at the mount point # unmount that and mount it as /tmp if t[0] != self.device: cmd.run('umount %s' % self.mount_point) cmd.run('mount %s /tmp' % t[0]) cmd.run('chmod 777 /tmp') break # Mount up our new EBS volume onto mount_point cmd.run("mount %s %s" % (self.device, self.mount_point)) cmd.run('xfs_growfs %s' % self.mount_point) def make_ready(self, server): self.server = server self.put() self.install_xfs() self.attach() self.wait() self.format() self.mount() def freeze(self): if self.server: return self.server.run("/usr/sbin/xfs_freeze -f %s" % self.mount_point) def unfreeze(self): if self.server: return self.server.run("/usr/sbin/xfs_freeze -u %s" % self.mount_point) def snapshot(self): # if this volume is attached to a server # we need to freeze the XFS file system try: self.freeze() if self.server == None: snapshot = self.get_ec2_connection().create_snapshot( self.volume_id) else: snapshot = self.server.ec2.create_snapshot(self.volume_id) boto.log.info('Snapshot of Volume %s created: %s' % (self.name, snapshot)) except Exception: boto.log.info('Snapshot error') boto.log.info(traceback.format_exc()) finally: status = self.unfreeze() return status def get_snapshot_range(self, snaps, start_date=None, end_date=None): l = [] for snap in snaps: if start_date and end_date: if snap.date >= start_date and snap.date <= end_date: l.append(snap) elif start_date: if snap.date >= start_date: l.append(snap) elif end_date: if snap.date <= end_date: l.append(snap) else: l.append(snap) return l def trim_snapshots(self, delete=False): """ Trim the number of snapshots for this volume. This method always keeps the oldest snapshot. It then uses the parameters passed in to determine how many others should be kept. The algorithm is to keep all snapshots from the current day. Then it will keep the first snapshot of the day for the previous seven days. Then, it will keep the first snapshot of the week for the previous four weeks. After than, it will keep the first snapshot of the month for as many months as there are. """ snaps = self.get_snapshots() # Always keep the oldest and the newest if len(snaps) <= 2: return snaps snaps = snaps[1:-1] now = datetime.datetime.now(snaps[0].date.tzinfo) midnight = datetime.datetime(year=now.year, month=now.month, day=now.day, tzinfo=now.tzinfo) # Keep the first snapshot from each day of the previous week one_week = datetime.timedelta(days=7, seconds=60 * 60) print midnight - one_week, midnight previous_week = self.get_snapshot_range(snaps, midnight - one_week, midnight) print previous_week if not previous_week: return snaps current_day = None for snap in previous_week: if current_day and current_day == snap.date.day: snap.keep = False else: current_day = snap.date.day # Get ourselves onto the next full week boundary if previous_week: week_boundary = previous_week[0].date if week_boundary.weekday() != 0: delta = datetime.timedelta(days=week_boundary.weekday()) week_boundary = week_boundary - delta # Keep one within this partial week partial_week = self.get_snapshot_range(snaps, week_boundary, previous_week[0].date) if len(partial_week) > 1: for snap in partial_week[1:]: snap.keep = False # Keep the first snapshot of each week for the previous 4 weeks for i in range(0, 4): weeks_worth = self.get_snapshot_range(snaps, week_boundary - one_week, week_boundary) if len(weeks_worth) > 1: for snap in weeks_worth[1:]: snap.keep = False week_boundary = week_boundary - one_week # Now look through all remaining snaps and keep one per month remainder = self.get_snapshot_range(snaps, end_date=week_boundary) current_month = None for snap in remainder: if current_month and current_month == snap.date.month: snap.keep = False else: current_month = snap.date.month if delete: for snap in snaps: if not snap.keep: boto.log.info('Deleting %s(%s) for %s' % (snap, snap.date, self.name)) snap.delete() return snaps def grow(self, size): pass def copy(self, snapshot): pass def get_snapshot_from_date(self, date): pass def delete(self, delete_ebs_volume=False): if delete_ebs_volume: self.detach() ec2 = self.get_ec2_connection() ec2.delete_volume(self.volume_id) Model.delete(self) def archive(self): # snapshot volume, trim snaps, delete volume-id pass