# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, dis- # tribute, sublicense, and/or sell copies of the Software, and to permit # persons to whom the Software is furnished to do so, subject to the fol- # lowing conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. from __future__ import print_function from boto.sdb.db.model import Model from boto.sdb.db.property import StringProperty, IntegerProperty, ListProperty, ReferenceProperty, CalculatedProperty from boto.manage.server import Server from boto.manage import propget import boto.utils import boto.ec2 import time import traceback from contextlib import closing import datetime class CommandLineGetter(object): def get_region(self, params): if not params.get('region', None): prop = self.cls.find_property('region_name') params['region'] = propget.get(prop, choices=boto.ec2.regions) def get_zone(self, params): if not params.get('zone', None): prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone', choices=self.ec2.get_all_zones) params['zone'] = propget.get(prop) def get_name(self, params): if not params.get('name', None): prop = self.cls.find_property('name') params['name'] = propget.get(prop) def get_size(self, params): if not params.get('size', None): prop = IntegerProperty(name='size', verbose_name='Size (GB)') params['size'] = propget.get(prop) def get_mount_point(self, params): if not params.get('mount_point', None): prop = self.cls.find_property('mount_point') params['mount_point'] = propget.get(prop) def get_device(self, params): if not params.get('device', None): prop = self.cls.find_property('device') params['device'] = propget.get(prop) def get(self, cls, params): self.cls = cls self.get_region(params) self.ec2 = params['region'].connect() self.get_zone(params) self.get_name(params) self.get_size(params) self.get_mount_point(params) self.get_device(params) 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 is 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 is 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 = boto.utils.parse_ts(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 is 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 is 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 is 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 is 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 is 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 is 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) super(Volume, self).delete() def archive(self): # snapshot volume, trim snaps, delete volume-id pass