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