~smoser/ubuntu/lucid/python-boto/lucid

« back to all changes in this revision

Viewing changes to boto/manage/volume.py

  • Committer: Bazaar Package Importer
  • Author(s): Eric Evans
  • Date: 2009-04-28 21:11:20 UTC
  • mfrom: (1.1.5 upstream) (4.1.3 squeeze)
  • Revision ID: james.westby@ubuntu.com-20090428211120-ciqofy4vccm47pe3
Tags: 1.7a-2
Patched test file to restore compatibility with python 2.4,
(Closes: #525365).

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2006-2009 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
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
 
27
import boto.ec2
 
28
import time, traceback
 
29
from contextlib import closing
 
30
import dateutil.parser
 
31
 
 
32
class CommandLineGetter(object):
 
33
    
 
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)
 
38
 
 
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)
 
44
            
 
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)
 
49
 
 
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)
 
54
 
 
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)
 
59
 
 
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)
 
64
 
 
65
    def get(self, cls, params):
 
66
        self.cls = cls
 
67
        self.get_region(params)
 
68
        self.ec2 = params['region'].connect()
 
69
        self.get_zone(params)
 
70
        self.get_name(params)
 
71
        self.get_size(params)
 
72
        self.get_mount_point(params)
 
73
        self.get_device(params)
 
74
 
 
75
class Volume(Model):
 
76
 
 
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)
 
91
 
 
92
    @classmethod
 
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)
 
101
        v = cls()
 
102
        v.ec2 = ec2
 
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
 
108
        v.put()
 
109
        return v
 
110
 
 
111
    @classmethod
 
112
    def create_from_volume_id(cls, region_name, volume_id, name):
 
113
        vol = None
 
114
        ec2 = boto.ec2.connect_to_region(region_name)
 
115
        rs = ec2.get_all_volumes([volume_id])
 
116
        if len(rs) == 1:
 
117
            v = rs[0]
 
118
            vol = cls()
 
119
            vol.volume_id = v.id
 
120
            vol.name = name
 
121
            vol.region_name = v.region.name
 
122
            vol.put()
 
123
        return vol
 
124
    
 
125
    def get_ec2_connection(self):
 
126
        if self.server:
 
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)
 
130
        return self.ec2
 
131
 
 
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()
 
136
 
 
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()
 
141
 
 
142
    def _size(self):
 
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
 
147
        return self.__size
 
148
 
 
149
    def install_xfs(self):
 
150
        if self.server:
 
151
            self.server.install('xfsprogs xfsdump')
 
152
 
 
153
    def get_snapshots(self):
 
154
        """
 
155
        Returns a list of all completed snapshots for this volume ID.
 
156
        """
 
157
        ec2 = self.get_ec2_connection()
 
158
        rs = ec2.get_all_snapshots()
 
159
        all_vols = [self.volume_id] + self.past_volume_ids
 
160
        snaps = []
 
161
        for snapshot in rs:
 
162
            if snapshot.volume_id in all_vols:
 
163
                if snapshot.progress == '100%':
 
164
                    snapshot.date = dateutil.parser.parse(snapshot.start_time)
 
165
                    snapshot.keep = True
 
166
                    snaps.append(snapshot)
 
167
        snaps.sort(cmp=lambda x,y: cmp(x.date, y.date))
 
168
        return snaps
 
169
 
 
170
    def attach(self, server=None):
 
171
        if self.attachment_state == 'attached':
 
172
            print 'already attached'
 
173
            return None
 
174
        if server:
 
175
            self.server = server
 
176
            self.put()
 
177
        ec2 = self.get_ec2_connection()
 
178
        ec2.attach_volume(self.volume_id, self.server.instance_id, self.device)
 
179
 
 
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'
 
184
            return None
 
185
        ec2 = self.get_ec2_connection()
 
186
        ec2.detach_volume(self.volume_id, self.server.instance_id, self.device, force)
 
187
        self.server = None
 
188
        self.put()
 
189
 
 
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
 
194
        if use_cmd:
 
195
            cmd = use_cmd
 
196
        else:
 
197
            cmd = self.server.get_cmdshell()
 
198
        status = cmd.run('xfs_check %s' % self.device)
 
199
        if not use_cmd:
 
200
            cmd.close()
 
201
        if status[1].startswith('bad superblock magic number 0'):
 
202
            return False
 
203
        return True
 
204
 
 
205
    def wait(self):
 
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)
 
213
                time.sleep(10)
 
214
 
 
215
    def format(self):
 
216
        if self.server == None:
 
217
            raise ValueError, 'server attribute must be set to run this command'
 
218
        status = None
 
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)
 
223
        return status
 
224
 
 
225
    def mount(self):
 
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)
 
235
            else:
 
236
                boto.log.info('directory exists already')
 
237
                status = cmd.run('mount -l')
 
238
                lines = status[1].split('\n')
 
239
                for line in lines:
 
240
                    t = line.split()
 
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')
 
248
                            break
 
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)
 
252
 
 
253
    def make_ready(self, server):
 
254
        self.server = server
 
255
        self.put()
 
256
        self.install_xfs()
 
257
        self.attach()
 
258
        self.wait()
 
259
        self.format()
 
260
        self.mount()
 
261
 
 
262
    def freeze(self):
 
263
        if self.server:
 
264
            return self.server.run("/usr/sbin/xfs_freeze -f %s" % self.mount_point)
 
265
 
 
266
    def unfreeze(self):
 
267
        if self.server:
 
268
            return self.server.run("/usr/sbin/xfs_freeze -u %s" % self.mount_point)
 
269
 
 
270
    def snapshot(self):
 
271
        # if this volume is attached to a server
 
272
        # we need to freeze the XFS file system
 
273
        try:
 
274
            status = self.freeze(keep_alive=True)
 
275
            print status[1]
 
276
            snapshot = self.server.ec2.create_snapshot(self.volume_id)
 
277
            boto.log.info('Snapshot of Volume %s created: %s' %  (self.name, snapshot))
 
278
        except Exception, e:
 
279
            boto.log.info('Snapshot error')
 
280
            boto.log.info(traceback.format_exc())
 
281
        finally:
 
282
            status = self.unfreeze()
 
283
            return status
 
284
 
 
285
    def get_snapshot_range(self, snaps, start_date=None, end_date=None):
 
286
        l = []
 
287
        for snap in snaps:
 
288
            if start_date and end_date:
 
289
                if snap.date >= start_date and snap.date <= end_date:
 
290
                    l.append(snap)
 
291
            elif start_date:
 
292
                if snap.date >= start_date:
 
293
                    l.append(snap)
 
294
            elif end_date:
 
295
                if snap.date <= end_date:
 
296
                    l.append(snap)
 
297
            else:
 
298
                l.append(snap)
 
299
        return l
 
300
 
 
301
    def trim_snapshots(self, delete=False):
 
302
        """
 
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.
 
306
 
 
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.
 
312
 
 
313
        """
 
314
        snaps = self.get_snapshots()
 
315
        # Always keep the oldest and the newest
 
316
        if len(snaps) <= 2:
 
317
            return snaps
 
318
        snaps = snaps[1:-1]
 
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)
 
325
        current_day = None
 
326
        for snap in previous_week:
 
327
            if current_day and current_day == snap.date.day:
 
328
                snap.keep = False
 
329
            else:
 
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:]:
 
340
                snap.keep = False
 
341
        # Keep the first snapshot of each week for the previous 4 weeks
 
342
        for i in range(0,4):
 
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:]:
 
346
                    snap.keep = False
 
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)
 
350
        current_month = None
 
351
        for snap in remainder:
 
352
            if current_month and current_month == snap.date.month:
 
353
                snap.keep = False
 
354
            else:
 
355
                current_month = snap.date.month
 
356
        if delete:
 
357
            for snap in snaps:
 
358
                if not snap.keep:
 
359
                    boto.log.info('Deleting %s(%s) for %s' % (snap, snap.date, self.name))
 
360
                    snap.delete()
 
361
        return snaps
 
362
                
 
363
    def grow(self, size):
 
364
        pass
 
365
 
 
366
    def copy(self, snapshot):
 
367
        pass
 
368
 
 
369
    def get_snapshot_from_date(self, date):
 
370
        pass
 
371
 
 
372
    def delete(self, delete_ebs_volume=False):
 
373
        if delete_ebs_volume:
 
374
            self.detach()
 
375
            ec2 = self.get_ec2_connection()
 
376
            ec2.delete_volume(self.volume_id)
 
377
        Model.delete(self)
 
378
 
 
379
    def archive(self):
 
380
        # snapshot volume, trim snaps, delete volume-id
 
381
        pass
 
382
    
 
383