summaryrefslogtreecommitdiff
path: root/cinder/api/v2/volumes.py
blob: ddff0471e6cabbcf8c5350d26d33b9e8fa0e437a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# Copyright 2011 Justin Santa Barbara
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""The volumes api."""

from http import HTTPStatus

from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
from oslo_utils import uuidutils
import webob
from webob import exc

from cinder.api import api_utils
from cinder.api import common
from cinder.api.contrib import scheduler_hints
from cinder.api import microversions as mv
from cinder.api.openstack import wsgi
from cinder.api.schemas import volumes
from cinder.api.v2.views import volumes as volume_views
from cinder.api import validation
from cinder import exception
from cinder import group as group_api
from cinder.i18n import _
from cinder.image import glance
from cinder import objects
from cinder import utils
from cinder import volume as cinder_volume
from cinder.volume import volume_utils

CONF = cfg.CONF

LOG = logging.getLogger(__name__)


class VolumeController(wsgi.Controller):
    """The Volumes API controller for the OpenStack API."""

    _view_builder_class = volume_views.ViewBuilder

    def __init__(self, ext_mgr):
        self.volume_api = cinder_volume.API()
        self.group_api = group_api.API()
        self.ext_mgr = ext_mgr
        super(VolumeController, self).__init__()

    def show(self, req, id):
        """Return data about the given volume."""
        context = req.environ['cinder.context']

        # Not found exception will be handled at the wsgi level
        vol = self.volume_api.get(context, id, viewable_admin_meta=True)
        req.cache_db_volume(vol)

        api_utils.add_visible_admin_metadata(vol)

        return self._view_builder.detail(req, vol)

    def delete(self, req, id):
        """Delete a volume."""
        context = req.environ['cinder.context']

        cascade = utils.get_bool_param('cascade', req.params)

        LOG.info("Delete volume with id: %s", id)

        # Not found exception will be handled at the wsgi level
        volume = self.volume_api.get(context, id)
        self.volume_api.delete(context, volume, cascade=cascade)
        return webob.Response(status_int=HTTPStatus.ACCEPTED)

    def index(self, req):
        """Returns a summary list of volumes."""
        return self._get_volumes(req, is_detail=False)

    def detail(self, req):
        """Returns a detailed list of volumes."""
        return self._get_volumes(req, is_detail=True)

    def _get_volumes(self, req, is_detail):
        """Returns a list of volumes, transformed through view builder."""

        context = req.environ['cinder.context']

        params = req.params.copy()
        marker, limit, offset = common.get_pagination_params(params)
        sort_keys, sort_dirs = common.get_sort_params(params)
        filters = params

        # NOTE(wanghao): Always removing glance_metadata since we support it
        # only in API version >= VOLUME_LIST_GLANCE_METADATA.
        filters.pop('glance_metadata', None)
        api_utils.remove_invalid_filter_options(
            context,
            filters,
            self._get_volume_filter_options())

        # NOTE(thingee): v2 API allows name instead of display_name
        if 'name' in sort_keys:
            sort_keys[sort_keys.index('name')] = 'display_name'

        if 'name' in filters:
            filters['display_name'] = filters.pop('name')

        self.volume_api.check_volume_filters(filters)
        volumes = self.volume_api.get_all(context, marker, limit,
                                          sort_keys=sort_keys,
                                          sort_dirs=sort_dirs,
                                          filters=filters,
                                          viewable_admin_meta=True,
                                          offset=offset)

        for volume in volumes:
            api_utils.add_visible_admin_metadata(volume)

        req.cache_db_volumes(volumes.objects)

        if is_detail:
            volumes = self._view_builder.detail_list(req, volumes)
        else:
            volumes = self._view_builder.summary_list(req, volumes)
        return volumes

    def _image_uuid_from_ref(self, image_ref, context):
        # If the image ref was generated by nova api, strip image_ref
        # down to an id.
        image_uuid = None
        try:
            image_uuid = image_ref.split('/').pop()
        except AttributeError:
            msg = _("Invalid imageRef provided.")
            raise exc.HTTPBadRequest(explanation=msg)

        image_service = glance.get_default_image_service()

        # First see if this is an actual image ID
        if uuidutils.is_uuid_like(image_uuid):
            try:
                image = image_service.show(context, image_uuid)
                if 'id' in image:
                    return image['id']
            except Exception:
                # Pass and see if there is a matching image name
                pass

        # Could not find by ID, check if it is an image name
        try:
            params = {'filters': {'name': image_ref}}
            images = list(image_service.detail(context, **params))
            if len(images) > 1:
                msg = _("Multiple matches found for '%s', use an ID to be more"
                        " specific.") % image_ref
                raise exc.HTTPConflict(explanation=msg)
            for img in images:
                return img['id']
        except exc.HTTPConflict:
            raise
        except Exception:
            # Pass the other exception and let default not found error
            # handling take care of it
            pass

        msg = _("Invalid image identifier or unable to "
                "access requested image.")
        raise exc.HTTPBadRequest(explanation=msg)

    # NOTE: using mv.BASE_VERSION (which is 3.0) is a bit nonstandard,
    # but this class is no longer consumed by the v2 API, though it is
    # a superclass of cinder.api.v3.volumes.  Although create() is
    # overridden in the subclass, I didn't want to remove it from
    # here until we are sure that the v3 unit tests for create() test
    # everything that the v2 unit tests covered.
    @wsgi.response(HTTPStatus.ACCEPTED)
    @validation.schema(volumes.create, mv.BASE_VERSION)
    def create(self, req, body):
        """Creates a new volume."""

        LOG.debug('Create volume request body: %s', body)
        context = req.environ['cinder.context']

        # NOTE (pooja_jadhav) To fix bug 1774155, scheduler hints is not
        # loaded as a standard extension. If user passes
        # OS-SCH-HNT:scheduler_hints in the request body, then it will be
        # validated in the create method and this method will add
        # scheduler_hints in body['volume'].
        body = scheduler_hints.create(req, body)
        volume = body['volume']

        kwargs = {}
        self.validate_name_and_description(volume, check_length=False)

        # NOTE(thingee): v2 API allows name instead of display_name
        if 'name' in volume:
            volume['display_name'] = volume.pop('name')

        # NOTE(thingee): v2 API allows description instead of
        #                display_description
        if 'description' in volume:
            volume['display_description'] = volume.pop('description')

        if 'image_id' in volume:
            volume['imageRef'] = volume.pop('image_id')

        req_volume_type = volume.get('volume_type', None)
        if req_volume_type:
            # Not found exception will be handled at the wsgi level
            kwargs['volume_type'] = (
                objects.VolumeType.get_by_name_or_id(context, req_volume_type))

        kwargs['metadata'] = volume.get('metadata', None)

        snapshot_id = volume.get('snapshot_id')
        if snapshot_id is not None:
            # Not found exception will be handled at the wsgi level
            kwargs['snapshot'] = self.volume_api.get_snapshot(context,
                                                              snapshot_id)
        else:
            kwargs['snapshot'] = None

        source_volid = volume.get('source_volid')
        if source_volid is not None:
            # Not found exception will be handled at the wsgi level
            kwargs['source_volume'] = \
                self.volume_api.get_volume(context,
                                           source_volid)
        else:
            kwargs['source_volume'] = None

        kwargs['group'] = None
        kwargs['consistencygroup'] = None
        consistencygroup_id = volume.get('consistencygroup_id')
        if consistencygroup_id is not None:
            # Not found exception will be handled at the wsgi level
            kwargs['group'] = self.group_api.get(context, consistencygroup_id)

        size = volume.get('size', None)
        if size is None and kwargs['snapshot'] is not None:
            size = kwargs['snapshot']['volume_size']
        elif size is None and kwargs['source_volume'] is not None:
            size = kwargs['source_volume']['size']

        LOG.info("Create volume of %s GB", size)

        image_ref = volume.get('imageRef')
        if image_ref is not None:
            image_uuid = self._image_uuid_from_ref(image_ref, context)
            kwargs['image_id'] = image_uuid

        kwargs['availability_zone'] = volume.get('availability_zone', None)
        kwargs['scheduler_hints'] = volume.get('scheduler_hints', None)
        kwargs['multiattach'] = utils.get_bool_param('multiattach', volume)

        if kwargs.get('multiattach', False):
            msg = ("The option 'multiattach' "
                   "is deprecated and will be removed in a future "
                   "release.  The default behavior going forward will "
                   "be to specify multiattach enabled volume types.")
            versionutils.report_deprecated_feature(LOG, msg)

        try:
            new_volume = self.volume_api.create(
                context, size, volume.get('display_name'),
                volume.get('display_description'), **kwargs)
        except exception.VolumeTypeDefaultMisconfiguredError as err:
            raise webob.exc.HTTPInternalServerError(explanation=err.msg)

        retval = self._view_builder.detail(req, new_volume)

        return retval

    def _get_volume_filter_options(self):
        """Return volume search options allowed by non-admin."""
        return common.get_enabled_resource_filters('volume').get('volume', [])

    # NOTE: see NOTE for create(), above
    @validation.schema(volumes.update, mv.BASE_VERSION,
                       mv.get_prior_version(mv.SUPPORT_VOLUME_SCHEMA_CHANGES))
    @validation.schema(volumes.update_volume_v353,
                       mv.SUPPORT_VOLUME_SCHEMA_CHANGES)
    def update(self, req, id, body):
        """Update a volume."""
        context = req.environ['cinder.context']
        update_dict = body['volume']

        self.validate_name_and_description(update_dict, check_length=False)

        # NOTE(thingee): v2 API allows name instead of display_name
        if 'name' in update_dict:
            update_dict['display_name'] = update_dict.pop('name')

        # NOTE(thingee): v2 API allows description instead of
        #                display_description
        if 'description' in update_dict:
            update_dict['display_description'] = update_dict.pop('description')

        # Not found and Invalid exceptions will be handled at the wsgi level
        try:
            volume = self.volume_api.get(context, id, viewable_admin_meta=True)
            volume_utils.notify_about_volume_usage(context, volume,
                                                   'update.start')
            self.volume_api.update(context, volume, update_dict)
        except exception.InvalidVolumeMetadataSize as error:
            raise webob.exc.HTTPRequestEntityTooLarge(explanation=error.msg)

        volume.update(update_dict)

        api_utils.add_visible_admin_metadata(volume)

        volume_utils.notify_about_volume_usage(context, volume,
                                               'update.end')

        return self._view_builder.detail(req, volume)


def create_resource(ext_mgr):
    return wsgi.Resource(VolumeController(ext_mgr))