volumes.py 12.3 KB
Newer Older
Vangelis Koukis's avatar
Vangelis Koukis committed
1
# Copyright (C) 2010-2014 GRNET S.A.
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3
4
5
6
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8
9
10
11
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13
14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

16
17
18
import logging

from django.db import transaction
19
from django.conf import settings
20
from snf_django.lib.api import faults
21
from synnefo.db.models import Volume, VolumeMetadata
22
from synnefo.volume import util
23
from synnefo.logic import server_attachments, utils, commands
24
from synnefo import quotas
25
26
27
28
29
30
31

log = logging.getLogger(__name__)


@transaction.commit_on_success
def create(user_id, size, server_id, name=None, description=None,
           source_volume_id=None, source_snapshot_id=None,
32
33
           source_image_id=None, volume_type_id=None, metadata=None,
           project=None):
34

35
    # Currently we cannot create volumes without being attached to a server
36
37
38
    if server_id is None:
        raise faults.BadRequest("Volume must be attached to server")
    server = util.get_server(user_id, server_id, for_update=True,
39
                             non_deleted=True,
40
41
                             exception=faults.BadRequest)

42
43
44
45
46
47
48
49
50
51
52
53
    server_vtype = server.flavor.volume_type
    if volume_type_id is not None:
        volume_type = util.get_volume_type(volume_type_id,
                                           include_deleted=False,
                                           exception=faults.BadRequest)
        if volume_type != server_vtype:
            raise faults.BadRequest("Cannot create a volume with type '%s' to"
                                    " a server with volume type '%s'."
                                    % (volume_type.id, server_vtype.id))
    else:
        volume_type = server_vtype

54
55
56
57
58
59
    # Assert that not more than one source are used
    sources = filter(lambda x: x is not None,
                     [source_volume_id, source_snapshot_id, source_image_id])
    if len(sources) > 1:
        raise faults.BadRequest("Volume can not have more than one source!")

60
61
62
63
64
65
66
67
68
69
    if source_volume_id is not None:
        source_type = "volume"
        source_uuid = source_volume_id
    elif source_snapshot_id is not None:
        source_type = "snapshot"
        source_uuid = source_snapshot_id
    elif source_image_id is not None:
        source_type = "image"
        source_uuid = source_image_id
    else:
70
71
        source_type = "blank"
        source_uuid = None
72

73
74
75
76
77
    if project is None:
        project = user_id

    volume = _create_volume(server, user_id, project, size,
                            source_type, source_uuid,
78
79
                            volume_type=volume_type, name=name,
                            description=description, index=None)
80
81
82

    if metadata is not None:
        for meta_key, meta_val in metadata.items():
83
84
85
86
            utils.check_name_length(meta_key, VolumeMetadata.KEY_LENGTH,
                                    "Metadata key is too long")
            utils.check_name_length(meta_val, VolumeMetadata.VALUE_LENGTH,
                                    "Metadata key is too long")
87
88
89
90
91
92
93
            volume.metadata.create(key=meta_key, value=meta_val)

    server_attachments.attach_volume(server, volume)

    return volume


94
def _create_volume(server, user_id, project, size, source_type, source_uuid,
95
                   volume_type, name=None, description=None, index=None,
96
97
                   delete_on_termination=True):

98
99
100
    utils.check_name_length(name, Volume.NAME_LENGTH,
                            "Volume name is too long")
    utils.check_name_length(description, Volume.DESCRIPTION_LENGTH,
Christos Stavrakakis's avatar
Christos Stavrakakis committed
101
                            "Volume description is too long")
102
    validate_volume_termination(volume_type, delete_on_termination)
103

104
105
106
107
108
    if index is None:
        # Counting a server's volumes is safe, because we have an
        # X-lock on the server.
        index = server.volumes.filter(deleted=False).count()

109
110
111
112
113
114
    if size is not None:
        try:
            size = int(size)
        except (TypeError, ValueError):
            raise faults.BadRequest("Volume 'size' needs to be a positive"
                                    " integer value.")
115
116
117
118
119
        if size < 1:
            raise faults.BadRequest("Volume size must be a positive integer")
        if size > settings.CYCLADES_VOLUME_MAX_SIZE:
            raise faults.BadRequest("Maximum volume size is '%sGB'" %
                                    settings.CYCLADES_VOLUME_MAX_SIZE)
120

121
122
    # Only ext_ disk template supports cloning from another source. Otherwise
    # is must be the root volume so that 'snf-image' fill the volume
123
    can_have_source = (index == 0 or
124
                       volume_type.provider in settings.GANETI_CLONE_PROVIDERS)
125
    if not can_have_source and source_type != "blank":
126
127
128
        msg = ("Cannot specify a 'source' attribute for volume type '%s' with"
               " disk template '%s'" %
               (volume_type.id, volume_type.disk_template))
129
130
        raise faults.BadRequest(msg)

131
    source_version = None
132
    # TODO: Check Volume/Snapshot Status
133
    if source_type == "snapshot":
134
        source_snapshot = util.get_snapshot(user_id, source_uuid,
135
                                            exception=faults.BadRequest)
136
137
138
139
140
        snap_status = source_snapshot.get("status", "").upper()
        if snap_status != "AVAILABLE":
            raise faults.BadRequest("Cannot create volume from snapshot, while"
                                    " snapshot is in '%s' status" %
                                    snap_status)
141
        source = Volume.prefix_source(source_uuid,
142
                                      source_type="snapshot")
143
144
145
146
147
148
        if size is None:
            raise faults.BadRequest("Volume size is required")
        elif (size << 30) < int(source_snapshot["size"]):
            raise faults.BadRequest("Volume size '%s' is smaller than"
                                    " snapshot's size '%s'"
                                    % (size << 30, source_snapshot["size"]))
149
        source_version = source_snapshot["version"]
150
        origin = source_snapshot["mapfile"]
151
152
    elif source_type == "image":
        source_image = util.get_image(user_id, source_uuid,
153
                                      exception=faults.BadRequest)
154
155
156
157
        img_status = source_image.get("status", "").upper()
        if img_status != "AVAILABLE":
            raise faults.BadRequest("Cannot create volume from image, while"
                                    " image is in '%s' status" % img_status)
158
159
160
161
162
163
164
        if size is None:
            raise faults.BadRequest("Volume size is required")
        elif (size << 30) < int(source_image["size"]):
            raise faults.BadRequest("Volume size '%s' is smaller than"
                                    " image's size '%s'"
                                    % (size << 30, source_image["size"]))
        source = Volume.prefix_source(source_uuid, source_type="image")
165
        source_version = source_image["version"]
166
        origin = source_image["mapfile"]
167
168
169
170
    elif source_type == "blank":
        if size is None:
            raise faults.BadRequest("Volume size is required")
        source = origin = None
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
    elif source_type == "volume":
        # Currently, Archipelago does not support cloning a volume
        raise faults.BadRequest("Cloning a volume is not supported")
        # source_volume = util.get_volume(user_id, source_uuid,
        #                                 for_update=True, non_deleted=True,
        #                                 exception=faults.BadRequest)
        # if source_volume.status != "IN_USE":
        #     raise faults.BadRequest("Cannot clone volume while it is in '%s'"
        #                             " status" % source_volume.status)
        # # If no size is specified, use the size of the volume
        # if size is None:
        #     size = source_volume.size
        # elif size < source_volume.size:
        #     raise faults.BadRequest("Volume size cannot be smaller than the"
        #                             " source volume")
        # source = Volume.prefix_source(source_uuid, source_type="volume")
        # origin = source_volume.backend_volume_uuid
188
    else:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
189
        raise faults.BadRequest("Unknown source type")
190
191

    volume = Volume.objects.create(userid=user_id,
192
                                   project=project,
193
                                   size=size,
194
                                   volume_type=volume_type,
195
196
197
                                   name=name,
                                   machine=server,
                                   description=description,
198
                                   delete_on_termination=delete_on_termination,
199
                                   source=source,
200
                                   source_version=source_version,
201
                                   origin=origin,
202
                                   index=index,
203
204
205
206
207
208
                                   status="CREATING")
    return volume


@transaction.commit_on_success
def delete(volume):
209
210
211
    """Delete a Volume"""
    # A volume is deleted by detaching it from the server that is attached.
    # Deleting a detached volume is not implemented.
212
213
214
215
216
217
    server_id = volume.machine_id
    if server_id is not None:
        server = util.get_server(volume.userid, server_id, for_update=True,
                                 non_deleted=True,
                                 exception=faults.BadRequest)
        server_attachments.detach_volume(server, volume)
218
        log.info("Detach volume '%s' from server '%s', job: %s",
219
                 volume.id, server_id, volume.backendjobid)
220
221
    else:
        raise faults.BadRequest("Cannot delete a detached volume")
222
223
224
225
226

    return volume


@transaction.commit_on_success
227
228
def update(volume, name=None, description=None, delete_on_termination=None):
    if name is not None:
229
230
        utils.check_name_length(name, Volume.NAME_LENGTH,
                                "Volume name is too long")
231
232
        volume.name = name
    if description is not None:
233
234
        utils.check_name_length(description, Volume.DESCRIPTION_LENGTH,
                                "Volume description is too long")
235
236
        volume.description = description
    if delete_on_termination is not None:
237
        validate_volume_termination(volume.volume_type, delete_on_termination)
238
        volume.delete_on_termination = delete_on_termination
239
240
241

    volume.save()
    return volume
242
243
244
245
246
247


@transaction.commit_on_success
def reassign_volume(volume, project):
    if volume.index == 0:
        raise faults.Conflict("Cannot reassign: %s is a system volume" %
248
                              volume.id)
249
250
251
252
253
    if volume.machine_id is not None:
        server = util.get_server(volume.userid, volume.machine_id,
                                 for_update=True, non_deleted=True,
                                 exception=faults.BadRequest)
        commands.validate_server_action(server, "REASSIGN")
254
255
    action_fields = {"from_project": volume.project, "to_project": project}
    log.info("Reassigning volume %s from project %s to %s",
256
             volume.id, volume.project, project)
257
258
259
260
    volume.project = project
    volume.save()
    quotas.issue_and_accept_commission(volume, action="REASSIGN",
                                       action_fields=action_fields)
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279


def validate_volume_termination(volume_type, delete_on_termination):
    """Validate volume's termination mode based on volume's type.

    NOTE: Currently, detached volumes are not supported, so all volumes
    must be terminated upon instance deletion.

    """
    if delete_on_termination is False:
        # Only ext_* volumes can be preserved
        if volume_type.template != "ext":
            raise faults.BadRequest("Volumes of '%s' disk template cannot have"
                                    " 'delete_on_termination' attribute set"
                                    " to 'False'" % volume_type.disk_template)
        # But currently all volumes must be terminated
        raise faults.NotImplemented("Volumes with the 'delete_on_termination'"
                                    " attribute set to False are not"
                                    " supported")