1
# Copyright (c) 2006-2009 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
22
from __future__ import with_statement
23
from boto.sdb.db.model import Model
24
from boto.sdb.db.property import *
25
from boto.manage.server import Server
26
from boto.manage import propget
28
import time, traceback
29
from contextlib import closing
30
import dateutil.parser
32
class CommandLineGetter(object):
34
def get_region(self, params):
35
if not params.get('region', None):
36
prop = self.cls.find_property('region_name')
37
params['region'] = propget.get(prop, choices=boto.ec2.regions)
39
def get_zone(self, params):
40
if not params.get('zone', None):
41
prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone',
42
choices=self.ec2.get_all_zones)
43
params['zone'] = propget.get(prop)
45
def get_name(self, params):
46
if not params.get('name', None):
47
prop = self.cls.find_property('name')
48
params['name'] = propget.get(prop)
50
def get_size(self, params):
51
if not params.get('size', None):
52
prop = IntegerProperty(name='size', verbose_name='Size (GB)')
53
params['size'] = propget.get(prop)
55
def get_mount_point(self, params):
56
if not params.get('mount_point', None):
57
prop = self.cls.find_property('mount_point')
58
params['mount_point'] = propget.get(prop)
60
def get_device(self, params):
61
if not params.get('device', None):
62
prop = self.cls.find_property('device')
63
params['device'] = propget.get(prop)
65
def get(self, cls, params):
67
self.get_region(params)
68
self.ec2 = params['region'].connect()
72
self.get_mount_point(params)
73
self.get_device(params)
77
name = StringProperty(required=True, unique=True, verbose_name='Name')
78
region_name = StringProperty(required=True, verbose_name='EC2 Region')
79
mount_point = StringProperty(verbose_name='Mount Point')
80
device = StringProperty(verbose_name="Device Name", default='/dev/sdp')
81
volume_id = StringProperty(required=True)
82
past_volume_ids = ListProperty(item_type=str)
83
server = ReferenceProperty(Server, collection_name='volumes',
84
verbose_name='Server Attached To')
85
volume_state = CalculatedProperty(verbose_name="Volume State",
86
calculated_type=str, use_method=True)
87
attachment_state = CalculatedProperty(verbose_name="Attachment State",
88
calculated_type=str, use_method=True)
89
size = CalculatedProperty(verbose_name="Size (GB)",
90
calculated_type=int, use_method=True)
93
def create(cls, **params):
94
getter = CommandLineGetter()
95
getter.get(cls, params)
96
region = params.get('region')
97
ec2 = region.connect()
98
zone = params.get('zone')
99
size = params.get('size')
100
ebs_volume = ec2.create_volume(size, zone.name)
103
v.volume_id = ebs_volume.id
104
v.name = params.get('name')
105
v.mount_point = params.get('mount_point')
106
v.device = params.get('device')
107
v.region_name = region.name
112
def create_from_volume_id(cls, region_name, volume_id, name):
114
ec2 = boto.ec2.connect_to_region(region_name)
115
rs = ec2.get_all_volumes([volume_id])
121
vol.region_name = v.region.name
125
def get_ec2_connection(self):
127
return self.server.ec2
128
if not hasattr(self, 'ec2') or self.ec2 == None:
129
self.ec2 = boto.ec2.connect_to_region(self.region_name)
132
def _volume_state(self):
133
ec2 = self.get_ec2_connection()
134
rs = ec2.get_all_volumes([self.volume_id])
135
return rs[0].volume_state()
137
def _attachment_state(self):
138
ec2 = self.get_ec2_connection()
139
rs = ec2.get_all_volumes([self.volume_id])
140
return rs[0].attachment_state()
143
if not hasattr(self, '__size'):
144
ec2 = self.get_ec2_connection()
145
rs = ec2.get_all_volumes([self.volume_id])
146
self.__size = rs[0].size
149
def install_xfs(self):
151
self.server.install('xfsprogs xfsdump')
153
def get_snapshots(self):
155
Returns a list of all completed snapshots for this volume ID.
157
ec2 = self.get_ec2_connection()
158
rs = ec2.get_all_snapshots()
159
all_vols = [self.volume_id] + self.past_volume_ids
162
if snapshot.volume_id in all_vols:
163
if snapshot.progress == '100%':
164
snapshot.date = dateutil.parser.parse(snapshot.start_time)
166
snaps.append(snapshot)
167
snaps.sort(cmp=lambda x,y: cmp(x.date, y.date))
170
def attach(self, server=None):
171
if self.attachment_state == 'attached':
172
print 'already attached'
177
ec2 = self.get_ec2_connection()
178
ec2.attach_volume(self.volume_id, self.server.instance_id, self.device)
180
def detach(self, force=False):
181
state = self.attachment_state
182
if state == 'available' or state == None or state == 'detaching':
183
print 'already detached'
185
ec2 = self.get_ec2_connection()
186
ec2.detach_volume(self.volume_id, self.server.instance_id, self.device, force)
190
def checkfs(self, use_cmd=None):
191
if self.server == None:
192
raise ValueError, 'server attribute must be set to run this command'
193
# detemine state of file system on volume, only works if attached
197
cmd = self.server.get_cmdshell()
198
status = cmd.run('xfs_check %s' % self.device)
201
if status[1].startswith('bad superblock magic number 0'):
206
if self.server == None:
207
raise ValueError, 'server attribute must be set to run this command'
208
with closing(self.server.get_cmdshell()) as cmd:
209
# wait for the volume device to appear
210
cmd = self.server.get_cmdshell()
211
while not cmd.exists(self.device):
212
boto.log.info('%s still does not exist, waiting 10 seconds' % self.device)
216
if self.server == None:
217
raise ValueError, 'server attribute must be set to run this command'
219
with closing(self.server.get_cmdshell()) as cmd:
220
if not self.checkfs(cmd):
221
boto.log.info('make_fs...')
222
status = cmd.run('mkfs -t xfs %s' % self.device)
226
if self.server == None:
227
raise ValueError, 'server attribute must be set to run this command'
228
boto.log.info('handle_mount_point')
229
with closing(self.server.get_cmdshell()) as cmd:
230
cmd = self.server.get_cmdshell()
231
if not cmd.isdir(self.mount_point):
232
boto.log.info('making directory')
233
# mount directory doesn't exist so create it
234
cmd.run("mkdir %s" % self.mount_point)
236
boto.log.info('directory exists already')
237
status = cmd.run('mount -l')
238
lines = status[1].split('\n')
241
if t and t[2] == self.mount_point:
242
# something is already mounted at the mount point
243
# unmount that and mount it as /tmp
244
if t[0] != self.device:
245
cmd.run('umount %s' % self.mount_point)
246
cmd.run('mount %s /tmp' % t[0])
247
cmd.run('chmod 777 /tmp')
249
# Mount up our new EBS volume onto mount_point
250
cmd.run("mount %s %s" % (self.device, self.mount_point))
251
cmd.run('xfs_growfs %s' % self.mount_point)
253
def make_ready(self, server):
264
return self.server.run("/usr/sbin/xfs_freeze -f %s" % self.mount_point)
268
return self.server.run("/usr/sbin/xfs_freeze -u %s" % self.mount_point)
271
# if this volume is attached to a server
272
# we need to freeze the XFS file system
274
status = self.freeze(keep_alive=True)
276
snapshot = self.server.ec2.create_snapshot(self.volume_id)
277
boto.log.info('Snapshot of Volume %s created: %s' % (self.name, snapshot))
279
boto.log.info('Snapshot error')
280
boto.log.info(traceback.format_exc())
282
status = self.unfreeze()
285
def get_snapshot_range(self, snaps, start_date=None, end_date=None):
288
if start_date and end_date:
289
if snap.date >= start_date and snap.date <= end_date:
292
if snap.date >= start_date:
295
if snap.date <= end_date:
301
def trim_snapshots(self, delete=False):
303
Trim the number of snapshots for this volume. This method always
304
keeps the oldest snapshot. It then uses the parameters passed in
305
to determine how many others should be kept.
307
The algorithm is to keep all snapshots from the current day. Then
308
it will keep the first snapshot of the day for the previous seven days.
309
Then, it will keep the first snapshot of the week for the previous
310
four weeks. After than, it will keep the first snapshot of the month
311
for as many months as there are.
314
snaps = self.get_snapshots()
315
# Always keep the oldest and the newest
319
now = datetime.datetime.now(snaps[0].date.tzinfo)
320
midnight = datetime.datetime(year=now.year, month=now.month,
321
day=now.day, tzinfo=now.tzinfo)
322
# Keep the first snapshot from each day of the previous week
323
one_week = datetime.timedelta(days=7, seconds=60*60)
324
previous_week = self.get_snapshot_range(snaps, midnight-one_week, midnight)
326
for snap in previous_week:
327
if current_day and current_day == snap.date.day:
330
current_day = snap.date.day
331
# Get ourselves onto the next full week boundary
332
week_boundary = previous_week[0].date
333
if week_boundary.weekday() != 0:
334
delta = datetime.timedelta(days=week_boundary.weekday())
335
week_boundary = week_boundary - delta
336
# Keep one within this partial week
337
partial_week = self.get_snapshot_range(snaps, week_boundary, previous_week[0].date)
338
if len(partial_week) > 1:
339
for snap in partial_week[1:]:
341
# Keep the first snapshot of each week for the previous 4 weeks
343
weeks_worth = self.get_snapshot_range(snaps, week_boundary-one_week, week_boundary)
344
if len(weeks_worth) > 1:
345
for snap in weeks_worth[1:]:
347
week_boundary = week_boundary - one_week
348
# Now look through all remaining snaps and keep one per month
349
remainder = self.get_snapshot_range(snaps, end_date=week_boundary)
351
for snap in remainder:
352
if current_month and current_month == snap.date.month:
355
current_month = snap.date.month
359
boto.log.info('Deleting %s(%s) for %s' % (snap, snap.date, self.name))
363
def grow(self, size):
366
def copy(self, snapshot):
369
def get_snapshot_from_date(self, date):
372
def delete(self, delete_ebs_volume=False):
373
if delete_ebs_volume:
375
ec2 = self.get_ec2_connection()
376
ec2.delete_volume(self.volume_id)
380
# snapshot volume, trim snaps, delete volume-id