From ffd93c5334bbe293bdd199d2f6d98eede93e4b15 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Thu, 29 Sep 2011 16:38:06 -0700 Subject: [PATCH 1/6] adding edit/delete capability for owner of an image through the user dashboard --- django-openstack/django_openstack/api.py | 2 +- .../django_openstack/dash/urls.py | 1 + .../django_openstack/dash/views/images.py | 98 +++++++++++++++++++ .../django_openstack/dash/images/_delete.html | 8 ++ .../django_openstack/dash/images/_form.html | 10 ++ .../django_openstack/dash/images/_list.html | 13 ++- .../django_openstack/dash/images/update.html | 25 +++++ 7 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html create mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html create mode 100644 django-openstack/django_openstack/templates/django_openstack/dash/images/update.html diff --git a/django-openstack/django_openstack/api.py b/django-openstack/django_openstack/api.py index 106c4f52c..d988e6c0c 100644 --- a/django-openstack/django_openstack/api.py +++ b/django-openstack/django_openstack/api.py @@ -141,7 +141,7 @@ class Image(APIDictWrapper): """Simple wrapper around glance image dictionary""" _attrs = ['checksum', 'container_format', 'created_at', 'deleted', 'deleted_at', 'disk_format', 'id', 'is_public', 'location', - 'name', 'properties', 'size', 'status', 'updated_at'] + 'name', 'properties', 'size', 'status', 'updated_at', 'owner'] def __getattr__(self, attrname): if attrname == "properties": diff --git a/django-openstack/django_openstack/dash/urls.py b/django-openstack/django_openstack/dash/urls.py index 553c71b6f..002413f62 100644 --- a/django-openstack/django_openstack/dash/urls.py +++ b/django-openstack/django_openstack/dash/urls.py @@ -50,6 +50,7 @@ urlpatterns += patterns('django_openstack.dash.views.security_groups', urlpatterns += patterns('django_openstack.dash.views.images', url(r'^(?P[^/]+)/images/$', 'index', name='dash_images'), url(IMAGES % 'launch', 'launch', name='dash_images_launch'), + url(IMAGES % 'update', 'update', name='dash_images_update'), ) urlpatterns += patterns('django_openstack.dash.views.keypairs', diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index b17bb2e1d..b88f27d2d 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -46,6 +46,75 @@ from novaclient import exceptions as novaclient_exceptions LOG = logging.getLogger('django_openstack.dash.views.images') +class UpdateImageForm(forms.SelfHandlingForm): + image_id = forms.CharField(widget=forms.HiddenInput()) + name = forms.CharField(max_length="25", label="Name") + kernel = forms.CharField(max_length="25", label="Kernel ID", + required=False) + ramdisk = forms.CharField(max_length="25", label="Ramdisk ID", + required=False) + architecture = forms.CharField(label="Architecture", required=False) + #project_id = forms.CharField(label="Project ID") + container_format = forms.CharField(label="Container Format", + required=False) + disk_format = forms.CharField(label="Disk Format") + #is_public = forms.BooleanField(label="Publicly Available", required=False) + + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = request.user.tenant_id + + try: + image = api.image_get(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, "Error connecting to glance: %s" + % e.message) + except glance_exception.Error, e: + LOG.exception('Error retrieving image with id "%s"' % image_id) + messages.error(request, "Error retrieving image %s: %s" + % (image_id, e.message)) + + if image.owner == request.user.username: + try: + meta = { + 'is_public': True, + 'disk_format': data['disk_format'], + 'container_format': data['container_format'], + 'name': data['name'], + } + # TODO add public flag to properties + meta['properties'] = {} + if data['kernel']: + meta['properties']['kernel_id'] = data['kernel'] + + if data['ramdisk']: + meta['properties']['ramdisk_id'] = data['ramdisk'] + + if data['architecture']: + meta['properties']['architecture'] = data['architecture'] + + api.image_update(request, image_id, meta) + messages.success(request, "Image was successfully updated.") + + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, "Error connecting to glance: %s" + % e.message) + except glance_exception.Error, e: + LOG.exception('Error updating image with id "%s"' % image_id) + messages.error(request, "Error updating image: %s" % e.message) + except: + LOG.exception('Unspecified Exception in image update') + messages.error(request, "Image could not be updated, \ + please try again.") + return redirect('dash_images_update', tenant_id, image_id) + else: + messages.info(request, "Unable to update image, you are not its \ + owner.") + return redirect('dash_images_update', tenant_id, image_id) + + class LaunchForm(forms.SelfHandlingForm): name = forms.CharField(max_length=80, label="Server Name") image_id = forms.CharField(widget=forms.HiddenInput()) @@ -210,3 +279,32 @@ def launch(request, tenant_id, image_id): 'form': form, 'quotas': quotas, }, context_instance=template.RequestContext(request)) + + +@login_required +def update(request, tenant_id, image_id): + try: + image = api.image_get(request, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, "Error connecting to glance: %s" + % e.message) + except glance_exception.Error, e: + LOG.exception('Error retrieving image with id "%s"' % image_id) + messages.error(request, "Error retrieving image %s: %s" + % (image_id, e.message)) + + form, handled = UpdateImageForm().maybe_handle(request, initial={ + 'image_id': image_id, + 'name': image.get('name', ''), + 'kernel': image['properties'].get('kernel_id', ''), + 'ramdisk': image['properties'].get('ramdisk_id', ''), + 'architecture': image['properties'].get('architecture', ''), + 'container_format': image.get('container_format', ''), + 'disk_format': image.get('disk_format', ''),}) + if handled: + return handled + + return render_to_response('django_openstack/dash/images/update.html', { + 'form': form, + }, context_instance=template.RequestContext(request)) diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html new file mode 100644 index 000000000..1f9d0868f --- /dev/null +++ b/django-openstack/django_openstack/templates/django_openstack/dash/images/_delete.html @@ -0,0 +1,8 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{hidden}} + {% endfor %} + + +
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html new file mode 100644 index 000000000..c6494bb9e --- /dev/null +++ b/django-openstack/django_openstack/templates/django_openstack/dash/images/_form.html @@ -0,0 +1,10 @@ +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} + {% for field in form.visible_fields %} + {{ field.label_tag }} + {{ field.errors }} + {{ field }} + {% endfor %} + +
diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html index 11c8ba8bf..e04fcf5e0 100644 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html +++ b/django-openstack/django_openstack/templates/django_openstack/dash/images/_list.html @@ -14,11 +14,14 @@ {{image.created_at|parse_date}} {{image.updated_at|parse_date}} {{image.status|capfirst}} - - - + {% if image.owner == request.user.username %} + + + + {% endif %} {% endfor %} diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html new file mode 100644 index 000000000..ce88bc80f --- /dev/null +++ b/django-openstack/django_openstack/templates/django_openstack/dash/images/update.html @@ -0,0 +1,25 @@ +{% extends 'django_openstack/dash/base.html' %} + +{% block sidebar %} + {% with current_sidebar="images" %} + {{block.super}} + {% endwith %} +{% endblock %} + +{% block page_header %} + {% include "django_openstack/common/_page_header.html" with title="Update Image" %} +{% endblock page_header %} + +{% block dash_main %} +
+
+ {% include 'django_openstack/dash/images/_form.html' %} +
+ +
+

Description:

+

From here you can modify different properties of an image.

+
+
 
+
+{% endblock %} From 9f06bd040e6afab3e4a7d9dfc85f9f9cf2f62d2b Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Thu, 29 Sep 2011 16:58:04 -0700 Subject: [PATCH 2/6] adding delete form to index, so images owned by a user can be deleted by them --- .../django_openstack/dash/views/images.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index b88f27d2d..afc2aae9f 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -183,16 +183,45 @@ class LaunchForm(forms.SelfHandlingForm): 'Unable to launch instance: %s' % e.message) +class DeleteImage(forms.SelfHandlingForm): + image_id = forms.CharField(required=True) + + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = request.user.tenant_id + try: + image = api.image_get(request, image_id) + if image.owner == request.user.username: + api.image_delete(request, image_id) + else: + messages.info(request, "Unable to delete image, you are not \ + its owner.") + return redirect('dash_images_update', tenant_id, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, "Error connecting to glance: %s" + % e.message) + except glance_exception.Error, e: + LOG.exception('Error deleting image with id "%s"' % image_id) + messages.error(request, "Error deleting image: %s: %s" + % (image_id, e.message)) + return redirect(request.build_absolute_uri()) + + @login_required def index(request, tenant_id): - tenant = {} + for f in (DeleteImage, ): + _, handled = f.maybe_handle(request) + if handled: + return handled + delete_form = DeleteImage() + tenant = {} try: tenant = api.token_get_tenant(request, request.user.tenant_id) except api_exceptions.ApiException, e: messages.error(request, "Unable to retrienve tenant info\ from keystone: %s" % e.message) - all_images = [] try: all_images = api.image_list_detailed(request) @@ -214,6 +243,7 @@ def index(request, tenant_id): return render_to_response( 'django_openstack/dash/images/index.html', { + 'delete_form': delete_form, 'tenant': tenant, 'images': images, }, context_instance=template.RequestContext(request)) From a5816d684d55cd21e9ac143e0cc4950e0b745ae5 Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 30 Sep 2011 10:43:33 -0700 Subject: [PATCH 3/6] merging master --- .../django_openstack/dash/views/images.py | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index afc2aae9f..1dd0fb262 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -22,19 +22,13 @@ Views for managing Nova images. """ -import datetime import logging -import re -from django import http from django import template -from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render_to_response from django.utils.text import normalize_newlines -from django.utils.translation import ugettext as _ -from django import shortcuts from django_openstack import api from django_openstack import forms @@ -55,10 +49,10 @@ class UpdateImageForm(forms.SelfHandlingForm): required=False) architecture = forms.CharField(label="Architecture", required=False) #project_id = forms.CharField(label="Project ID") + #is_public = forms.BooleanField(label="Publicly Available", required=False) container_format = forms.CharField(label="Container Format", required=False) disk_format = forms.CharField(label="Disk Format") - #is_public = forms.BooleanField(label="Publicly Available", required=False) def handle(self, request, data): image_id = data['image_id'] @@ -139,13 +133,16 @@ class LaunchForm(forms.SelfHandlingForm): required=False, help_text="Which keypair to use for authentication") - securitygrouplist = kwargs.get('initial', {}).get('securitygrouplist', []) - self.fields['security_groups'] = forms.MultipleChoiceField(choices=securitygrouplist, + securitygrouplist = kwargs.get('initial', {}).get( + 'securitygrouplist', []) + self.fields['security_groups'] = forms.MultipleChoiceField( + choices=securitygrouplist, label='Security Groups', required=True, initial=['default'], - widget=forms.SelectMultiple(attrs={'class': 'chzn-select', - 'style': "min-width: 200px"}), + widget=forms.SelectMultiple( + attrs={'class': 'chzn-select', + 'style': "min-width: 200px"}), help_text="Launch instance in these Security Groups") # setting self.fields.keyOrder seems to break validation, # so ordering fields manually @@ -177,35 +174,35 @@ class LaunchForm(forms.SelfHandlingForm): return redirect('dash_instances', tenant_id) except api_exceptions.ApiException, e: - LOG.exception('ApiException while creating instances of image "%s"' % - image_id) + LOG.exception('ApiException while creating instances of image "%s"' + % image_id) messages.error(request, 'Unable to launch instance: %s' % e.message) class DeleteImage(forms.SelfHandlingForm): - image_id = forms.CharField(required=True) + image_id = forms.CharField(required=True) - def handle(self, request, data): - image_id = data['image_id'] - tenant_id = request.user.tenant_id - try: - image = api.image_get(request, image_id) - if image.owner == request.user.username: - api.image_delete(request, image_id) - else: - messages.info(request, "Unable to delete image, you are not \ + def handle(self, request, data): + image_id = data['image_id'] + tenant_id = request.user.tenant_id + try: + image = api.image_get(request, image_id) + if image.owner == request.user.username: + api.image_delete(request, image_id) + else: + messages.info(request, "Unable to delete image, you are not \ its owner.") - return redirect('dash_images_update', tenant_id, image_id) - except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, "Error connecting to glance: %s" + return redirect('dash_images_update', tenant_id, image_id) + except glance_exception.ClientConnectionError, e: + LOG.exception("Error connecting to glance") + messages.error(request, "Error connecting to glance: %s" % e.message) - except glance_exception.Error, e: - LOG.exception('Error deleting image with id "%s"' % image_id) - messages.error(request, "Error deleting image: %s: %s" + except glance_exception.Error, e: + LOG.exception('Error deleting image with id "%s"' % image_id) + messages.error(request, "Error deleting image: %s: %s" % (image_id, e.message)) - return redirect(request.build_absolute_uri()) + return redirect(request.build_absolute_uri()) @login_required @@ -251,6 +248,7 @@ def index(request, tenant_id): @login_required def launch(request, tenant_id, image_id): + def flavorlist(): try: fl = api.flavor_list(request) @@ -331,7 +329,7 @@ def update(request, tenant_id, image_id): 'ramdisk': image['properties'].get('ramdisk_id', ''), 'architecture': image['properties'].get('architecture', ''), 'container_format': image.get('container_format', ''), - 'disk_format': image.get('disk_format', ''),}) + 'disk_format': image.get('disk_format', ''), }) if handled: return handled From 69496de1106bb1ee0c65c6b50d05c1083d0e8a9f Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Thu, 29 Sep 2011 17:23:01 -0700 Subject: [PATCH 4/6] adding a notice for when there are no images, so the page isnt just blank --- .../templates/django_openstack/dash/images/index.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html b/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html index c16041eff..1f40bb5c2 100644 --- a/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html +++ b/django-openstack/django_openstack/templates/django_openstack/dash/images/index.html @@ -16,5 +16,10 @@ {% if images %} {% include 'django_openstack/dash/images/_list.html' %} - {% endif %} + {% else %} +
+

Info

+

There are currently no images.

+
+ {% endif %} {% endblock %} From cac45eb883646ec902b4b0c35e1e336ac7532e2a Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 30 Sep 2011 11:00:50 -0700 Subject: [PATCH 5/6] fixing style nits --- django-openstack/django_openstack/dash/views/images.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index 1dd0fb262..f8d1b7d07 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -44,14 +44,12 @@ class UpdateImageForm(forms.SelfHandlingForm): image_id = forms.CharField(widget=forms.HiddenInput()) name = forms.CharField(max_length="25", label="Name") kernel = forms.CharField(max_length="25", label="Kernel ID", - required=False) + required=False) ramdisk = forms.CharField(max_length="25", label="Ramdisk ID", - required=False) + required=False) architecture = forms.CharField(label="Architecture", required=False) - #project_id = forms.CharField(label="Project ID") - #is_public = forms.BooleanField(label="Publicly Available", required=False) container_format = forms.CharField(label="Container Format", - required=False) + required=False) disk_format = forms.CharField(label="Disk Format") def handle(self, request, data): @@ -65,7 +63,7 @@ class UpdateImageForm(forms.SelfHandlingForm): messages.error(request, "Error connecting to glance: %s" % e.message) except glance_exception.Error, e: - LOG.exception('Error retrieving image with id "%s"' % image_id) + LOG.exception('Error retrieving image with id "%s"', image_id) messages.error(request, "Error retrieving image %s: %s" % (image_id, e.message)) From 6572902c5ae29e877ba9446b516e0db685391b5d Mon Sep 17 00:00:00 2001 From: Jake Dahn Date: Fri, 30 Sep 2011 15:20:26 -0700 Subject: [PATCH 6/6] fixed other nits --- .../django_openstack/dash/views/images.py | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/django-openstack/django_openstack/dash/views/images.py b/django-openstack/django_openstack/dash/views/images.py index f8d1b7d07..ccac7cf41 100644 --- a/django-openstack/django_openstack/dash/views/images.py +++ b/django-openstack/django_openstack/dash/views/images.py @@ -29,6 +29,7 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import redirect, render_to_response from django.utils.text import normalize_newlines +from django.utils.translation import ugettext as _ from django_openstack import api from django_openstack import forms @@ -55,17 +56,17 @@ class UpdateImageForm(forms.SelfHandlingForm): def handle(self, request, data): image_id = data['image_id'] tenant_id = request.user.tenant_id + error_retrieving = _('Unable to retreive image info from glance: %s' % image_id) + error_updating = _('Error updating image with id: %s' % image_id) try: image = api.image_get(request, image_id) except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, "Error connecting to glance: %s" - % e.message) + LOG.exception(_('Error connecting to glance')) + messages.error(request, error_retrieving) except glance_exception.Error, e: - LOG.exception('Error retrieving image with id "%s"', image_id) - messages.error(request, "Error retrieving image %s: %s" - % (image_id, e.message)) + LOG.exception(error_retrieving) + messages.error(request, error_retrieving) if image.owner == request.user.username: try: @@ -87,23 +88,21 @@ class UpdateImageForm(forms.SelfHandlingForm): meta['properties']['architecture'] = data['architecture'] api.image_update(request, image_id, meta) - messages.success(request, "Image was successfully updated.") + messages.success(request, _('Image was successfully updated.')) except glance_exception.ClientConnectionError, e: - LOG.exception("Error connecting to glance") - messages.error(request, "Error connecting to glance: %s" - % e.message) + LOG.exception(_('Error connecting to glance')) + messages.error(request, error_retrieving) except glance_exception.Error, e: - LOG.exception('Error updating image with id "%s"' % image_id) - messages.error(request, "Error updating image: %s" % e.message) + LOG.exception(error_updating) + messages.error(request, error_updating) except: - LOG.exception('Unspecified Exception in image update') - messages.error(request, "Image could not be updated, \ - please try again.") + LOG.exception(_('Unspecified Exception in image update')) + messages.error(request, error_updating) return redirect('dash_images_update', tenant_id, image_id) else: - messages.info(request, "Unable to update image, you are not its \ - owner.") + messages.info(request, _('Unable to update image. You are not its \ + owner.')) return redirect('dash_images_update', tenant_id, image_id) @@ -206,7 +205,7 @@ class DeleteImage(forms.SelfHandlingForm): @login_required def index(request, tenant_id): for f in (DeleteImage, ): - _, handled = f.maybe_handle(request) + unused, handled = f.maybe_handle(request) if handled: return handled delete_form = DeleteImage()