1# Copyright (c) 2016 EMC Corporation 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may 4# not use this file except in compliance with the License. You may obtain 5# a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations 13# under the License. 14 15"""The groups controller.""" 16 17from oslo_log import log as logging 18from oslo_utils import strutils 19from oslo_utils import uuidutils 20from six.moves import http_client 21import webob 22from webob import exc 23 24from cinder.api import common 25from cinder.api import microversions as mv 26from cinder.api.openstack import wsgi 27from cinder.api.v3.views import groups as views_groups 28from cinder import exception 29from cinder import group as group_api 30from cinder.i18n import _ 31from cinder import rpc 32from cinder.volume import group_types 33 34LOG = logging.getLogger(__name__) 35 36 37class GroupsController(wsgi.Controller): 38 """The groups API controller for the OpenStack API.""" 39 40 _view_builder_class = views_groups.ViewBuilder 41 42 def __init__(self): 43 self.group_api = group_api.API() 44 super(GroupsController, self).__init__() 45 46 def _check_default_cgsnapshot_type(self, group_type_id): 47 if group_types.is_default_cgsnapshot_type(group_type_id): 48 msg = _("Group_type %(group_type)s is reserved for migrating " 49 "CGs to groups. Migrated group can only be operated by " 50 "CG APIs.") % {'group_type': group_type_id} 51 raise exc.HTTPBadRequest(explanation=msg) 52 53 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 54 def show(self, req, id): 55 """Return data about the given group.""" 56 LOG.debug('show called for member %s', id) 57 context = req.environ['cinder.context'] 58 59 # Not found exception will be handled at the wsgi level 60 group = self.group_api.get( 61 context, 62 group_id=id) 63 64 self._check_default_cgsnapshot_type(group.group_type_id) 65 66 return self._view_builder.detail(req, group) 67 68 @wsgi.Controller.api_version(mv.GROUP_VOLUME_RESET_STATUS) 69 @wsgi.action("reset_status") 70 def reset_status(self, req, id, body): 71 return self._reset_status(req, id, body) 72 73 def _reset_status(self, req, id, body): 74 """Reset status on generic group.""" 75 76 context = req.environ['cinder.context'] 77 try: 78 status = body['reset_status']['status'].lower() 79 except (TypeError, KeyError): 80 raise exc.HTTPBadRequest(explanation=_("Must specify 'status'")) 81 82 LOG.debug("Updating group '%(id)s' with " 83 "'%(update)s'", {'id': id, 84 'update': status}) 85 try: 86 notifier = rpc.get_notifier('groupStatusUpdate') 87 notifier.info(context, 'groups.reset_status.start', 88 {'id': id, 89 'update': status}) 90 group = self.group_api.get(context, id) 91 92 self.group_api.reset_status(context, group, status) 93 notifier.info(context, 'groups.reset_status.end', 94 {'id': id, 95 'update': status}) 96 except exception.GroupNotFound as error: 97 # Not found exception will be handled at the wsgi level 98 notifier.error(context, 'groups.reset_status', 99 {'error_message': error.msg, 100 'id': id}) 101 raise 102 except exception.InvalidGroupStatus as error: 103 notifier.error(context, 'groups.reset_status', 104 {'error_message': error.msg, 105 'id': id}) 106 raise exc.HTTPBadRequest(explanation=error.msg) 107 return webob.Response(status_int=http_client.ACCEPTED) 108 109 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 110 @wsgi.action("delete") 111 def delete_group(self, req, id, body): 112 return self._delete(req, id, body) 113 114 def _delete(self, req, id, body): 115 """Delete a group.""" 116 LOG.debug('delete called for group %s', id) 117 context = req.environ['cinder.context'] 118 del_vol = False 119 if body: 120 self.assert_valid_body(body, 'delete') 121 122 grp_body = body['delete'] 123 try: 124 del_vol = strutils.bool_from_string( 125 grp_body.get('delete-volumes', False), 126 strict=True) 127 except ValueError: 128 msg = (_("Invalid value '%s' for delete-volumes flag.") 129 % del_vol) 130 raise exc.HTTPBadRequest(explanation=msg) 131 132 LOG.info('Delete group with id: %s', id, 133 context=context) 134 135 try: 136 group = self.group_api.get(context, id) 137 self._check_default_cgsnapshot_type(group.group_type_id) 138 self.group_api.delete(context, group, del_vol) 139 except exception.GroupNotFound: 140 # Not found exception will be handled at the wsgi level 141 raise 142 except exception.InvalidGroup as error: 143 raise exc.HTTPBadRequest(explanation=error.msg) 144 145 return webob.Response(status_int=http_client.ACCEPTED) 146 147 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 148 def index(self, req): 149 """Returns a summary list of groups.""" 150 return self._get_groups(req, is_detail=False) 151 152 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 153 def detail(self, req): 154 """Returns a detailed list of groups.""" 155 return self._get_groups(req, is_detail=True) 156 157 def _get_groups(self, req, is_detail): 158 """Returns a list of groups through view builder.""" 159 context = req.environ['cinder.context'] 160 filters = req.params.copy() 161 api_version = req.api_version_request 162 marker, limit, offset = common.get_pagination_params(filters) 163 sort_keys, sort_dirs = common.get_sort_params(filters) 164 165 filters.pop('list_volume', None) 166 if api_version.matches(mv.RESOURCE_FILTER): 167 support_like = (True if api_version.matches( 168 mv.LIKE_FILTER) else False) 169 common.reject_invalid_filters(context, filters, 'group', 170 support_like) 171 172 groups = self.group_api.get_all( 173 context, filters=filters, marker=marker, limit=limit, 174 offset=offset, sort_keys=sort_keys, sort_dirs=sort_dirs) 175 176 new_groups = [] 177 for grp in groups: 178 try: 179 # Only show groups not migrated from CGs 180 self._check_default_cgsnapshot_type(grp.group_type_id) 181 new_groups.append(grp) 182 except exc.HTTPBadRequest: 183 # Skip migrated group 184 pass 185 186 if is_detail: 187 groups = self._view_builder.detail_list( 188 req, new_groups) 189 else: 190 groups = self._view_builder.summary_list( 191 req, new_groups) 192 return groups 193 194 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 195 @wsgi.response(http_client.ACCEPTED) 196 def create(self, req, body): 197 """Create a new group.""" 198 LOG.debug('Creating new group %s', body) 199 self.assert_valid_body(body, 'group') 200 201 context = req.environ['cinder.context'] 202 group = body['group'] 203 self.validate_name_and_description(group) 204 name = group.get('name') 205 description = group.get('description') 206 group_type = group.get('group_type') 207 if not group_type: 208 msg = _("group_type must be provided to create " 209 "group %(name)s.") % {'name': name} 210 raise exc.HTTPBadRequest(explanation=msg) 211 if not uuidutils.is_uuid_like(group_type): 212 req_group_type = group_types.get_group_type_by_name(context, 213 group_type) 214 group_type = req_group_type['id'] 215 self._check_default_cgsnapshot_type(group_type) 216 volume_types = group.get('volume_types') 217 if not volume_types: 218 msg = _("volume_types must be provided to create " 219 "group %(name)s.") % {'name': name} 220 raise exc.HTTPBadRequest(explanation=msg) 221 availability_zone = group.get('availability_zone') 222 223 LOG.info("Creating group %(name)s.", 224 {'name': name}, 225 context=context) 226 227 try: 228 new_group = self.group_api.create( 229 context, name, description, group_type, volume_types, 230 availability_zone=availability_zone) 231 except (exception.Invalid, exception.ObjectActionError) as error: 232 raise exc.HTTPBadRequest(explanation=error.msg) 233 except exception.NotFound: 234 # Not found exception will be handled at the wsgi level 235 raise 236 237 retval = self._view_builder.summary(req, new_group) 238 return retval 239 240 @wsgi.Controller.api_version(mv.GROUP_SNAPSHOTS) 241 @wsgi.action("create-from-src") 242 @wsgi.response(http_client.ACCEPTED) 243 def create_from_src(self, req, body): 244 """Create a new group from a source. 245 246 The source can be a group snapshot or a group. Note that 247 this does not require group_type and volume_types as the 248 "create" API above. 249 """ 250 LOG.debug('Creating new group %s.', body) 251 self.assert_valid_body(body, 'create-from-src') 252 253 context = req.environ['cinder.context'] 254 group = body['create-from-src'] 255 self.validate_name_and_description(group) 256 name = group.get('name', None) 257 description = group.get('description', None) 258 group_snapshot_id = group.get('group_snapshot_id', None) 259 source_group_id = group.get('source_group_id', None) 260 if not group_snapshot_id and not source_group_id: 261 msg = (_("Either 'group_snapshot_id' or 'source_group_id' must be " 262 "provided to create group %(name)s from source.") 263 % {'name': name}) 264 raise exc.HTTPBadRequest(explanation=msg) 265 266 if group_snapshot_id and source_group_id: 267 msg = _("Cannot provide both 'group_snapshot_id' and " 268 "'source_group_id' to create group %(name)s from " 269 "source.") % {'name': name} 270 raise exc.HTTPBadRequest(explanation=msg) 271 272 group_type_id = None 273 if group_snapshot_id: 274 LOG.info("Creating group %(name)s from group_snapshot " 275 "%(snap)s.", 276 {'name': name, 'snap': group_snapshot_id}, 277 context=context) 278 grp_snap = self.group_api.get_group_snapshot(context, 279 group_snapshot_id) 280 group_type_id = grp_snap.group_type_id 281 elif source_group_id: 282 LOG.info("Creating group %(name)s from " 283 "source group %(source_group_id)s.", 284 {'name': name, 'source_group_id': source_group_id}, 285 context=context) 286 source_group = self.group_api.get(context, source_group_id) 287 group_type_id = source_group.group_type_id 288 289 self._check_default_cgsnapshot_type(group_type_id) 290 291 try: 292 new_group = self.group_api.create_from_src( 293 context, name, description, group_snapshot_id, source_group_id) 294 except exception.InvalidGroup as error: 295 raise exc.HTTPBadRequest(explanation=error.msg) 296 except (exception.GroupNotFound, exception.GroupSnapshotNotFound): 297 # Not found exception will be handled at the wsgi level 298 raise 299 except exception.CinderException as error: 300 raise exc.HTTPBadRequest(explanation=error.msg) 301 302 retval = self._view_builder.summary(req, new_group) 303 return retval 304 305 @wsgi.Controller.api_version(mv.GROUP_VOLUME) 306 def update(self, req, id, body): 307 """Update the group. 308 309 Expected format of the input parameter 'body': 310 311 .. code-block:: json 312 313 { 314 "group": 315 { 316 "name": "my_group", 317 "description": "My group", 318 "add_volumes": "volume-uuid-1,volume-uuid-2,...", 319 "remove_volumes": "volume-uuid-8,volume-uuid-9,..." 320 } 321 } 322 323 """ 324 LOG.debug('Update called for group %s.', id) 325 326 if not body: 327 msg = _("Missing request body.") 328 raise exc.HTTPBadRequest(explanation=msg) 329 330 self.assert_valid_body(body, 'group') 331 context = req.environ['cinder.context'] 332 333 group = body.get('group') 334 self.validate_name_and_description(group) 335 name = group.get('name') 336 description = group.get('description') 337 add_volumes = group.get('add_volumes') 338 remove_volumes = group.get('remove_volumes') 339 340 # Allow name or description to be changed to an empty string ''. 341 if (name is None and description is None and not add_volumes 342 and not remove_volumes): 343 msg = _("Name, description, add_volumes, and remove_volumes " 344 "can not be all empty in the request body.") 345 raise exc.HTTPBadRequest(explanation=msg) 346 347 LOG.info("Updating group %(id)s with name %(name)s " 348 "description: %(description)s add_volumes: " 349 "%(add_volumes)s remove_volumes: %(remove_volumes)s.", 350 {'id': id, 'name': name, 351 'description': description, 352 'add_volumes': add_volumes, 353 'remove_volumes': remove_volumes}, 354 context=context) 355 356 try: 357 group = self.group_api.get(context, id) 358 self._check_default_cgsnapshot_type(group.group_type_id) 359 self.group_api.update( 360 context, group, name, description, 361 add_volumes, remove_volumes) 362 except exception.GroupNotFound: 363 # Not found exception will be handled at the wsgi level 364 raise 365 except exception.InvalidGroup as error: 366 raise exc.HTTPBadRequest(explanation=error.msg) 367 368 return webob.Response(status_int=http_client.ACCEPTED) 369 370 @wsgi.Controller.api_version(mv.GROUP_REPLICATION) 371 @wsgi.action("enable_replication") 372 def enable_replication(self, req, id, body): 373 """Enables replications for a group.""" 374 context = req.environ['cinder.context'] 375 if body: 376 self.assert_valid_body(body, 'enable_replication') 377 378 LOG.info('Enable replication group with id: %s.', id, 379 context=context) 380 381 try: 382 group = self.group_api.get(context, id) 383 self.group_api.enable_replication(context, group) 384 # Not found exception will be handled at the wsgi level 385 except (exception.InvalidGroup, exception.InvalidGroupType, 386 exception.InvalidVolume, exception.InvalidVolumeType) as error: 387 raise exc.HTTPBadRequest(explanation=error.msg) 388 389 return webob.Response(status_int=202) 390 391 @wsgi.Controller.api_version(mv.GROUP_REPLICATION) 392 @wsgi.action("disable_replication") 393 def disable_replication(self, req, id, body): 394 """Disables replications for a group.""" 395 context = req.environ['cinder.context'] 396 if body: 397 self.assert_valid_body(body, 'disable_replication') 398 399 LOG.info('Disable replication group with id: %s.', id, 400 context=context) 401 402 try: 403 group = self.group_api.get(context, id) 404 self.group_api.disable_replication(context, group) 405 # Not found exception will be handled at the wsgi level 406 except (exception.InvalidGroup, exception.InvalidGroupType, 407 exception.InvalidVolume, exception.InvalidVolumeType) as error: 408 raise exc.HTTPBadRequest(explanation=error.msg) 409 410 return webob.Response(status_int=202) 411 412 @wsgi.Controller.api_version(mv.GROUP_REPLICATION) 413 @wsgi.action("failover_replication") 414 def failover_replication(self, req, id, body): 415 """Fails over replications for a group.""" 416 context = req.environ['cinder.context'] 417 if body: 418 self.assert_valid_body(body, 'failover_replication') 419 420 grp_body = body['failover_replication'] 421 try: 422 allow_attached = strutils.bool_from_string( 423 grp_body.get('allow_attached_volume', False), 424 strict=True) 425 except ValueError: 426 msg = (_("Invalid value '%s' for allow_attached_volume flag.") 427 % grp_body) 428 raise exc.HTTPBadRequest(explanation=msg) 429 secondary_backend_id = grp_body.get('secondary_backend_id') 430 431 LOG.info('Failover replication group with id: %s.', id, 432 context=context) 433 434 try: 435 group = self.group_api.get(context, id) 436 self.group_api.failover_replication(context, group, allow_attached, 437 secondary_backend_id) 438 # Not found exception will be handled at the wsgi level 439 except (exception.InvalidGroup, exception.InvalidGroupType, 440 exception.InvalidVolume, exception.InvalidVolumeType) as error: 441 raise exc.HTTPBadRequest(explanation=error.msg) 442 443 return webob.Response(status_int=202) 444 445 @wsgi.Controller.api_version(mv.GROUP_REPLICATION) 446 @wsgi.action("list_replication_targets") 447 def list_replication_targets(self, req, id, body): 448 """List replication targets for a group.""" 449 context = req.environ['cinder.context'] 450 if body: 451 self.assert_valid_body(body, 'list_replication_targets') 452 453 LOG.info('List replication targets for group with id: %s.', id, 454 context=context) 455 456 # Not found exception will be handled at the wsgi level 457 group = self.group_api.get(context, id) 458 replication_targets = self.group_api.list_replication_targets( 459 context, group) 460 461 return replication_targets 462 463 464def create_resource(): 465 return wsgi.Resource(GroupsController()) 466