1# Copyright (c) 2013 OpenStack Foundation 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16"""Volume interface (v2 extension).""" 17 18from cinderclient.apiclient import base as common_base 19from cinderclient import base 20 21 22class Volume(base.Resource): 23 """A volume is an extra block level storage to the OpenStack instances.""" 24 def __repr__(self): 25 return "<Volume: %s>" % self.id 26 27 def delete(self, cascade=False): 28 """Delete this volume.""" 29 return self.manager.delete(self, cascade=cascade) 30 31 def update(self, **kwargs): 32 """Update the name or description for this volume.""" 33 return self.manager.update(self, **kwargs) 34 35 def attach(self, instance_uuid, mountpoint, mode='rw', host_name=None): 36 """Inform Cinder that the given volume is attached to the given instance. 37 38 Calling this method will not actually ask Cinder to attach 39 a volume, but to mark it on the DB as attached. If the volume 40 is not actually attached to the given instance, inconsistent 41 data will result. 42 43 The right flow of calls is : 44 1- call reserve 45 2- call initialize_connection 46 3- call attach 47 48 :param instance_uuid: uuid of the attaching instance. 49 :param mountpoint: mountpoint on the attaching instance or host. 50 :param mode: the access mode. 51 :param host_name: name of the attaching host. 52 """ 53 return self.manager.attach(self, instance_uuid, mountpoint, mode, 54 host_name) 55 56 def detach(self): 57 """Inform Cinder that the given volume is detached from the given instance. 58 59 Calling this method will not actually ask Cinder to detach 60 a volume, but to mark it on the DB as detached. If the volume 61 is not actually detached from the given instance, inconsistent 62 data will result. 63 64 The right flow of calls is : 65 1- call reserve 66 2- call initialize_connection 67 3- call detach 68 """ 69 return self.manager.detach(self) 70 71 def reserve(self, volume): 72 """Reserve this volume.""" 73 return self.manager.reserve(self) 74 75 def unreserve(self, volume): 76 """Unreserve this volume.""" 77 return self.manager.unreserve(self) 78 79 def begin_detaching(self, volume): 80 """Begin detaching volume.""" 81 return self.manager.begin_detaching(self) 82 83 def roll_detaching(self, volume): 84 """Roll detaching volume.""" 85 return self.manager.roll_detaching(self) 86 87 def initialize_connection(self, volume, connector): 88 """Initialize a volume connection. 89 90 :param connector: connector dict from nova. 91 """ 92 return self.manager.initialize_connection(self, connector) 93 94 def terminate_connection(self, volume, connector): 95 """Terminate a volume connection. 96 97 :param connector: connector dict from nova. 98 """ 99 return self.manager.terminate_connection(self, connector) 100 101 def set_metadata(self, volume, metadata): 102 """Set or Append metadata to a volume. 103 104 :param volume : The :class: `Volume` to set metadata on 105 :param metadata: A dict of key/value pairs to set 106 """ 107 return self.manager.set_metadata(self, metadata) 108 109 def set_image_metadata(self, volume, metadata): 110 """Set a volume's image metadata. 111 112 :param volume : The :class: `Volume` to set metadata on 113 :param metadata: A dict of key/value pairs to set 114 """ 115 return self.manager.set_image_metadata(self, volume, metadata) 116 117 def delete_image_metadata(self, volume, keys): 118 """Delete specified keys from volume's image metadata. 119 120 :param volume: The :class:`Volume`. 121 :param keys: A list of keys to be removed. 122 """ 123 return self.manager.delete_image_metadata(self, volume, keys) 124 125 def show_image_metadata(self, volume): 126 """Show a volume's image metadata. 127 128 :param volume : The :class: `Volume` where the image metadata 129 associated. 130 """ 131 return self.manager.show_image_metadata(self) 132 133 def upload_to_image(self, force, image_name, container_format, 134 disk_format, visibility=None, 135 protected=None): 136 """Upload a volume to image service as an image. 137 138 :param force: Boolean to enables or disables upload of a volume that 139 is attached to an instance. 140 :param image_name: The new image name. 141 :param container_format: Container format type. 142 :param disk_format: Disk format type. 143 :param visibility: The accessibility of image (allowed for 144 3.1-latest). 145 :param protected: Boolean to decide whether prevents image from being 146 deleted (allowed for 3.1-latest). 147 """ 148 return self.manager.upload_to_image(self, force, image_name, 149 container_format, disk_format) 150 151 def force_delete(self): 152 """Delete the specified volume ignoring its current state. 153 154 :param volume: The UUID of the volume to force-delete. 155 """ 156 return self.manager.force_delete(self) 157 158 def reset_state(self, state, attach_status=None, migration_status=None): 159 """Update the volume with the provided state. 160 161 :param state: The state of the volume to set. 162 :param attach_status: The attach_status of the volume to be set, 163 or None to keep the current status. 164 :param migration_status: The migration_status of the volume to be set, 165 or None to keep the current status. 166 """ 167 return self.manager.reset_state(self, state, attach_status, 168 migration_status) 169 170 def extend(self, volume, new_size): 171 """Extend the size of the specified volume. 172 173 :param volume: The UUID of the volume to extend 174 :param new_size: The desired size to extend volume to. 175 """ 176 return self.manager.extend(self, new_size) 177 178 def migrate_volume(self, host, force_host_copy, lock_volume): 179 """Migrate the volume to a new host.""" 180 return self.manager.migrate_volume(self, host, force_host_copy, 181 lock_volume) 182 183 def retype(self, volume_type, policy): 184 """Change a volume's type.""" 185 return self.manager.retype(self, volume_type, policy) 186 187 def update_all_metadata(self, metadata): 188 """Update all metadata of this volume.""" 189 return self.manager.update_all_metadata(self, metadata) 190 191 def update_readonly_flag(self, volume, read_only): 192 """Update the read-only access mode flag of the specified volume. 193 194 :param volume: The UUID of the volume to update. 195 :param read_only: The value to indicate whether to update volume to 196 read-only access mode. 197 """ 198 return self.manager.update_readonly_flag(self, read_only) 199 200 def manage(self, host, ref, name=None, description=None, 201 volume_type=None, availability_zone=None, metadata=None, 202 bootable=False): 203 """Manage an existing volume.""" 204 return self.manager.manage(host=host, ref=ref, name=name, 205 description=description, 206 volume_type=volume_type, 207 availability_zone=availability_zone, 208 metadata=metadata, bootable=bootable) 209 210 def list_manageable(self, host, detailed=True, marker=None, limit=None, 211 offset=None, sort=None): 212 return self.manager.list_manageable(host, detailed=detailed, 213 marker=marker, limit=limit, 214 offset=offset, sort=sort) 215 216 def unmanage(self, volume): 217 """Unmanage a volume.""" 218 return self.manager.unmanage(volume) 219 220 def get_pools(self, detail): 221 """Show pool information for backends.""" 222 return self.manager.get_pools(detail) 223 224 225class VolumeManager(base.ManagerWithFind): 226 """Manage :class:`Volume` resources.""" 227 resource_class = Volume 228 229 def create(self, size, consistencygroup_id=None, 230 snapshot_id=None, 231 source_volid=None, name=None, description=None, 232 volume_type=None, user_id=None, 233 project_id=None, availability_zone=None, 234 metadata=None, imageRef=None, scheduler_hints=None): 235 """Create a volume. 236 237 :param size: Size of volume in GB 238 :param consistencygroup_id: ID of the consistencygroup 239 :param snapshot_id: ID of the snapshot 240 :param name: Name of the volume 241 :param description: Description of the volume 242 :param volume_type: Type of volume 243 :param user_id: User id derived from context (IGNORED) 244 :param project_id: Project id derived from context (IGNORED) 245 :param availability_zone: Availability Zone to use 246 :param metadata: Optional metadata to set on volume creation 247 :param imageRef: reference to an image stored in glance 248 :param source_volid: ID of source volume to clone from 249 :param scheduler_hints: (optional extension) arbitrary key-value pairs 250 specified by the client to help boot an instance 251 :rtype: :class:`Volume` 252 """ 253 if metadata is None: 254 volume_metadata = {} 255 else: 256 volume_metadata = metadata 257 258 body = {'volume': {'size': size, 259 'consistencygroup_id': consistencygroup_id, 260 'snapshot_id': snapshot_id, 261 'name': name, 262 'description': description, 263 'volume_type': volume_type, 264 'availability_zone': availability_zone, 265 'metadata': volume_metadata, 266 'imageRef': imageRef, 267 'source_volid': source_volid, 268 }} 269 270 if scheduler_hints: 271 body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints 272 273 return self._create('/volumes', body, 'volume') 274 275 def get(self, volume_id): 276 """Get a volume. 277 278 :param volume_id: The ID of the volume to get. 279 :rtype: :class:`Volume` 280 """ 281 return self._get("/volumes/%s" % volume_id, "volume") 282 283 def list(self, detailed=True, search_opts=None, marker=None, limit=None, 284 sort=None): 285 """Lists all volumes. 286 287 :param detailed: Whether to return detailed volume info. 288 :param search_opts: Search options to filter out volumes. 289 :param marker: Begin returning volumes that appear later in the volume 290 list than that represented by this volume id. 291 :param limit: Maximum number of volumes to return. 292 :param sort: Sort information 293 :rtype: list of :class:`Volume` 294 """ 295 296 resource_type = "volumes" 297 url = self._build_list_url(resource_type, detailed=detailed, 298 search_opts=search_opts, marker=marker, 299 limit=limit, sort=sort) 300 return self._list(url, resource_type, limit=limit) 301 302 def delete(self, volume, cascade=False): 303 """Delete a volume. 304 305 :param volume: The :class:`Volume` to delete. 306 :param cascade: Also delete dependent snapshots. 307 """ 308 309 loc = "/volumes/%s" % base.getid(volume) 310 311 if cascade: 312 loc += '?cascade=True' 313 314 return self._delete(loc) 315 316 def update(self, volume, **kwargs): 317 """Update the name or description for a volume. 318 319 :param volume: The :class:`Volume` to update. 320 """ 321 if not kwargs: 322 return 323 324 body = {"volume": kwargs} 325 326 return self._update("/volumes/%s" % base.getid(volume), body) 327 328 def _action(self, action, volume, info=None, **kwargs): 329 """Perform a volume "action." 330 331 :returns: tuple (response, body) 332 """ 333 body = {action: info} 334 self.run_hooks('modify_body_for_action', body, **kwargs) 335 url = '/volumes/%s/action' % base.getid(volume) 336 resp, body = self.api.client.post(url, body=body) 337 return common_base.TupleWithMeta((resp, body), resp) 338 339 def attach(self, volume, instance_uuid, mountpoint, mode='rw', 340 host_name=None): 341 """Set attachment metadata. 342 343 :param volume: The :class:`Volume` (or its ID) 344 you would like to attach. 345 :param instance_uuid: uuid of the attaching instance. 346 :param mountpoint: mountpoint on the attaching instance or host. 347 :param mode: the access mode. 348 :param host_name: name of the attaching host. 349 """ 350 body = {'mountpoint': mountpoint, 'mode': mode} 351 if instance_uuid is not None: 352 body.update({'instance_uuid': instance_uuid}) 353 if host_name is not None: 354 body.update({'host_name': host_name}) 355 return self._action('os-attach', volume, body) 356 357 def detach(self, volume, attachment_uuid=None): 358 """Clear attachment metadata. 359 360 :param volume: The :class:`Volume` (or its ID) 361 you would like to detach. 362 :param attachment_uuid: The uuid of the volume attachment. 363 """ 364 return self._action('os-detach', volume, 365 {'attachment_id': attachment_uuid}) 366 367 def reserve(self, volume): 368 """Reserve this volume. 369 370 :param volume: The :class:`Volume` (or its ID) 371 you would like to reserve. 372 """ 373 return self._action('os-reserve', volume) 374 375 def unreserve(self, volume): 376 """Unreserve this volume. 377 378 :param volume: The :class:`Volume` (or its ID) 379 you would like to unreserve. 380 """ 381 return self._action('os-unreserve', volume) 382 383 def begin_detaching(self, volume): 384 """Begin detaching this volume. 385 386 :param volume: The :class:`Volume` (or its ID) 387 you would like to detach. 388 """ 389 return self._action('os-begin_detaching', volume) 390 391 def roll_detaching(self, volume): 392 """Roll detaching this volume. 393 394 :param volume: The :class:`Volume` (or its ID) 395 you would like to roll detaching. 396 """ 397 return self._action('os-roll_detaching', volume) 398 399 def initialize_connection(self, volume, connector): 400 """Initialize a volume connection. 401 402 :param volume: The :class:`Volume` (or its ID). 403 :param connector: connector dict from nova. 404 """ 405 resp, body = self._action('os-initialize_connection', volume, 406 {'connector': connector}) 407 return common_base.DictWithMeta(body['connection_info'], resp) 408 409 def terminate_connection(self, volume, connector): 410 """Terminate a volume connection. 411 412 :param volume: The :class:`Volume` (or its ID). 413 :param connector: connector dict from nova. 414 """ 415 return self._action('os-terminate_connection', volume, 416 {'connector': connector}) 417 418 def set_metadata(self, volume, metadata): 419 """Update/Set a volumes metadata. 420 421 :param volume: The :class:`Volume`. 422 :param metadata: A list of keys to be set. 423 """ 424 body = {'metadata': metadata} 425 return self._create("/volumes/%s/metadata" % base.getid(volume), 426 body, "metadata") 427 428 def delete_metadata(self, volume, keys): 429 """Delete specified keys from volumes metadata. 430 431 :param volume: The :class:`Volume`. 432 :param keys: A list of keys to be removed. 433 """ 434 response_list = [] 435 for k in keys: 436 resp, body = self._delete("/volumes/%s/metadata/%s" % 437 (base.getid(volume), k)) 438 response_list.append(resp) 439 440 return common_base.ListWithMeta([], response_list) 441 442 def set_image_metadata(self, volume, metadata): 443 """Set a volume's image metadata. 444 445 :param volume: The :class:`Volume`. 446 :param metadata: keys and the values to be set with. 447 :type metadata: dict 448 """ 449 return self._action("os-set_image_metadata", volume, 450 {'metadata': metadata}) 451 452 def delete_image_metadata(self, volume, keys): 453 """Delete specified keys from volume's image metadata. 454 455 :param volume: The :class:`Volume`. 456 :param keys: A list of keys to be removed. 457 """ 458 response_list = [] 459 for key in keys: 460 resp, body = self._action("os-unset_image_metadata", volume, 461 {'key': key}) 462 response_list.append(resp) 463 464 return common_base.ListWithMeta([], response_list) 465 466 def show_image_metadata(self, volume): 467 """Show a volume's image metadata. 468 469 :param volume : The :class: `Volume` where the image metadata 470 associated. 471 """ 472 return self._action("os-show_image_metadata", volume) 473 474 def upload_to_image(self, volume, force, image_name, container_format, 475 disk_format): 476 """Upload volume to image service as image. 477 478 :param volume: The :class:`Volume` to upload. 479 """ 480 return self._action('os-volume_upload_image', 481 volume, 482 {'force': force, 483 'image_name': image_name, 484 'container_format': container_format, 485 'disk_format': disk_format}) 486 487 def force_delete(self, volume): 488 """Delete the specified volume ignoring its current state. 489 490 :param volume: The :class:`Volume` to force-delete. 491 """ 492 return self._action('os-force_delete', base.getid(volume)) 493 494 def reset_state(self, volume, state, attach_status=None, 495 migration_status=None): 496 """Update the provided volume with the provided state. 497 498 :param volume: The :class:`Volume` to set the state. 499 :param state: The state of the volume to be set. 500 :param attach_status: The attach_status of the volume to be set, 501 or None to keep the current status. 502 :param migration_status: The migration_status of the volume to be set, 503 or None to keep the current status. 504 """ 505 body = {'status': state} if state else {} 506 if attach_status: 507 body.update({'attach_status': attach_status}) 508 if migration_status: 509 body.update({'migration_status': migration_status}) 510 return self._action('os-reset_status', volume, body) 511 512 def extend(self, volume, new_size): 513 """Extend the size of the specified volume. 514 515 :param volume: The UUID of the volume to extend. 516 :param new_size: The requested size to extend volume to. 517 """ 518 return self._action('os-extend', 519 base.getid(volume), 520 {'new_size': new_size}) 521 522 def get_encryption_metadata(self, volume_id): 523 """ 524 Retrieve the encryption metadata from the desired volume. 525 526 :param volume_id: the id of the volume to query 527 :return: a dictionary of volume encryption metadata 528 """ 529 metadata = self._get("/volumes/%s/encryption" % volume_id) 530 return common_base.DictWithMeta(metadata._info, metadata.request_ids) 531 532 def migrate_volume(self, volume, host, force_host_copy, lock_volume): 533 """Migrate volume to new host. 534 535 :param volume: The :class:`Volume` to migrate 536 :param host: The destination host 537 :param force_host_copy: Skip driver optimizations 538 :param lock_volume: Lock the volume and guarantee the migration 539 to finish 540 """ 541 return self._action('os-migrate_volume', 542 volume, 543 {'host': host, 'force_host_copy': force_host_copy, 544 'lock_volume': lock_volume}) 545 546 def migrate_volume_completion(self, old_volume, new_volume, error): 547 """Complete the migration from the old volume to the temp new one. 548 549 :param old_volume: The original :class:`Volume` in the migration 550 :param new_volume: The new temporary :class:`Volume` in the migration 551 :param error: Inform of an error to cause migration cleanup 552 """ 553 new_volume_id = base.getid(new_volume) 554 resp, body = self._action('os-migrate_volume_completion', old_volume, 555 {'new_volume': new_volume_id, 556 'error': error}) 557 return common_base.DictWithMeta(body, resp) 558 559 def update_all_metadata(self, volume, metadata): 560 """Update all metadata of a volume. 561 562 :param volume: The :class:`Volume`. 563 :param metadata: A list of keys to be updated. 564 """ 565 body = {'metadata': metadata} 566 return self._update("/volumes/%s/metadata" % base.getid(volume), 567 body) 568 569 def update_readonly_flag(self, volume, flag): 570 return self._action('os-update_readonly_flag', 571 base.getid(volume), 572 {'readonly': flag}) 573 574 def retype(self, volume, volume_type, policy): 575 """Change a volume's type. 576 577 :param volume: The :class:`Volume` to retype 578 :param volume_type: New volume type 579 :param policy: Policy for migration during the retype 580 """ 581 return self._action('os-retype', 582 volume, 583 {'new_type': volume_type, 584 'migration_policy': policy}) 585 586 def set_bootable(self, volume, flag): 587 return self._action('os-set_bootable', 588 base.getid(volume), 589 {'bootable': flag}) 590 591 def manage(self, host, ref, name=None, description=None, 592 volume_type=None, availability_zone=None, metadata=None, 593 bootable=False): 594 """Manage an existing volume.""" 595 body = {'volume': {'host': host, 596 'ref': ref, 597 'name': name, 598 'description': description, 599 'volume_type': volume_type, 600 'availability_zone': availability_zone, 601 'metadata': metadata, 602 'bootable': bootable 603 }} 604 return self._create('/os-volume-manage', body, 'volume') 605 606 def list_manageable(self, host, detailed=True, marker=None, limit=None, 607 offset=None, sort=None): 608 url = self._build_list_url("os-volume-manage", detailed=detailed, 609 search_opts={'host': host}, marker=marker, 610 limit=limit, offset=offset, sort=sort) 611 return self._list(url, "manageable-volumes") 612 613 def unmanage(self, volume): 614 """Unmanage a volume.""" 615 return self._action('os-unmanage', volume, None) 616 617 def get_pools(self, detail): 618 """Show pool information for backends.""" 619 query_string = "" 620 if detail: 621 query_string = "?detail=True" 622 623 return self._get('/scheduler-stats/get_pools%s' % query_string, None) 624