1# Copyright 2012 Nebula, Inc.
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"""
16Views for managing volumes.
17"""
18
19from collections import OrderedDict
20import json
21
22from django import shortcuts
23from django.template.defaultfilters import slugify
24from django.urls import reverse
25from django.urls import reverse_lazy
26from django.utils.decorators import method_decorator
27from django.utils import encoding
28from django.utils.translation import ugettext_lazy as _
29from django.views.decorators.cache import never_cache
30from django.views import generic
31
32from horizon import exceptions
33from horizon import forms
34from horizon import tables
35from horizon import tabs
36from horizon.utils import memoized
37
38from openstack_dashboard.api import cinder
39from openstack_dashboard.api import nova
40from openstack_dashboard import exceptions as dashboard_exception
41from openstack_dashboard.usage import quotas
42from openstack_dashboard.utils import filters
43from openstack_dashboard.utils import futurist_utils
44
45from openstack_dashboard.dashboards.project.volumes \
46    import forms as volume_forms
47from openstack_dashboard.dashboards.project.volumes \
48    import tables as volume_tables
49from openstack_dashboard.dashboards.project.volumes \
50    import tabs as project_tabs
51
52
53class VolumeTableMixIn(object):
54    _has_more_data = False
55    _has_prev_data = False
56
57    def _get_volumes(self, search_opts=None):
58        try:
59            marker, sort_dir = self._get_marker()
60            volumes, self._has_more_data, self._has_prev_data = \
61                cinder.volume_list_paged(self.request, marker=marker,
62                                         search_opts=search_opts,
63                                         sort_dir=sort_dir, paginate=True)
64            return volumes
65        except Exception:
66            exceptions.handle(self.request,
67                              _('Unable to retrieve volume list.'))
68            return []
69
70    def _get_instances(self, search_opts=None):
71        try:
72            # TODO(tsufiev): we should pass attached_instance_ids to
73            # nova.server_list as soon as Nova API allows for this
74            instances, has_more = nova.server_list(self.request,
75                                                   search_opts=search_opts)
76            return instances
77        except Exception:
78            exceptions.handle(self.request,
79                              _("Unable to retrieve volume/instance "
80                                "attachment information"))
81            return []
82
83    def _get_volumes_ids_with_snapshots(self, search_opts=None):
84        try:
85            volume_ids = []
86            snapshots = cinder.volume_snapshot_list(
87                self.request, search_opts=search_opts)
88            if snapshots:
89                # extract out the volume ids
90                volume_ids = set(s.volume_id for s in snapshots)
91        except Exception:
92            exceptions.handle(self.request,
93                              _("Unable to retrieve snapshot list."))
94
95        return volume_ids
96
97    def _get_attached_instance_ids(self, volumes):
98        attached_instance_ids = []
99        for volume in volumes:
100            for att in volume.attachments:
101                server_id = att.get('server_id', None)
102                if server_id is not None:
103                    attached_instance_ids.append(server_id)
104        return attached_instance_ids
105
106    def _get_groups(self, volumes, search_opts=None):
107        needs_group = False
108        if volumes and hasattr(volumes[0], 'group_id'):
109            needs_group = True
110        if needs_group:
111            try:
112                groups_list = cinder.group_list(self.request,
113                                                search_opts=search_opts)
114                groups = dict((g.id, g) for g in groups_list)
115            except Exception:
116                groups = {}
117                exceptions.handle(self.request,
118                                  _("Unable to retrieve volume groups"))
119        for volume in volumes:
120            if needs_group:
121                volume.group = groups.get(volume.group_id)
122            else:
123                volume.group = None
124
125    # set attachment string and if volume has snapshots
126    def _set_volume_attributes(self,
127                               volumes,
128                               instances,
129                               volume_ids_with_snapshots):
130        instances = OrderedDict([(inst.id, inst) for inst in instances])
131        for volume in volumes:
132            if volume_ids_with_snapshots:
133                if volume.id in volume_ids_with_snapshots:
134                    setattr(volume, 'has_snapshot', True)
135            if instances:
136                for att in volume.attachments:
137                    server_id = att.get('server_id', None)
138                    att['instance'] = instances.get(server_id, None)
139
140
141class VolumesView(tables.PagedTableMixin, VolumeTableMixIn,
142                  tables.DataTableView):
143    table_class = volume_tables.VolumesTable
144    page_title = _("Volumes")
145
146    def get_data(self):
147        volumes = []
148        attached_instance_ids = []
149        instances = []
150        volume_ids_with_snapshots = []
151
152        def _task_get_volumes():
153            volumes.extend(self._get_volumes())
154            attached_instance_ids.extend(
155                self._get_attached_instance_ids(volumes))
156
157        def _task_get_instances():
158            # As long as Nova API does not allow passing attached_instance_ids
159            # to nova.server_list, this call can be forged to pass anything
160            # != None
161            instances.extend(self._get_instances())
162
163            # In volumes tab we don't need to know about the assignment
164            # instance-image, therefore fixing it to an empty value
165            for instance in instances:
166                if hasattr(instance, 'image'):
167                    if isinstance(instance.image, dict):
168                        instance.image['name'] = "-"
169
170        def _task_get_volumes_snapshots():
171            volume_ids_with_snapshots.extend(
172                self._get_volumes_ids_with_snapshots())
173
174        futurist_utils.call_functions_parallel(
175            _task_get_volumes,
176            _task_get_instances,
177            _task_get_volumes_snapshots)
178
179        self._set_volume_attributes(
180            volumes, instances, volume_ids_with_snapshots)
181        self._get_groups(volumes)
182        return volumes
183
184
185class DetailView(tabs.TabbedTableView):
186    tab_group_class = project_tabs.VolumeDetailTabs
187    template_name = 'horizon/common/_detail.html'
188    page_title = "{{ volume.name|default:volume.id }}"
189
190    def get_context_data(self, **kwargs):
191        context = super().get_context_data(**kwargs)
192        volume, snapshots = self.get_data()
193        table = volume_tables.VolumesTable(self.request)
194        context["volume"] = volume
195        context["url"] = self.get_redirect_url()
196        context["actions"] = table.render_row_actions(volume)
197        choices = volume_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES
198        volume.status_label = filters.get_display_label(choices, volume.status)
199        return context
200
201    def get_search_opts(self, volume):
202        return {'volume_id': volume.id}
203
204    @memoized.memoized_method
205    def get_data(self):
206        try:
207            volume_id = self.kwargs['volume_id']
208            volume = cinder.volume_get(self.request, volume_id)
209            search_opts = self.get_search_opts(volume)
210            snapshots = cinder.volume_snapshot_list(
211                self.request, search_opts=search_opts)
212            if snapshots:
213                setattr(volume, 'has_snapshot', True)
214            for att in volume.attachments:
215                att['instance'] = nova.server_get(self.request,
216                                                  att['server_id'])
217            if getattr(volume, 'group_id', None):
218                volume.group = cinder.group_get(self.request, volume.group_id)
219            else:
220                volume.group = None
221        except Exception:
222            redirect = self.get_redirect_url()
223            exceptions.handle(self.request,
224                              _('Unable to retrieve volume details.'),
225                              redirect=redirect)
226        return volume, snapshots
227
228    def get_redirect_url(self):
229        return reverse('horizon:project:volumes:index')
230
231    def get_tabs(self, request, *args, **kwargs):
232        volume, snapshots = self.get_data()
233        return self.tab_group_class(
234            request, volume=volume, snapshots=snapshots, **kwargs)
235
236
237class CreateView(forms.ModalFormView):
238    form_class = volume_forms.CreateForm
239    template_name = 'project/volumes/create.html'
240    submit_label = _("Create Volume")
241    submit_url = reverse_lazy("horizon:project:volumes:create")
242    success_url = reverse_lazy('horizon:project:volumes:index')
243    page_title = _("Create Volume")
244
245    def get_initial(self):
246        initial = super().get_initial()
247        self.default_vol_type = None
248        try:
249            self.default_vol_type = cinder.volume_type_default(self.request)
250            initial['type'] = self.default_vol_type.name
251        except dashboard_exception.NOT_FOUND:
252            pass
253        return initial
254
255    def get_context_data(self, **kwargs):
256        context = super().get_context_data(**kwargs)
257        try:
258            context['usages'] = quotas.tenant_quota_usages(
259                self.request, targets=('volumes', 'gigabytes'))
260            context['volume_types'] = self._get_volume_types()
261        except Exception:
262            exceptions.handle(self.request)
263        return context
264
265    def _get_volume_types(self):
266        volume_types = []
267        try:
268            volume_types = cinder.volume_type_list(self.request)
269        except Exception:
270            exceptions.handle(self.request,
271                              _('Unable to retrieve volume type list.'))
272
273        # check if we have default volume type so we can present the
274        # description of no volume type differently
275        no_type_description = None
276        if self.default_vol_type is None:
277            message = \
278                _("If \"No volume type\" is selected, the volume will be "
279                  "created without a volume type.")
280
281            no_type_description = encoding.force_text(message)
282
283        type_descriptions = [{'name': '',
284                              'description': no_type_description}] + \
285                            [{'name': type.name,
286                              'description': getattr(type, "description", "")}
287                             for type in volume_types]
288
289        return json.dumps(type_descriptions)
290
291
292class ExtendView(forms.ModalFormView):
293    form_class = volume_forms.ExtendForm
294    template_name = 'project/volumes/extend.html'
295    submit_label = _("Extend Volume")
296    submit_url = "horizon:project:volumes:extend"
297    success_url = reverse_lazy("horizon:project:volumes:index")
298    page_title = _("Extend Volume")
299
300    def get_object(self):
301        if not hasattr(self, "_object"):
302            volume_id = self.kwargs['volume_id']
303            try:
304                self._object = cinder.volume_get(self.request, volume_id)
305            except Exception:
306                self._object = None
307                exceptions.handle(self.request,
308                                  _('Unable to retrieve volume information.'))
309        return self._object
310
311    def get_context_data(self, **kwargs):
312        context = super().get_context_data(**kwargs)
313        context['volume'] = self.get_object()
314        args = (self.kwargs['volume_id'],)
315        context['submit_url'] = reverse(self.submit_url, args=args)
316        try:
317            usages = quotas.tenant_quota_usages(self.request,
318                                                targets=('gigabytes',))
319            usages.tally('gigabytes', - context['volume'].size)
320            context['usages'] = usages
321        except Exception:
322            exceptions.handle(self.request)
323        return context
324
325    def get_initial(self):
326        volume = self.get_object()
327        return {'id': self.kwargs['volume_id'],
328                'name': volume.name,
329                'orig_size': volume.size}
330
331
332class CreateSnapshotView(forms.ModalFormView):
333    form_class = volume_forms.CreateSnapshotForm
334    template_name = 'project/volumes/create_snapshot.html'
335    submit_url = "horizon:project:volumes:create_snapshot"
336    success_url = reverse_lazy('horizon:project:snapshots:index')
337    page_title = _("Create Volume Snapshot")
338
339    def get_context_data(self, **kwargs):
340        context = super().get_context_data(**kwargs)
341        context['volume_id'] = self.kwargs['volume_id']
342        args = (self.kwargs['volume_id'],)
343        context['submit_url'] = reverse(self.submit_url, args=args)
344        try:
345            volume = cinder.volume_get(self.request, context['volume_id'])
346            if (volume.status == 'in-use'):
347                context['attached'] = True
348                context['form'].set_warning(_("This volume is currently "
349                                              "attached to an instance. "
350                                              "In some cases, creating a "
351                                              "snapshot from an attached "
352                                              "volume can result in a "
353                                              "corrupted snapshot."))
354            context['usages'] = quotas.tenant_quota_usages(
355                self.request, targets=('snapshots', 'gigabytes'))
356        except Exception:
357            exceptions.handle(self.request,
358                              _('Unable to retrieve volume information.'))
359        return context
360
361    def get_initial(self):
362        return {'volume_id': self.kwargs["volume_id"]}
363
364
365class UploadToImageView(forms.ModalFormView):
366    form_class = volume_forms.UploadToImageForm
367    template_name = 'project/volumes/upload_to_image.html'
368    submit_label = _("Upload")
369    submit_url = "horizon:project:volumes:upload_to_image"
370    success_url = reverse_lazy("horizon:project:volumes:index")
371    page_title = _("Upload Volume to Image")
372
373    @memoized.memoized_method
374    def get_data(self):
375        try:
376            volume_id = self.kwargs['volume_id']
377            volume = cinder.volume_get(self.request, volume_id)
378        except Exception:
379            error_message = _(
380                'Unable to retrieve volume information for volume: "%s"') \
381                % volume_id
382            exceptions.handle(self.request,
383                              error_message,
384                              redirect=self.success_url)
385
386        return volume
387
388    def get_context_data(self, **kwargs):
389        context = super().get_context_data(**kwargs)
390        context['volume'] = self.get_data()
391        args = (self.kwargs['volume_id'],)
392        context['submit_url'] = reverse(self.submit_url, args=args)
393        return context
394
395    def get_initial(self):
396        volume = self.get_data()
397
398        return {'id': self.kwargs['volume_id'],
399                'name': volume.name,
400                'status': volume.status}
401
402
403class CreateTransferView(forms.ModalFormView):
404    form_class = volume_forms.CreateTransferForm
405    template_name = 'project/volumes/create_transfer.html'
406    success_url = reverse_lazy('horizon:project:volumes:index')
407    modal_id = "create_volume_transfer_modal"
408    submit_label = _("Create Volume Transfer")
409    submit_url = "horizon:project:volumes:create_transfer"
410    page_title = _("Create Volume Transfer")
411
412    def get_context_data(self, *args, **kwargs):
413        context = super().get_context_data(**kwargs)
414        volume_id = self.kwargs['volume_id']
415        context['volume_id'] = volume_id
416        context['submit_url'] = reverse(self.submit_url, args=[volume_id])
417        return context
418
419    def get_initial(self):
420        return {'volume_id': self.kwargs["volume_id"]}
421
422    def get_form_kwargs(self):
423        kwargs = super().get_form_kwargs()
424        kwargs['next_view'] = ShowTransferView
425        return kwargs
426
427
428class AcceptTransferView(forms.ModalFormView):
429    form_class = volume_forms.AcceptTransferForm
430    template_name = 'project/volumes/accept_transfer.html'
431    success_url = reverse_lazy('horizon:project:volumes:index')
432    modal_id = "accept_volume_transfer_modal"
433    submit_label = _("Accept Volume Transfer")
434    submit_url = reverse_lazy(
435        "horizon:project:volumes:accept_transfer")
436    page_title = _("Accept Volume Transfer")
437
438
439class ShowTransferView(forms.ModalFormView):
440    form_class = volume_forms.ShowTransferForm
441    template_name = 'project/volumes/show_transfer.html'
442    success_url = reverse_lazy('horizon:project:volumes:index')
443    modal_id = "show_volume_transfer_modal"
444    modal_header = _("Volume Transfer")
445    submit_url = "horizon:project:volumes:show_transfer"
446    cancel_label = _("Close")
447    download_label = _("Download transfer credentials")
448    page_title = _("Volume Transfer Details")
449
450    def get_object(self):
451        try:
452            return self._object
453        except AttributeError:
454            transfer_id = self.kwargs['transfer_id']
455            try:
456                self._object = cinder.transfer_get(self.request, transfer_id)
457                return self._object
458            except Exception:
459                exceptions.handle(self.request,
460                                  _('Unable to retrieve volume transfer.'))
461
462    def get_context_data(self, **kwargs):
463        context = super().get_context_data(**kwargs)
464        context['transfer_id'] = self.kwargs['transfer_id']
465        context['auth_key'] = self.kwargs['auth_key']
466        context['download_label'] = self.download_label
467        context['download_url'] = reverse(
468            'horizon:project:volumes:download_transfer_creds',
469            args=[context['transfer_id'], context['auth_key']]
470        )
471        return context
472
473    def get_initial(self):
474        transfer = self.get_object()
475        return {'id': transfer.id,
476                'name': transfer.name,
477                'auth_key': self.kwargs['auth_key']}
478
479
480class UpdateView(forms.ModalFormView):
481    form_class = volume_forms.UpdateForm
482    modal_id = "update_volume_modal"
483    template_name = 'project/volumes/update.html'
484    submit_url = "horizon:project:volumes:update"
485    success_url = reverse_lazy("horizon:project:volumes:index")
486    page_title = _("Edit Volume")
487
488    def get_object(self):
489        if not hasattr(self, "_object"):
490            vol_id = self.kwargs['volume_id']
491            try:
492                self._object = cinder.volume_get(self.request, vol_id)
493            except Exception:
494                msg = _('Unable to retrieve volume.')
495                url = reverse('horizon:project:volumes:index')
496                exceptions.handle(self.request, msg, redirect=url)
497        return self._object
498
499    def get_context_data(self, **kwargs):
500        context = super().get_context_data(**kwargs)
501        context['volume'] = self.get_object()
502        args = (self.kwargs['volume_id'],)
503        context['submit_url'] = reverse(self.submit_url, args=args)
504        return context
505
506    def get_initial(self):
507        volume = self.get_object()
508        return {'volume_id': self.kwargs["volume_id"],
509                'name': volume.name,
510                'description': volume.description,
511                'bootable': volume.is_bootable}
512
513
514class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
515    table_class = volume_tables.AttachmentsTable
516    form_class = volume_forms.AttachForm
517    form_id = "attach_volume_form"
518    modal_id = "attach_volume_modal"
519    template_name = 'project/volumes/attach.html'
520    submit_url = "horizon:project:volumes:attach"
521    success_url = reverse_lazy("horizon:project:volumes:index")
522    page_title = _("Manage Volume Attachments")
523
524    @memoized.memoized_method
525    def get_object(self):
526        volume_id = self.kwargs['volume_id']
527        try:
528            return cinder.volume_get(self.request, volume_id)
529        except Exception:
530            self._object = None
531            exceptions.handle(self.request,
532                              _('Unable to retrieve volume information.'))
533
534    def get_data(self):
535        attachments = []
536        volume = self.get_object()
537        if volume is not None:
538            for att in volume.attachments:
539                att['volume_name'] = getattr(volume, 'name', att['device'])
540                attachments.append(att)
541        return attachments
542
543    def get_initial(self):
544        try:
545            instances, has_more = nova.server_list(self.request)
546        except Exception:
547            instances = []
548            exceptions.handle(self.request,
549                              _("Unable to retrieve attachment information."))
550        return {'volume': self.get_object(),
551                'instances': instances}
552
553    @memoized.memoized_method
554    def get_form(self, **kwargs):
555        form_class = kwargs.get('form_class', self.get_form_class())
556        return super().get_form(form_class)
557
558    def get_context_data(self, **kwargs):
559        context = super().get_context_data(**kwargs)
560        context['form'] = self.get_form()
561        volume = self.get_object()
562        args = (self.kwargs['volume_id'],)
563        context['submit_url'] = reverse(self.submit_url, args=args)
564        if volume and volume.status == 'available':
565            context['show_attach'] = True
566        else:
567            context['show_attach'] = False
568        context['volume'] = volume
569        if self.request.is_ajax():
570            context['hide'] = True
571        return context
572
573    def get(self, request, *args, **kwargs):
574        # Table action handling
575        handled = self.construct_tables()
576        if handled:
577            return handled
578        return self.render_to_response(self.get_context_data(**kwargs))
579
580    def post(self, request, *args, **kwargs):
581        form = self.get_form()
582        if form.is_valid():
583            return self.form_valid(form)
584        return self.get(request, *args, **kwargs)
585
586
587class RetypeView(forms.ModalFormView):
588    form_class = volume_forms.RetypeForm
589    modal_id = "retype_volume_modal"
590    template_name = 'project/volumes/retype.html'
591    submit_label = _("Change Volume Type")
592    submit_url = "horizon:project:volumes:retype"
593    success_url = reverse_lazy("horizon:project:volumes:index")
594    page_title = _("Change Volume Type")
595
596    @memoized.memoized_method
597    def get_data(self):
598        try:
599            volume_id = self.kwargs['volume_id']
600            volume = cinder.volume_get(self.request, volume_id)
601        except Exception:
602            error_message = _(
603                'Unable to retrieve volume information for volume: "%s"') \
604                % volume_id
605            exceptions.handle(self.request,
606                              error_message,
607                              redirect=self.success_url)
608
609        return volume
610
611    def get_context_data(self, **kwargs):
612        context = super().get_context_data(**kwargs)
613        context['volume'] = self.get_data()
614        args = (self.kwargs['volume_id'],)
615        context['submit_url'] = reverse(self.submit_url, args=args)
616        return context
617
618    def get_initial(self):
619        volume = self.get_data()
620
621        return {'id': self.kwargs['volume_id'],
622                'name': volume.name,
623                'volume_type': volume.volume_type}
624
625
626class EncryptionDetailView(generic.TemplateView):
627    template_name = 'project/volumes/encryption_detail.html'
628    page_title = _("Volume Encryption Details: {{ volume.name }}")
629
630    def get_context_data(self, **kwargs):
631        context = super().get_context_data(**kwargs)
632        volume = self.get_volume_data()
633        context["encryption_metadata"] = self.get_encryption_data()
634        context["volume"] = volume
635        context["page_title"] = _("Volume Encryption Details: "
636                                  "%(volume_name)s") % {'volume_name':
637                                                        volume.name}
638        return context
639
640    @memoized.memoized_method
641    def get_encryption_data(self):
642        try:
643            volume_id = self.kwargs['volume_id']
644            self._encryption_metadata = \
645                cinder.volume_get_encryption_metadata(self.request,
646                                                      volume_id)
647        except Exception:
648            redirect = self.get_redirect_url()
649            exceptions.handle(self.request,
650                              _('Unable to retrieve volume encryption '
651                                'details.'),
652                              redirect=redirect)
653        return self._encryption_metadata
654
655    @memoized.memoized_method
656    def get_volume_data(self):
657        try:
658            volume_id = self.kwargs['volume_id']
659            volume = cinder.volume_get(self.request, volume_id)
660        except Exception:
661            redirect = self.get_redirect_url()
662            exceptions.handle(self.request,
663                              _('Unable to retrieve volume details.'),
664                              redirect=redirect)
665        return volume
666
667    def get_redirect_url(self):
668        return reverse('horizon:project:volumes:index')
669
670
671class DownloadTransferCreds(generic.View):
672    @method_decorator(never_cache)
673    def get(self, request, transfer_id, auth_key):
674        try:
675            transfer = cinder.transfer_get(self.request, transfer_id)
676        except Exception:
677            transfer = None
678        context = {'transfer': {
679            'name': getattr(transfer, 'name', ''),
680            'id': transfer_id,
681            'auth_key': auth_key,
682        }}
683        response = shortcuts.render(
684            request,
685            'project/volumes/download_transfer_creds.html',
686            context, content_type='application/text')
687        response['Content-Disposition'] = (
688            'attachment; filename=%s.txt' % slugify(transfer_id))
689        return response
690