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