1# Copyright (c) 2006-2012 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2010, Eucalyptus Systems, Inc.
3# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.  All Rights Reserved
4#
5# Permission is hereby granted, free of charge, to any person obtaining a
6# copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish, dis-
9# tribute, sublicense, and/or sell copies of the Software, and to permit
10# persons to whom the Software is furnished to do so, subject to the fol-
11# lowing conditions:
12#
13# The above copyright notice and this permission notice shall be included
14# in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22# IN THE SOFTWARE.
23
24"""
25Represents an EC2 Elastic Block Storage Volume
26"""
27from boto.resultset import ResultSet
28from boto.ec2.tag import Tag
29from boto.ec2.ec2object import TaggedEC2Object
30
31
32class Volume(TaggedEC2Object):
33    """
34    Represents an EBS volume.
35
36    :ivar id: The unique ID of the volume.
37    :ivar create_time: The timestamp of when the volume was created.
38    :ivar status: The status of the volume.
39    :ivar size: The size (in GB) of the volume.
40    :ivar snapshot_id: The ID of the snapshot this volume was created
41        from, if applicable.
42    :ivar attach_data: An AttachmentSet object.
43    :ivar zone: The availability zone this volume is in.
44    :ivar type: The type of volume (standard or consistent-iops)
45    :ivar iops: If this volume is of type consistent-iops, this is
46        the number of IOPS provisioned (10-300).
47    :ivar encrypted: True if this volume is encrypted.
48    """
49
50    def __init__(self, connection=None):
51        super(Volume, self).__init__(connection)
52        self.id = None
53        self.create_time = None
54        self.status = None
55        self.size = None
56        self.snapshot_id = None
57        self.attach_data = None
58        self.zone = None
59        self.type = None
60        self.iops = None
61        self.encrypted = None
62
63    def __repr__(self):
64        return 'Volume:%s' % self.id
65
66    def startElement(self, name, attrs, connection):
67        retval = super(Volume, self).startElement(name, attrs, connection)
68        if retval is not None:
69            return retval
70        if name == 'attachmentSet':
71            self.attach_data = AttachmentSet()
72            return self.attach_data
73        elif name == 'tagSet':
74            self.tags = ResultSet([('item', Tag)])
75            return self.tags
76        else:
77            return None
78
79    def endElement(self, name, value, connection):
80        if name == 'volumeId':
81            self.id = value
82        elif name == 'createTime':
83            self.create_time = value
84        elif name == 'status':
85            if value != '':
86                self.status = value
87        elif name == 'size':
88            self.size = int(value)
89        elif name == 'snapshotId':
90            self.snapshot_id = value
91        elif name == 'availabilityZone':
92            self.zone = value
93        elif name == 'volumeType':
94            self.type = value
95        elif name == 'iops':
96            self.iops = int(value)
97        elif name == 'encrypted':
98            self.encrypted = (value.lower() == 'true')
99        else:
100            setattr(self, name, value)
101
102    def _update(self, updated):
103        self.__dict__.update(updated.__dict__)
104
105    def update(self, validate=False, dry_run=False):
106        """
107        Update the data associated with this volume by querying EC2.
108
109        :type validate: bool
110        :param validate: By default, if EC2 returns no data about the
111                         volume the update method returns quietly.  If
112                         the validate param is True, however, it will
113                         raise a ValueError exception if no data is
114                         returned from EC2.
115        """
116        # Check the resultset since Eucalyptus ignores the volumeId param
117        unfiltered_rs = self.connection.get_all_volumes(
118            [self.id],
119            dry_run=dry_run
120        )
121        rs = [x for x in unfiltered_rs if x.id == self.id]
122        if len(rs) > 0:
123            self._update(rs[0])
124        elif validate:
125            raise ValueError('%s is not a valid Volume ID' % self.id)
126        return self.status
127
128    def delete(self, dry_run=False):
129        """
130        Delete this EBS volume.
131
132        :rtype: bool
133        :return: True if successful
134        """
135        return self.connection.delete_volume(self.id, dry_run=dry_run)
136
137    def attach(self, instance_id, device, dry_run=False):
138        """
139        Attach this EBS volume to an EC2 instance.
140
141        :type instance_id: str
142        :param instance_id: The ID of the EC2 instance to which it will
143                            be attached.
144
145        :type device: str
146        :param device: The device on the instance through which the
147                       volume will be exposed (e.g. /dev/sdh)
148
149        :rtype: bool
150        :return: True if successful
151        """
152        return self.connection.attach_volume(
153            self.id,
154            instance_id,
155            device,
156            dry_run=dry_run
157        )
158
159    def detach(self, force=False, dry_run=False):
160        """
161        Detach this EBS volume from an EC2 instance.
162
163        :type force: bool
164        :param force: Forces detachment if the previous detachment
165            attempt did not occur cleanly.  This option can lead to
166            data loss or a corrupted file system. Use this option only
167            as a last resort to detach a volume from a failed
168            instance. The instance will not have an opportunity to
169            flush file system caches nor file system meta data. If you
170            use this option, you must perform file system check and
171            repair procedures.
172
173        :rtype: bool
174        :return: True if successful
175        """
176        instance_id = None
177        if self.attach_data:
178            instance_id = self.attach_data.instance_id
179        device = None
180        if self.attach_data:
181            device = self.attach_data.device
182        return self.connection.detach_volume(
183            self.id,
184            instance_id,
185            device,
186            force,
187            dry_run=dry_run
188        )
189
190    def create_snapshot(self, description=None, dry_run=False):
191        """
192        Create a snapshot of this EBS Volume.
193
194        :type description: str
195        :param description: A description of the snapshot.
196            Limited to 256 characters.
197
198        :rtype: :class:`boto.ec2.snapshot.Snapshot`
199        :return: The created Snapshot object
200        """
201        return self.connection.create_snapshot(
202            self.id,
203            description,
204            dry_run=dry_run
205        )
206
207    def volume_state(self):
208        """
209        Returns the state of the volume.  Same value as the status attribute.
210        """
211        return self.status
212
213    def attachment_state(self):
214        """
215        Get the attachment state.
216        """
217        state = None
218        if self.attach_data:
219            state = self.attach_data.status
220        return state
221
222    def snapshots(self, owner=None, restorable_by=None, dry_run=False):
223        """
224        Get all snapshots related to this volume.  Note that this requires
225        that all available snapshots for the account be retrieved from EC2
226        first and then the list is filtered client-side to contain only
227        those for this volume.
228
229        :type owner: str
230        :param owner: If present, only the snapshots owned by the
231            specified user will be returned.  Valid values are:
232
233            * self
234            * amazon
235            * AWS Account ID
236
237        :type restorable_by: str
238        :param restorable_by: If present, only the snapshots that
239            are restorable by the specified account id will be returned.
240
241        :rtype: list of L{boto.ec2.snapshot.Snapshot}
242        :return: The requested Snapshot objects
243
244        """
245        rs = self.connection.get_all_snapshots(
246            owner=owner,
247            restorable_by=restorable_by,
248            dry_run=dry_run
249        )
250        mine = []
251        for snap in rs:
252            if snap.volume_id == self.id:
253                mine.append(snap)
254        return mine
255
256
257class AttachmentSet(object):
258    """
259    Represents an EBS attachmentset.
260
261    :ivar id: The unique ID of the volume.
262    :ivar instance_id: The unique ID of the attached instance
263    :ivar status: The status of the attachment
264    :ivar attach_time: Attached since
265    :ivar device: The device the instance has mapped
266    """
267    def __init__(self):
268        self.id = None
269        self.instance_id = None
270        self.status = None
271        self.attach_time = None
272        self.device = None
273
274    def __repr__(self):
275        return 'AttachmentSet:%s' % self.id
276
277    def startElement(self, name, attrs, connection):
278        pass
279
280    def endElement(self, name, value, connection):
281        if name == 'volumeId':
282            self.id = value
283        elif name == 'instanceId':
284            self.instance_id = value
285        elif name == 'status':
286            self.status = value
287        elif name == 'attachTime':
288            self.attach_time = value
289        elif name == 'device':
290            self.device = value
291        else:
292            setattr(self, name, value)
293
294
295class VolumeAttribute(object):
296    def __init__(self, parent=None):
297        self.id = None
298        self._key_name = None
299        self.attrs = {}
300
301    def startElement(self, name, attrs, connection):
302        if name == 'autoEnableIO':
303            self._key_name = name
304        return None
305
306    def endElement(self, name, value, connection):
307        if name == 'value':
308            if value.lower() == 'true':
309                self.attrs[self._key_name] = True
310            else:
311                self.attrs[self._key_name] = False
312        elif name == 'volumeId':
313            self.id = value
314        else:
315            setattr(self, name, value)
316