summaryrefslogtreecommitdiff
path: root/openstack_dashboard/dashboards/project/volumes/views.py
blob: 3fe3ff2ca4eaf643a42aeda881e9ac77bcbbc9d6 (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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
# Copyright 2012 Nebula, Inc.
#
#    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.

"""
Views for managing volumes.
"""

from collections import OrderedDict
import json

from django import shortcuts
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils import encoding
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views import generic

from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from horizon.utils import memoized

from openstack_dashboard.api import cinder
from openstack_dashboard.api import nova
from openstack_dashboard import exceptions as dashboard_exception
from openstack_dashboard.usage import quotas
from openstack_dashboard.utils import filters
from openstack_dashboard.utils import futurist_utils

from openstack_dashboard.dashboards.project.volumes \
    import forms as volume_forms
from openstack_dashboard.dashboards.project.volumes \
    import tables as volume_tables
from openstack_dashboard.dashboards.project.volumes \
    import tabs as project_tabs


class VolumeTableMixIn(object):
    _has_more_data = False
    _has_prev_data = False

    def _get_volumes(self, search_opts=None):
        try:
            marker, sort_dir = self._get_marker()
            volumes, self._has_more_data, self._has_prev_data = \
                cinder.volume_list_paged(self.request, marker=marker,
                                         search_opts=search_opts,
                                         sort_dir=sort_dir, paginate=True)
            return volumes
        except Exception:
            exceptions.handle(self.request,
                              _('Unable to retrieve volume list.'))
            return []

    def _get_instances(self, search_opts=None):
        try:
            # TODO(tsufiev): we should pass attached_instance_ids to
            # nova.server_list as soon as Nova API allows for this
            instances, has_more = nova.server_list(self.request,
                                                   search_opts=search_opts)
            return instances
        except Exception:
            exceptions.handle(self.request,
                              _("Unable to retrieve volume/instance "
                                "attachment information"))
            return []

    def _get_volumes_ids_with_snapshots(self, search_opts=None):
        try:
            volume_ids = []
            snapshots = cinder.volume_snapshot_list(
                self.request, search_opts=search_opts)
            if snapshots:
                # extract out the volume ids
                volume_ids = set(s.volume_id for s in snapshots)
        except Exception:
            exceptions.handle(self.request,
                              _("Unable to retrieve snapshot list."))

        return volume_ids

    def _get_attached_instance_ids(self, volumes):
        attached_instance_ids = []
        for volume in volumes:
            for att in volume.attachments:
                server_id = att.get('server_id', None)
                if server_id is not None:
                    attached_instance_ids.append(server_id)
        return attached_instance_ids

    def _get_groups(self, volumes, search_opts=None):
        needs_group = False
        if volumes and hasattr(volumes[0], 'group_id'):
            needs_group = True
        if needs_group:
            try:
                groups_list = cinder.group_list(self.request,
                                                search_opts=search_opts)
                groups = dict((g.id, g) for g in groups_list)
            except Exception:
                groups = {}
                exceptions.handle(self.request,
                                  _("Unable to retrieve volume groups"))
        for volume in volumes:
            if needs_group:
                volume.group = groups.get(volume.group_id)
            else:
                volume.group = None

    # set attachment string and if volume has snapshots
    def _set_volume_attributes(self,
                               volumes,
                               instances,
                               volume_ids_with_snapshots):
        instances = OrderedDict([(inst.id, inst) for inst in instances])
        for volume in volumes:
            if volume_ids_with_snapshots:
                if volume.id in volume_ids_with_snapshots:
                    setattr(volume, 'has_snapshot', True)
            if instances:
                for att in volume.attachments:
                    server_id = att.get('server_id', None)
                    att['instance'] = instances.get(server_id, None)


class VolumesView(tables.PagedTableMixin, VolumeTableMixIn,
                  tables.DataTableView):
    table_class = volume_tables.VolumesTable
    page_title = _("Volumes")

    def get_data(self):
        volumes = []
        attached_instance_ids = []
        instances = []
        volume_ids_with_snapshots = []

        def _task_get_volumes():
            volumes.extend(self._get_volumes())
            attached_instance_ids.extend(
                self._get_attached_instance_ids(volumes))

        def _task_get_instances():
            # As long as Nova API does not allow passing attached_instance_ids
            # to nova.server_list, this call can be forged to pass anything
            # != None
            instances.extend(self._get_instances())

            # In volumes tab we don't need to know about the assignment
            # instance-image, therefore fixing it to an empty value
            for instance in instances:
                if hasattr(instance, 'image'):
                    if isinstance(instance.image, dict):
                        instance.image['name'] = "-"

        def _task_get_volumes_snapshots():
            volume_ids_with_snapshots.extend(
                self._get_volumes_ids_with_snapshots())

        futurist_utils.call_functions_parallel(
            _task_get_volumes,
            _task_get_instances,
            _task_get_volumes_snapshots)

        self._set_volume_attributes(
            volumes, instances, volume_ids_with_snapshots)
        self._get_groups(volumes)
        return volumes


class DetailView(tabs.TabbedTableView):
    tab_group_class = project_tabs.VolumeDetailTabs
    template_name = 'horizon/common/_detail.html'
    page_title = "{{ volume.name|default:volume.id }}"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        volume, snapshots = self.get_data()
        table = volume_tables.VolumesTable(self.request)
        context["volume"] = volume
        context["url"] = self.get_redirect_url()
        context["actions"] = table.render_row_actions(volume)
        choices = volume_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES
        volume.status_label = filters.get_display_label(choices, volume.status)
        return context

    def get_search_opts(self, volume):
        return {'volume_id': volume.id}

    @memoized.memoized_method
    def get_data(self):
        try:
            volume_id = self.kwargs['volume_id']
            volume = cinder.volume_get(self.request, volume_id)
            search_opts = self.get_search_opts(volume)
            snapshots = cinder.volume_snapshot_list(
                self.request, search_opts=search_opts)
            if snapshots:
                setattr(volume, 'has_snapshot', True)
            for att in volume.attachments:
                att['instance'] = nova.server_get(self.request,
                                                  att['server_id'])
            if getattr(volume, 'group_id', None):
                volume.group = cinder.group_get(self.request, volume.group_id)
            else:
                volume.group = None
        except Exception:
            redirect = self.get_redirect_url()
            exceptions.handle(self.request,
                              _('Unable to retrieve volume details.'),
                              redirect=redirect)
        return volume, snapshots

    def get_redirect_url(self):
        return reverse('horizon:project:volumes:index')

    def get_tabs(self, request, *args, **kwargs):
        volume, snapshots = self.get_data()
        return self.tab_group_class(
            request, volume=volume, snapshots=snapshots, **kwargs)


class CreateView(forms.ModalFormView):
    form_class = volume_forms.CreateForm
    template_name = 'project/volumes/create.html'
    submit_label = _("Create Volume")
    submit_url = reverse_lazy("horizon:project:volumes:create")
    success_url = reverse_lazy('horizon:project:volumes:index')
    page_title = _("Create Volume")

    def get_initial(self):
        initial = super().get_initial()
        self.default_vol_type = None
        try:
            self.default_vol_type = cinder.volume_type_default(self.request)
            initial['type'] = self.default_vol_type.name
        except dashboard_exception.NOT_FOUND:
            pass
        return initial

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        try:
            context['usages'] = quotas.tenant_quota_usages(
                self.request, targets=('volumes', 'gigabytes'))
            context['volume_types'] = self._get_volume_types()
        except Exception:
            exceptions.handle(self.request)
        return context

    def _get_volume_types(self):
        volume_types = []
        try:
            volume_types = cinder.volume_type_list(self.request)
        except Exception:
            exceptions.handle(self.request,
                              _('Unable to retrieve volume type list.'))

        # check if we have default volume type so we can present the
        # description of no volume type differently
        no_type_description = None
        if self.default_vol_type is None:
            message = \
                _("If \"No volume type\" is selected, the volume will be "
                  "created without a volume type.")

            no_type_description = encoding.force_str(message)

        type_descriptions = [{'name': '',
                              'description': no_type_description}] + \
                            [{'name': type.name,
                              'description': getattr(type, "description", "")}
                             for type in volume_types]

        return json.dumps(type_descriptions)


class ExtendView(forms.ModalFormView):
    form_class = volume_forms.ExtendForm
    template_name = 'project/volumes/extend.html'
    submit_label = _("Extend Volume")
    submit_url = "horizon:project:volumes:extend"
    success_url = reverse_lazy("horizon:project:volumes:index")
    page_title = _("Extend Volume")

    def get_object(self):
        if not hasattr(self, "_object"):
            volume_id = self.kwargs['volume_id']
            try:
                self._object = cinder.volume_get(self.request, volume_id)
            except Exception:
                self._object = None
                exceptions.handle(self.request,
                                  _('Unable to retrieve volume information.'))
        return self._object

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['volume'] = self.get_object()
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        try:
            usages = quotas.tenant_quota_usages(self.request,
                                                targets=('gigabytes',))
            usages.tally('gigabytes', - context['volume'].size)
            context['usages'] = usages
        except Exception:
            exceptions.handle(self.request)
        return context

    def get_initial(self):
        volume = self.get_object()
        return {'id': self.kwargs['volume_id'],
                'name': volume.name,
                'orig_size': volume.size}


class CreateSnapshotView(forms.ModalFormView):
    form_class = volume_forms.CreateSnapshotForm
    template_name = 'project/volumes/create_snapshot.html'
    submit_url = "horizon:project:volumes:create_snapshot"
    success_url = reverse_lazy('horizon:project:snapshots:index')
    page_title = _("Create Volume Snapshot")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['volume_id'] = self.kwargs['volume_id']
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        try:
            volume = cinder.volume_get(self.request, context['volume_id'])
            if (volume.status == 'in-use'):
                context['attached'] = True
                context['form'].set_warning(_("This volume is currently "
                                              "attached to an instance. "
                                              "In some cases, creating a "
                                              "snapshot from an attached "
                                              "volume can result in a "
                                              "corrupted snapshot."))
            context['usages'] = quotas.tenant_quota_usages(
                self.request, targets=('snapshots', 'gigabytes'))
        except Exception:
            exceptions.handle(self.request,
                              _('Unable to retrieve volume information.'))
        return context

    def get_initial(self):
        return {'volume_id': self.kwargs["volume_id"]}


class UploadToImageView(forms.ModalFormView):
    form_class = volume_forms.UploadToImageForm
    template_name = 'project/volumes/upload_to_image.html'
    submit_label = _("Upload")
    submit_url = "horizon:project:volumes:upload_to_image"
    success_url = reverse_lazy("horizon:project:volumes:index")
    page_title = _("Upload Volume to Image")

    @memoized.memoized_method
    def get_data(self):
        try:
            volume_id = self.kwargs['volume_id']
            volume = cinder.volume_get(self.request, volume_id)
        except Exception:
            error_message = _(
                'Unable to retrieve volume information for volume: "%s"') \
                % volume_id
            exceptions.handle(self.request,
                              error_message,
                              redirect=self.success_url)

        return volume

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['volume'] = self.get_data()
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        return context

    def get_initial(self):
        volume = self.get_data()

        return {'id': self.kwargs['volume_id'],
                'name': volume.name,
                'status': volume.status}


class CreateTransferView(forms.ModalFormView):
    form_class = volume_forms.CreateTransferForm
    template_name = 'project/volumes/create_transfer.html'
    success_url = reverse_lazy('horizon:project:volumes:index')
    modal_id = "create_volume_transfer_modal"
    submit_label = _("Create Volume Transfer")
    submit_url = "horizon:project:volumes:create_transfer"
    page_title = _("Create Volume Transfer")

    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(**kwargs)
        volume_id = self.kwargs['volume_id']
        context['volume_id'] = volume_id
        context['submit_url'] = reverse(self.submit_url, args=[volume_id])
        return context

    def get_initial(self):
        return {'volume_id': self.kwargs["volume_id"]}

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs['next_view'] = ShowTransferView
        return kwargs


class AcceptTransferView(forms.ModalFormView):
    form_class = volume_forms.AcceptTransferForm
    template_name = 'project/volumes/accept_transfer.html'
    success_url = reverse_lazy('horizon:project:volumes:index')
    modal_id = "accept_volume_transfer_modal"
    submit_label = _("Accept Volume Transfer")
    submit_url = reverse_lazy(
        "horizon:project:volumes:accept_transfer")
    page_title = _("Accept Volume Transfer")


class ShowTransferView(forms.ModalFormView):
    form_class = volume_forms.ShowTransferForm
    template_name = 'project/volumes/show_transfer.html'
    success_url = reverse_lazy('horizon:project:volumes:index')
    modal_id = "show_volume_transfer_modal"
    modal_header = _("Volume Transfer")
    submit_url = "horizon:project:volumes:show_transfer"
    cancel_label = _("Close")
    download_label = _("Download transfer credentials")
    page_title = _("Volume Transfer Details")

    def get_object(self):
        try:
            return self._object
        except AttributeError:
            transfer_id = self.kwargs['transfer_id']
            try:
                self._object = cinder.transfer_get(self.request, transfer_id)
                return self._object
            except Exception:
                exceptions.handle(self.request,
                                  _('Unable to retrieve volume transfer.'))

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['transfer_id'] = self.kwargs['transfer_id']
        context['auth_key'] = self.kwargs['auth_key']
        context['download_label'] = self.download_label
        context['download_url'] = reverse(
            'horizon:project:volumes:download_transfer_creds',
            args=[context['transfer_id'], context['auth_key']]
        )
        return context

    def get_initial(self):
        transfer = self.get_object()
        return {'id': transfer.id,
                'name': transfer.name,
                'auth_key': self.kwargs['auth_key']}


class UpdateView(forms.ModalFormView):
    form_class = volume_forms.UpdateForm
    modal_id = "update_volume_modal"
    template_name = 'project/volumes/update.html'
    submit_url = "horizon:project:volumes:update"
    success_url = reverse_lazy("horizon:project:volumes:index")
    page_title = _("Edit Volume")

    def get_object(self):
        if not hasattr(self, "_object"):
            vol_id = self.kwargs['volume_id']
            try:
                self._object = cinder.volume_get(self.request, vol_id)
            except Exception:
                msg = _('Unable to retrieve volume.')
                url = reverse('horizon:project:volumes:index')
                exceptions.handle(self.request, msg, redirect=url)
        return self._object

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['volume'] = self.get_object()
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        return context

    def get_initial(self):
        volume = self.get_object()
        return {'volume_id': self.kwargs["volume_id"],
                'name': volume.name,
                'description': volume.description,
                'bootable': volume.is_bootable}


class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
    table_class = volume_tables.AttachmentsTable
    form_class = volume_forms.AttachForm
    form_id = "attach_volume_form"
    modal_id = "attach_volume_modal"
    template_name = 'project/volumes/attach.html'
    submit_url = "horizon:project:volumes:attach"
    success_url = reverse_lazy("horizon:project:volumes:index")
    page_title = _("Manage Volume Attachments")

    @memoized.memoized_method
    def get_object(self):
        volume_id = self.kwargs['volume_id']
        try:
            return cinder.volume_get(self.request, volume_id)
        except Exception:
            self._object = None
            exceptions.handle(self.request,
                              _('Unable to retrieve volume information.'))

    def get_data(self):
        attachments = []
        volume = self.get_object()
        if volume is not None:
            for att in volume.attachments:
                att['volume_name'] = getattr(volume, 'name', att['device'])
                attachments.append(att)
        return attachments

    def get_initial(self):
        try:
            instances, has_more = nova.server_list(self.request)
        except Exception:
            instances = []
            exceptions.handle(self.request,
                              _("Unable to retrieve attachment information."))
        return {'volume': self.get_object(),
                'instances': instances}

    @memoized.memoized_method
    def get_form(self, **kwargs):
        form_class = kwargs.get('form_class', self.get_form_class())
        return super().get_form(form_class)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'] = self.get_form()
        volume = self.get_object()
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        if volume and volume.status == 'available':
            context['show_attach'] = True
        else:
            context['show_attach'] = False
        context['volume'] = volume
        if self.request.is_ajax():
            context['hide'] = True
        return context

    def get(self, request, *args, **kwargs):
        # Table action handling
        handled = self.construct_tables()
        if handled:
            return handled
        return self.render_to_response(self.get_context_data(**kwargs))

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        if form.is_valid():
            return self.form_valid(form)
        return self.get(request, *args, **kwargs)


class RetypeView(forms.ModalFormView):
    form_class = volume_forms.RetypeForm
    modal_id = "retype_volume_modal"
    template_name = 'project/volumes/retype.html'
    submit_label = _("Change Volume Type")
    submit_url = "horizon:project:volumes:retype"
    success_url = reverse_lazy("horizon:project:volumes:index")
    page_title = _("Change Volume Type")

    @memoized.memoized_method
    def get_data(self):
        try:
            volume_id = self.kwargs['volume_id']
            volume = cinder.volume_get(self.request, volume_id)
        except Exception:
            error_message = _(
                'Unable to retrieve volume information for volume: "%s"') \
                % volume_id
            exceptions.handle(self.request,
                              error_message,
                              redirect=self.success_url)

        return volume

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['volume'] = self.get_data()
        args = (self.kwargs['volume_id'],)
        context['submit_url'] = reverse(self.submit_url, args=args)
        return context

    def get_initial(self):
        volume = self.get_data()

        return {'id': self.kwargs['volume_id'],
                'name': volume.name,
                'volume_type': volume.volume_type}


class EncryptionDetailView(generic.TemplateView):
    template_name = 'project/volumes/encryption_detail.html'
    page_title = _("Volume Encryption Details: {{ volume.name }}")

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        volume = self.get_volume_data()
        context["encryption_metadata"] = self.get_encryption_data()
        context["volume"] = volume
        context["page_title"] = _("Volume Encryption Details: "
                                  "%(volume_name)s") % {'volume_name':
                                                        volume.name}
        return context

    @memoized.memoized_method
    def get_encryption_data(self):
        try:
            volume_id = self.kwargs['volume_id']
            self._encryption_metadata = \
                cinder.volume_get_encryption_metadata(self.request,
                                                      volume_id)
        except Exception:
            redirect = self.get_redirect_url()
            exceptions.handle(self.request,
                              _('Unable to retrieve volume encryption '
                                'details.'),
                              redirect=redirect)
        return self._encryption_metadata

    @memoized.memoized_method
    def get_volume_data(self):
        try:
            volume_id = self.kwargs['volume_id']
            volume = cinder.volume_get(self.request, volume_id)
        except Exception:
            redirect = self.get_redirect_url()
            exceptions.handle(self.request,
                              _('Unable to retrieve volume details.'),
                              redirect=redirect)
        return volume

    def get_redirect_url(self):
        return reverse('horizon:project:volumes:index')


class DownloadTransferCreds(generic.View):
    @method_decorator(never_cache)
    def get(self, request, transfer_id, auth_key):
        try:
            transfer = cinder.transfer_get(self.request, transfer_id)
        except Exception:
            transfer = None
        context = {'transfer': {
            'name': getattr(transfer, 'name', ''),
            'id': transfer_id,
            'auth_key': auth_key,
        }}
        response = shortcuts.render(
            request,
            'project/volumes/download_transfer_creds.html',
            context, content_type='application/text')
        response['Content-Disposition'] = (
            'attachment; filename=%s.txt' % slugify(transfer_id))
        return response