diff --git a/horizon/templates/horizon/common/_modal.html b/horizon/templates/horizon/common/_modal.html new file mode 100644 index 000000000..34863bf59 --- /dev/null +++ b/horizon/templates/horizon/common/_modal.html @@ -0,0 +1,10 @@ +
diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 3db80395e..68a0d2f21 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -167,6 +167,14 @@ class SecurityGroupRule(APIResourceWrapper): return _('ALLOW %(from)s:%(to)s from %(cidr)s') % vals +class FlavorExtraSpec(object): + def __init__(self, flavor_id, key, val): + self.flavor_id = flavor_id + self.id = key + self.key = key + self.value = val + + def novaclient(request): insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) LOG.debug('novaclient connection created using token "%s" and url "%s"' % @@ -206,6 +214,28 @@ def flavor_list(request): return novaclient(request).flavors.list() +def flavor_get_extras(request, flavor_id, raw=False): + """Get flavor extra specs.""" + flavor = novaclient(request).flavors.get(flavor_id) + extras = flavor.get_keys() + if raw: + return extras + return [FlavorExtraSpec(flavor_id, key, value) for + key, value in extras.items()] + + +def flavor_extra_delete(request, flavor_id, keys): + """Unset the flavor extra spec keys.""" + flavor = novaclient(request).flavors.get(flavor_id) + return flavor.unset_keys(keys) + + +def flavor_extra_set(request, flavor_id, metadata): + """Set the flavor extra spec keys.""" + flavor = novaclient(request).flavors.get(flavor_id) + return flavor.set_keys(metadata) + + def tenant_floating_ip_list(request): """Fetches a list of all floating ips.""" return novaclient(request).floating_ips.list() diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/__init__.py b/openstack_dashboard/dashboards/admin/flavors/extras/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/forms.py b/openstack_dashboard/dashboards/admin/flavors/extras/forms.py new file mode 100644 index 000000000..5cada8ec4 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/forms.py @@ -0,0 +1,66 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright (c) 2012 Intel, 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. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from openstack_dashboard import api +from horizon import exceptions +from horizon import forms +from horizon import messages + +LOG = logging.getLogger(__name__) + + +class CreateExtraSpec(forms.SelfHandlingForm): + key = forms.CharField(max_length="25", label=_("Key")) + value = forms.CharField(max_length="25", label=_("Value")) + flavor_id = forms.IntegerField(widget=forms.widgets.HiddenInput) + + def handle(self, request, data): + try: + api.nova.flavor_extra_set(request, + data['flavor_id'], + {data['key']: data['value']}) + msg = _('Created extra spec "%s".') % data['key'] + messages.success(request, msg) + return True + except: + exceptions.handle(request, + _("Unable to create flavor extra spec.")) + + +class EditExtraSpec(forms.SelfHandlingForm): + key = forms.CharField(max_length="25", label=_("Key")) + value = forms.CharField(max_length="25", label=_("Value")) + flavor_id = forms.IntegerField(widget=forms.widgets.HiddenInput) + + def handle(self, request, data): + flavor_id = data['flavor_id'] + try: + api.nova.flavor_extra_set(request, + flavor_id, + {data['key']: data['value']}) + msg = _('Saved extra spec "%s".') % data['key'] + messages.success(request, msg) + return True + except: + exceptions.handle(request, _("Unable to edit extra spec.")) diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/tables.py b/openstack_dashboard/dashboards/admin/flavors/extras/tables.py new file mode 100644 index 000000000..abb325a57 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/tables.py @@ -0,0 +1,75 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Intel, 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. + +import logging +import re + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +from openstack_dashboard import api + + +LOG = logging.getLogger(__name__) + + +class ExtraSpecDelete(tables.DeleteAction): + data_type_singular = _("ExtraSpec") + data_type_plural = _("ExtraSpecs") + + def delete(self, request, obj_ids): + flavor = api.nova.flavor_get(request, self.table.kwargs['id']) + flavor.unset_keys([obj_ids]) + + +class ExtraSpecCreate(tables.LinkAction): + name = "create" + verbose_name = _("Create") + url = "horizon:admin:flavors:extras:create" + classes = ("btn-create", "ajax-modal") + + def get_link_url(self, extra_spec=None): + return reverse(self.url, args=[self.table.kwargs['id']]) + + +class ExtraSpecEdit(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:admin:flavors:extras:edit" + classes = ("btn-edit", "ajax-modal") + + def get_link_url(self, extra_spec): + return reverse(self.url, args=[self.table.kwargs['id'], + extra_spec.key]) + + +class ExtraSpecsTable(tables.DataTable): + key = tables.Column('key', verbose_name=_('Key')) + value = tables.Column('value', verbose_name=_('Value')) + + class Meta: + name = "extras" + verbose_name = _("Extra Specs") + table_actions = (ExtraSpecCreate, ExtraSpecDelete) + row_actions = (ExtraSpecEdit, ExtraSpecDelete) + + def get_object_id(self, datum): + return datum.key + + def get_object_display(self, datum): + return datum.key diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/tests.py b/openstack_dashboard/dashboards/admin/flavors/extras/tests.py new file mode 100644 index 000000000..a8df96896 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/tests.py @@ -0,0 +1,65 @@ +from django import http +from django.core.urlresolvers import reverse + +from mox import IsA + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +class FlavorExtrasTests(test.BaseAdminViewTests): + + def test_list_extras_when_none_exists(self): + flavor = self.flavors.first() + extras = [api.FlavorExtraSpec(flavor.id, 'k1', 'v1')] + + self.mox.StubOutWithMock(api.nova, 'flavor_get') + self.mox.StubOutWithMock(api.nova, 'flavor_get_extras') + + # GET -- to determine correctness of output + api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + api.nova.flavor_get_extras(IsA(http.HttpRequest), + flavor.id).AndReturn(extras) + self.mox.ReplayAll() + url = reverse('horizon:admin:flavors:extras:index', args=[flavor.id]) + resp = self.client.get(url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, "admin/flavors/extras/index.html") + + def test_extra_create_post(self): + flavor = self.flavors.first() + create_url = reverse('horizon:admin:flavors:extras:create', + args=[flavor.id]) + index_url = reverse('horizon:admin:flavors:extras:index', + args=[flavor.id]) + + self.mox.StubOutWithMock(api.nova, 'flavor_extra_set') + + # GET to display the flavor_name + api.nova.flavor_extra_set(IsA(http.HttpRequest), + int(flavor.id), + {'k1': 'v1'}) + self.mox.ReplayAll() + + data = {'flavor_id': flavor.id, + 'key': 'k1', + 'value': 'v1'} + resp = self.client.post(create_url, data) + self.assertNoFormErrors(resp) + self.assertMessageCount(success=1) + self.assertRedirectsNoFollow(resp, index_url) + + def test_extra_create_get(self): + flavor = self.flavors.first() + create_url = reverse('horizon:admin:flavors:extras:create', + args=[flavor.id]) + + self.mox.StubOutWithMock(api.nova, 'flavor_get') + + api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor) + self.mox.ReplayAll() + + resp = self.client.get(create_url) + self.assertEqual(resp.status_code, 200) + self.assertTemplateUsed(resp, + 'admin/flavors/extras/create.html') diff --git a/openstack_dashboard/dashboards/admin/flavors/extras/urls.py b/openstack_dashboard/dashboards/admin/flavors/extras/urls.py new file mode 100644 index 000000000..b39211a7a --- /dev/null +++ b/openstack_dashboard/dashboards/admin/flavors/extras/urls.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +from django.conf.urls.defaults import patterns, url + +from .views import IndexView, EditView, CreateView + +urlpatterns = patterns('', + url(r'^$', IndexView.as_view(), name='index'), + url(r'^create/$', CreateView.as_view(), name='create'), + url(r'^(?P{% trans 'Create a new "extra spec" key-value pair for a flavor.' %}
+{% trans 'Update an "extra spec" key-value pair for a flavor.' %}
+