Add images CRUD support
Adding images CRUD support, importing code from Horizon. Change-Id: Ie054d33792881243201fdabed88aba2c72576e34
This commit is contained in:
parent
d1b731413e
commit
c11d10ff8e
21
tuskar_ui/infrastructure/images/forms.py
Normal file
21
tuskar_ui/infrastructure/images/forms.py
Normal file
@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
from openstack_dashboard.dashboards.project.images.images import forms
|
||||
|
||||
|
||||
class CreateImageForm(forms.CreateImageForm):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateImageForm(forms.UpdateImageForm):
|
||||
pass
|
@ -16,6 +16,46 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.images.images \
|
||||
import tables as project_tables
|
||||
|
||||
|
||||
class CreateImage(project_tables.CreateImage):
|
||||
url = "horizon:infrastructure:images:create"
|
||||
|
||||
|
||||
class DeleteImage(project_tables.DeleteImage):
|
||||
def allowed(self, request, image=None):
|
||||
if image and image.protected:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class UpdateRow(tables.Row):
|
||||
ajax = True
|
||||
|
||||
def get_data(self, request, image_id):
|
||||
image = api.glance.image_get(request, image_id)
|
||||
return image
|
||||
|
||||
|
||||
class ImageFilterAction(tables.FilterAction):
|
||||
filter_type = "server"
|
||||
filter_choices = (('name', _("Image Name ="), True),
|
||||
('status', _('Status ='), True),
|
||||
('disk_format', _('Format ='), True),
|
||||
('size_min', _('Min. Size (MB)'), True),
|
||||
('size_max', _('Max. Size (MB)'), True))
|
||||
|
||||
|
||||
class EditImage(project_tables.EditImage):
|
||||
url = "horizon:infrastructure:images:update"
|
||||
|
||||
def allowed(self, request, image=None):
|
||||
return True
|
||||
|
||||
|
||||
class ImagesTable(tables.DataTable):
|
||||
|
||||
@ -29,7 +69,9 @@ class ImagesTable(tables.DataTable):
|
||||
|
||||
class Meta:
|
||||
name = "images"
|
||||
row_class = UpdateRow
|
||||
verbose_name = _("Provisioning Images")
|
||||
multi_select = False
|
||||
table_actions = ()
|
||||
row_actions = ()
|
||||
table_actions = (CreateImage, DeleteImage,
|
||||
ImageFilterAction)
|
||||
row_actions = (EditImage, DeleteImage)
|
||||
|
@ -0,0 +1,35 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}create_image_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:images:create' %}{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>
|
||||
{% trans "Specify an image to upload to the Image Service." %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Currently only images available via an HTTP URL are supported. The image location must be accessible to the Image Service. Compressed image binaries are supported (.zip and .tar.gz.)" %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{% trans "Please note: " %}</strong>
|
||||
{% trans "The Image Location field MUST be a valid and direct URL to the image binary. URLs that redirect or serve error pages will result in unusable images." %}
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Image" %}" />
|
||||
<a href="{% url 'horizon:infrastructure:images:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,26 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}update_image_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:infrastructure:images:update' image.id %}{% endblock %}
|
||||
|
||||
{% block modal_id %}update_image_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Update Image" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% trans "Modify different properties of an image." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Update Image" %}" />
|
||||
<a href="{% url 'horizon:infrastructure:images:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
11
tuskar_ui/infrastructure/images/templates/images/create.html
Normal file
11
tuskar_ui/infrastructure/images/templates/images/create.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create An Image" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create An Image") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'infrastructure/images/_create.html' %}
|
||||
{% endblock %}
|
12
tuskar_ui/infrastructure/images/templates/images/update.html
Normal file
12
tuskar_ui/infrastructure/images/templates/images/update.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Update Image" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Update Image") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'infrastructure/images/_update.html' %}
|
||||
{% endblock %}
|
@ -13,16 +13,21 @@
|
||||
# under the License.
|
||||
|
||||
import contextlib
|
||||
import mock
|
||||
from mock import patch, call # noqa
|
||||
|
||||
from django.core import urlresolvers
|
||||
|
||||
from mock import patch, call # noqa
|
||||
from openstack_dashboard.dashboards.project.images.images import forms
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui.test import helpers as test
|
||||
|
||||
INDEX_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:images:index')
|
||||
CREATE_URL = urlresolvers.reverse(
|
||||
'horizon:infrastructure:images:create')
|
||||
UPDATE_URL = 'horizon:infrastructure:images:update'
|
||||
|
||||
|
||||
class ImagesTest(test.BaseAdminViewTests):
|
||||
@ -40,6 +45,125 @@ class ImagesTest(test.BaseAdminViewTests):
|
||||
return_value=plans),
|
||||
patch('openstack_dashboard.api.glance.image_list_detailed',
|
||||
return_value=[self.images.list(), False, False]),):
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'infrastructure/images/index.html')
|
||||
|
||||
def test_create_get(self):
|
||||
res = self.client.get(CREATE_URL)
|
||||
|
||||
self.assertTemplateUsed(res, 'infrastructure/images/create.html')
|
||||
|
||||
def test_update_get(self):
|
||||
image = self.images.list()[0]
|
||||
|
||||
with contextlib.nested(
|
||||
patch('openstack_dashboard.api.glance.image_get',
|
||||
return_value=image),) as (mocked_get,):
|
||||
|
||||
res = self.client.get(
|
||||
urlresolvers.reverse(UPDATE_URL, args=(image.id,)))
|
||||
|
||||
mocked_get.assert_called_once_with(mock.ANY, image.id)
|
||||
self.assertTemplateUsed(res, 'infrastructure/images/update.html')
|
||||
|
||||
def test_create_post(self):
|
||||
image = self.images.list()[0]
|
||||
data = {
|
||||
'name': 'Fedora',
|
||||
'description': 'Login with admin/admin',
|
||||
'source_type': 'url',
|
||||
'copy_from': 'http://test_url.com',
|
||||
'disk_format': 'qcow2',
|
||||
'architecture': 'x86-64',
|
||||
'minimum_disk': 15,
|
||||
'minimum_ram': 512,
|
||||
'is_public': True,
|
||||
'protected': False}
|
||||
|
||||
forms.IMAGE_FORMAT_CHOICES = [('qcow2', 'qcow2')]
|
||||
|
||||
with contextlib.nested(
|
||||
patch('openstack_dashboard.api.glance.image_create',
|
||||
return_value=image),) as (mocked_create,):
|
||||
|
||||
res = self.client.post(CREATE_URL, data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
mocked_create.assert_called_once_with(
|
||||
mock.ANY, name=u'Fedora', container_format='bare', min_ram=512,
|
||||
disk_format=u'qcow2', copy_from=u'http://test_url.com',
|
||||
protected=False, min_disk=15, is_public=True,
|
||||
properties={'description': u'Login with admin/admin',
|
||||
'architecture': u'x86-64'})
|
||||
|
||||
def test_update_post(self):
|
||||
image = self.images.list()[0]
|
||||
data = {
|
||||
'image_id': image.id,
|
||||
'name': 'Fedora',
|
||||
'description': 'Login with admin/admin',
|
||||
'source_type': 'url',
|
||||
'copy_from': 'http://test_url.com',
|
||||
'disk_format': 'qcow2',
|
||||
'architecture': 'x86-64',
|
||||
'minimum_disk': 15,
|
||||
'minimum_ram': 512,
|
||||
'is_public': True,
|
||||
'protected': False}
|
||||
|
||||
forms.IMAGE_FORMAT_CHOICES = [('qcow2', 'qcow2')]
|
||||
|
||||
with contextlib.nested(
|
||||
patch('openstack_dashboard.api.glance.image_get',
|
||||
return_value=image),
|
||||
patch('openstack_dashboard.api.glance.image_update',
|
||||
return_value=image),) as (mocked_get, mocked_update,):
|
||||
|
||||
res = self.client.post(
|
||||
urlresolvers.reverse(UPDATE_URL, args=(image.id,)), data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
mocked_get.assert_called_once_with(mock.ANY, image.id)
|
||||
mocked_update.assert_called_once_with(
|
||||
mock.ANY, image.id, name='Fedora', container_format='bare',
|
||||
min_ram=512, disk_format='qcow2', protected=False,
|
||||
is_public=False, min_disk=15, purge_props=False,
|
||||
properties={'description': 'Login with admin/admin',
|
||||
'architecture': 'x86-64'})
|
||||
|
||||
def test_delete_ok(self):
|
||||
roles = [api.tuskar.Role(role) for role in
|
||||
self.tuskarclient_roles.list()]
|
||||
plans = [api.tuskar.Plan(plan) for plan in
|
||||
self.tuskarclient_plans.list()]
|
||||
images = self.images.list()
|
||||
|
||||
data = {'action': 'images__delete',
|
||||
'object_ids': [images[0].id, images[1].id]}
|
||||
|
||||
with contextlib.nested(
|
||||
patch('tuskar_ui.api.tuskar.Role.list',
|
||||
return_value=roles),
|
||||
patch('tuskar_ui.api.tuskar.Plan.list',
|
||||
return_value=plans),
|
||||
patch('openstack_dashboard.api.glance.image_list_detailed',
|
||||
return_value=[images, False, False]),
|
||||
patch('openstack_dashboard.api.glance.image_delete',
|
||||
return_value=None),) as (
|
||||
mock_role_list, plan_list, mock_image_lict, mock_image_delete):
|
||||
|
||||
res = self.client.post(INDEX_URL, data)
|
||||
|
||||
mock_image_delete.has_calls(
|
||||
call(mock.ANY, images[0].id),
|
||||
call(mock.ANY, images[1].id))
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
@ -16,8 +16,10 @@ from django.conf import urls
|
||||
|
||||
from tuskar_ui.infrastructure.images import views
|
||||
|
||||
|
||||
urlpatterns = urls.patterns(
|
||||
'',
|
||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
urls.url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
urls.url(r'^(?P<image_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(), name='update'),
|
||||
)
|
||||
|
@ -12,23 +12,113 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import tables as horizon_tables
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard.api import glance
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.images.images import views
|
||||
|
||||
from tuskar_ui import api
|
||||
from tuskar_ui import api as tuskar_api
|
||||
from tuskar_ui.infrastructure.images import forms
|
||||
from tuskar_ui.infrastructure.images import tables
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(horizon_tables.DataTableView):
|
||||
table_class = tables.ImagesTable
|
||||
template_name = "infrastructure/images/index.html"
|
||||
|
||||
def has_prev_data(self, table):
|
||||
return self._prev
|
||||
|
||||
def has_more_data(self, table):
|
||||
return self._more
|
||||
|
||||
def get_data(self):
|
||||
plan = api.tuskar.Plan.get_the_plan(self.request)
|
||||
images = glance.image_list_detailed(self.request)[0]
|
||||
images = []
|
||||
filters = self.get_filters()
|
||||
prev_marker = self.request.GET.get(
|
||||
tables.ImagesTable._meta.prev_pagination_param, None)
|
||||
|
||||
if prev_marker is not None:
|
||||
sort_dir = 'asc'
|
||||
marker = prev_marker
|
||||
else:
|
||||
sort_dir = 'desc'
|
||||
marker = self.request.GET.get(
|
||||
tables.ImagesTable._meta.pagination_param, None)
|
||||
try:
|
||||
images, self._more, self._prev = api.glance.image_list_detailed(
|
||||
self.request,
|
||||
marker=marker,
|
||||
paginate=True,
|
||||
filters=filters,
|
||||
sort_dir=sort_dir)
|
||||
|
||||
if prev_marker is not None:
|
||||
images = sorted(images, key=lambda image:
|
||||
getattr(image, 'created_at'), reverse=True)
|
||||
|
||||
except Exception:
|
||||
self._prev = False
|
||||
self._more = False
|
||||
msg = _('Unable to retrieve image list.')
|
||||
exceptions.handle(self.request, msg)
|
||||
|
||||
# TODO(tzumainn): re-architect a bit to avoid inefficiency
|
||||
plan = tuskar_api.tuskar.Plan.get_the_plan(self.request)
|
||||
for image in images:
|
||||
image.role = api.tuskar.Role.get_by_image(
|
||||
image.role = tuskar_api.tuskar.Role.get_by_image(
|
||||
self.request, plan, image)
|
||||
|
||||
return images
|
||||
|
||||
def get_filters(self):
|
||||
filters = {'is_public': None}
|
||||
filter_field = self.table.get_filter_field()
|
||||
filter_string = self.table.get_filter_string()
|
||||
filter_action = self.table._meta._filter_action
|
||||
if filter_field and filter_string and (
|
||||
filter_action.is_api_filter(filter_field)):
|
||||
if filter_field in ['size_min', 'size_max']:
|
||||
invalid_msg = ('API query is not valid and is ignored: %s=%s'
|
||||
% (filter_field, filter_string))
|
||||
try:
|
||||
filter_string = long(float(filter_string) * (1024 ** 2))
|
||||
if filter_string >= 0:
|
||||
filters[filter_field] = filter_string
|
||||
else:
|
||||
LOG.warning(invalid_msg)
|
||||
except ValueError:
|
||||
LOG.warning(invalid_msg)
|
||||
else:
|
||||
filters[filter_field] = filter_string
|
||||
return filters
|
||||
|
||||
|
||||
class CreateView(views.CreateView):
|
||||
template_name = 'infrastructure/images/create.html'
|
||||
form_class = forms.CreateImageForm
|
||||
success_url = reverse_lazy('horizon:infrastructure:images:index')
|
||||
|
||||
|
||||
class UpdateView(views.UpdateView):
|
||||
template_name = 'infrastructure/images/update.html'
|
||||
form_class = forms.UpdateImageForm
|
||||
success_url = reverse_lazy('horizon:infrastructure:images:index')
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_object(self):
|
||||
try:
|
||||
return api.glance.image_get(self.request, self.kwargs['image_id'])
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve image.')
|
||||
url = reverse_lazy('horizon:infrastructure:images:index')
|
||||
exceptions.handle(self.request, msg, redirect=url)
|
||||
|
Loading…
x
Reference in New Issue
Block a user