Enable flavors metadata update
Nova currently expose an api to let the users update the flavors metadata so horizon should expose this functionality. At the same time Glance is exposing a metadata dictionary in Juno, where users can register the key/value pairs that describe their cloud deployment. This patch uses the glance metadata api to expose the available metadata and the nova api to update the aggregates metadata. Implements: blueprint glaance-metadata-flavors Co-Authored-By: Santiago Baldassin <santiago.b.baldassin@intel.com> Co-Authored-By: Pawel Skowron <pawel.skowron@intel.com> Co-Authored-By: Pawel Koniszewski <pawel.koniszewski@intel.com> Co-Authored-By: Michal Dulko <michal.dulko@intel.com> DocImpact: New UI. FLAVOR_EXTRA_KEYS setting deprecated. Change-Id: Ica9d3d06314a4d5afa77f644bd1f8900a1c328f7
This commit is contained in:
parent
bd998fb609
commit
d5fea888a8
@ -263,26 +263,6 @@ This example sorts flavors by vcpus in descending order::
|
||||
'reverse': True,
|
||||
}
|
||||
|
||||
``FLAVOR_EXTRA_KEYS``
|
||||
---------------------
|
||||
|
||||
.. versionadded:: 2014.1(Icehouse)
|
||||
|
||||
Default::
|
||||
|
||||
{
|
||||
'flavor_keys': [
|
||||
('quota:disk_read_bytes_sec', _('Quota: Read bytes')),
|
||||
('quota:disk_write_bytes_sec', _('Quota: Write bytes')),
|
||||
('quota:cpu_quota', _('Quota: CPU')),
|
||||
('quota:cpu_period', _('Quota: CPU period')),
|
||||
('quota:vif_inbound_average', _('Quota: Inbound average')),
|
||||
('quota:vif_outbound_average', _('Quota: Outbound average'))
|
||||
]
|
||||
}
|
||||
|
||||
Used to customize flavor extra specs keys
|
||||
|
||||
``IMAGES_LIST_FILTER_TENANTS``
|
||||
------------------------------
|
||||
|
||||
|
21
openstack_dashboard/dashboards/admin/flavors/constants.py
Normal file
21
openstack_dashboard/dashboards/admin/flavors/constants.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.
|
||||
|
||||
FLAVORS_TEMPLATE_NAME = 'admin/flavors/index.html'
|
||||
FLAVORS_INDEX_URL = 'horizon:admin:flavors:index'
|
||||
FLAVORS_CREATE_URL = 'horizon:admin:flavors:create'
|
||||
FLAVORS_CREATE_VIEW_TEMPLATE = 'admin/flavors/create.html'
|
||||
FLAVORS_UPDATE_URL = 'horizon:admin:flavors:update'
|
||||
FLAVORS_UPDATE_VIEW_TEMPLATE = 'admin/flavors/update.html'
|
||||
FLAVORS_UPDATE_METADATA_URL = 'horizon:admin:flavors:update_metadata'
|
||||
FLAVORS_UPDATE_METADATA_TEMPLATE = 'admin/flavors/update_metadata.html'
|
||||
FLAVORS_UPDATE_METADATA_SUBTEMPLATE = 'admin/flavors/_update_metadata.html'
|
@ -1,98 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from django.conf import settings
|
||||
from django.forms import ValidationError # noqa
|
||||
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
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class CreateExtraSpec(forms.SelfHandlingForm):
|
||||
_extraspec_name_regex = re.compile(r"^[\w\.\-: ]+$", re.UNICODE)
|
||||
keys = forms.ChoiceField(label=_("Keys"),
|
||||
widget=forms.Select(attrs={
|
||||
'class': 'switchable',
|
||||
'data-slug': 'keys'}))
|
||||
key = forms.RegexField(
|
||||
max_length=255,
|
||||
label=_("Key"),
|
||||
required=False,
|
||||
regex=_extraspec_name_regex,
|
||||
error_messages={'invalid': _('Key Name may only contain letters, '
|
||||
'numbers, underscores, periods, colons, '
|
||||
'spaces and hyphens.')},
|
||||
widget=forms.TextInput(attrs={
|
||||
'class': 'switched',
|
||||
'data-switch-on': 'keys',
|
||||
'data-keys-custom': _('Key')}))
|
||||
value = forms.CharField(max_length=255, label=_("Value"))
|
||||
flavor_id = forms.CharField(widget=forms.widgets.HiddenInput)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateExtraSpec, self).__init__(*args, **kwargs)
|
||||
key_settings = getattr(settings, 'FLAVOR_EXTRA_KEYS', {})
|
||||
key_list = key_settings.get('flavor_keys', [])
|
||||
self.fields['keys'].choices = key_list + [('custom', _('Other Key'))]
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(CreateExtraSpec, self).clean()
|
||||
keys = cleaned_data.get('keys', None)
|
||||
key = cleaned_data.get('key', None)
|
||||
if keys == 'custom' and key == "":
|
||||
msg = _('This field is required.')
|
||||
self._errors["key"] = self.error_class([msg])
|
||||
return cleaned_data
|
||||
|
||||
def handle(self, request, data):
|
||||
if data["keys"] != 'custom':
|
||||
data['key'] = data['keys']
|
||||
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 Exception:
|
||||
exceptions.handle(request,
|
||||
_("Unable to create flavor extra spec."))
|
||||
|
||||
|
||||
class EditExtraSpec(forms.SelfHandlingForm):
|
||||
key = forms.CharField(widget=forms.widgets.HiddenInput)
|
||||
value = forms.CharField(max_length=255, label=_("Value"))
|
||||
flavor_id = forms.CharField(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 Exception:
|
||||
exceptions.handle(request, _("Unable to edit extra spec."))
|
@ -1,69 +0,0 @@
|
||||
# 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.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
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 = ("ajax-modal",)
|
||||
icon = "plus"
|
||||
|
||||
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 = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
|
||||
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
|
@ -1,127 +0,0 @@
|
||||
# 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.core.urlresolvers import reverse
|
||||
from django import http
|
||||
|
||||
from mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
class FlavorExtrasTests(test.BaseAdminViewTests):
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_get_extras',
|
||||
'flavor_get'), })
|
||||
def test_list_extras_when_none_exists(self):
|
||||
flavor = self.flavors.first()
|
||||
extras = [api.nova.FlavorExtraSpec(flavor.id, 'k1', 'v1')]
|
||||
|
||||
# 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")
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_extra_set', ), })
|
||||
def _generic_extra_create_post(self, key_name):
|
||||
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])
|
||||
|
||||
# GET to display the flavor_name
|
||||
api.nova.flavor_extra_set(IsA(http.HttpRequest),
|
||||
flavor.id,
|
||||
{key_name: 'v1'})
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = {'flavor_id': flavor.id,
|
||||
'keys': 'custom',
|
||||
'key': key_name,
|
||||
'value': 'v1'}
|
||||
resp = self.client.post(create_url, data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertRedirectsNoFollow(resp, index_url)
|
||||
self.mox.UnsetStubs()
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_extra_set', ), })
|
||||
def test_extra_create_with_template(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])
|
||||
|
||||
# GET to display the flavor_name
|
||||
api.nova.flavor_extra_set(IsA(http.HttpRequest),
|
||||
flavor.id,
|
||||
{'quota:disk_read_bytes_sec': '1000'})
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = {'flavor_id': flavor.id,
|
||||
'keys': 'quota:disk_read_bytes_sec',
|
||||
'value': '1000'}
|
||||
resp = self.client.post(create_url, data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertRedirectsNoFollow(resp, index_url)
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_get', ), })
|
||||
def test_extra_create_get(self):
|
||||
flavor = self.flavors.first()
|
||||
create_url = reverse('horizon:admin:flavors:extras:create',
|
||||
args=[flavor.id])
|
||||
|
||||
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')
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_get', ), })
|
||||
def _generic_extra_create_names_format_fail(self, key_name):
|
||||
flavor = self.flavors.first()
|
||||
create_url = reverse('horizon:admin:flavors:extras:create',
|
||||
args=[flavor.id])
|
||||
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
data = {'flavor_id': flavor.id,
|
||||
'keys': 'custom',
|
||||
'key': key_name,
|
||||
'value': 'v1'}
|
||||
|
||||
resp = self.client.post(create_url, data)
|
||||
msg = ('Name may only contain letters, numbers, underscores, periods, '
|
||||
'colons, spaces and hyphens.')
|
||||
|
||||
self.assertFormErrors(resp, 1, msg)
|
||||
self.mox.UnsetStubs()
|
||||
|
||||
def test_create_extra_key_names_valid_formats(self):
|
||||
valid_keys = ("key1", "month.price", "I-Am:AK-ey. 22-")
|
||||
for x in valid_keys:
|
||||
self._generic_extra_create_post(key_name=x)
|
||||
|
||||
def test_create_extra_key_names_invalid_formats(self):
|
||||
invalid_keys = ("key1/", "<key>", "$$akey$", "!akey")
|
||||
for x in invalid_keys:
|
||||
self._generic_extra_create_names_format_fail(key_name=x)
|
@ -1,28 +0,0 @@
|
||||
# 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 import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.flavors.extras import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<key>[^/]+)/edit/$', views.EditView.as_view(), name='edit')
|
||||
)
|
@ -1,100 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from openstack_dashboard.dashboards.admin.flavors.extras \
|
||||
import forms as project_forms
|
||||
from openstack_dashboard.dashboards.admin.flavors.extras \
|
||||
import tables as project_tables
|
||||
|
||||
|
||||
class ExtraSpecMixin(object):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ExtraSpecMixin, self).get_context_data(**kwargs)
|
||||
try:
|
||||
context['flavor'] = api.nova.flavor_get(self.request,
|
||||
self.kwargs['id'])
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve flavor details."))
|
||||
if 'key' in self.kwargs:
|
||||
context['key'] = self.kwargs['key']
|
||||
return context
|
||||
|
||||
|
||||
class IndexView(ExtraSpecMixin, forms.ModalFormMixin, tables.DataTableView):
|
||||
table_class = project_tables.ExtraSpecsTable
|
||||
template_name = 'admin/flavors/extras/index.html'
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
flavor_id = self.kwargs['id']
|
||||
extras_list = api.nova.flavor_get_extras(self.request, flavor_id)
|
||||
extras_list.sort(key=lambda es: (es.key,))
|
||||
except Exception:
|
||||
extras_list = []
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve extra spec list.'))
|
||||
return extras_list
|
||||
|
||||
|
||||
class CreateView(ExtraSpecMixin, forms.ModalFormView):
|
||||
form_class = project_forms.CreateExtraSpec
|
||||
template_name = 'admin/flavors/extras/create.html'
|
||||
|
||||
def get_initial(self):
|
||||
return {'flavor_id': self.kwargs['id']}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("horizon:admin:flavors:extras:index",
|
||||
args=(self.kwargs["id"],))
|
||||
|
||||
|
||||
class EditView(ExtraSpecMixin, forms.ModalFormView):
|
||||
form_class = project_forms.EditExtraSpec
|
||||
template_name = 'admin/flavors/extras/edit.html'
|
||||
success_url = 'horizon:admin:flavors:extras:index'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(self.success_url,
|
||||
args=(self.kwargs['id'],))
|
||||
|
||||
def get_initial(self):
|
||||
flavor_id = self.kwargs['id']
|
||||
key = self.kwargs['key']
|
||||
try:
|
||||
extra_specs = api.nova.flavor_get_extras(self.request,
|
||||
flavor_id,
|
||||
raw=True)
|
||||
except Exception:
|
||||
extra_specs = {}
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve flavor extra spec '
|
||||
'details.'))
|
||||
return {'flavor_id': flavor_id,
|
||||
'key': key,
|
||||
'value': extra_specs.get(key, '')}
|
50
openstack_dashboard/dashboards/admin/flavors/forms.py
Normal file
50
openstack_dashboard/dashboards/admin/flavors/forms.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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 json
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
class UpdateMetadataForm(forms.SelfHandlingForm):
|
||||
|
||||
def handle(self, request, data):
|
||||
id = self.initial['id']
|
||||
old_metadata = self.initial['metadata']
|
||||
|
||||
try:
|
||||
new_metadata = json.loads(self.data['metadata'])
|
||||
|
||||
metadata = dict(
|
||||
(item['key'], str(item['value']))
|
||||
for item in new_metadata
|
||||
)
|
||||
api.nova.flavor_extra_set(request, id, metadata)
|
||||
|
||||
remove_keys = [key for key in old_metadata if key not in metadata]
|
||||
|
||||
api.nova.flavor_extra_delete(request, id, remove_keys)
|
||||
|
||||
message = _('Metadata successfully updated.')
|
||||
messages.success(request, message)
|
||||
except Exception:
|
||||
exceptions.handle(request,
|
||||
_('Unable to update the flavor metadata.'))
|
||||
return False
|
||||
return True
|
@ -50,10 +50,11 @@ class UpdateFlavor(tables.LinkAction):
|
||||
icon = "pencil"
|
||||
|
||||
|
||||
class ViewFlavorExtras(tables.LinkAction):
|
||||
name = "extras"
|
||||
verbose_name = _("View Extra Specs")
|
||||
url = "horizon:admin:flavors:extras:index"
|
||||
class UpdateMetadata(tables.LinkAction):
|
||||
url = "horizon:admin:flavors:update_metadata"
|
||||
name = "update_metadata"
|
||||
verbose_name = _("Update Metadata")
|
||||
classes = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
|
||||
|
||||
@ -124,7 +125,8 @@ class FlavorsTable(tables.DataTable):
|
||||
filters=(filters.yesno, filters.capfirst))
|
||||
extra_specs = tables.Column(get_extra_specs,
|
||||
verbose_name=_("Extra Specs"),
|
||||
link=("horizon:admin:flavors:extras:index"),
|
||||
link="horizon:admin:flavors:update_metadata",
|
||||
link_classes=("ajax-modal",),
|
||||
empty_value=False,
|
||||
filters=(filters.yesno, filters.capfirst))
|
||||
|
||||
@ -134,5 +136,5 @@ class FlavorsTable(tables.DataTable):
|
||||
table_actions = (FlavorFilterAction, CreateFlavor, DeleteFlavor)
|
||||
row_actions = (UpdateFlavor,
|
||||
ModifyAccess,
|
||||
ViewFlavorExtras,
|
||||
UpdateMetadata,
|
||||
DeleteFlavor)
|
||||
|
@ -0,0 +1,11 @@
|
||||
{% extends 'horizon/common/_modal_form_update_metadata.html' %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Update Flavor Metadata") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block form_action %}{% url 'horizon:admin:flavors:update_metadata' id %}{% endblock %}
|
||||
{% block modal-header %}{% trans "Update Metadata" %}{% endblock %}
|
@ -1,28 +0,0 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}extra_spec_create_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:flavors:extras:create' flavor.id %}{% endblock %}
|
||||
|
||||
|
||||
{% block modal_id %}extra_spec_create_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Flavor Extra Spec" %}{% 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 'Create a new "extra spec" key-value pair for a flavor.' %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create" %}" />
|
||||
<a href="{% url 'horizon:admin:flavors:extras:index' flavor.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -1,28 +0,0 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}extra_spec_edit_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:flavors:extras:edit' flavor.id key %}{% endblock %}
|
||||
|
||||
|
||||
{% block modal_id %}extra_spec_edit_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Edit Extra Spec Value" %}: {{ key }}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>{% trans "Description" %}:</h3>
|
||||
<p>{% blocktrans %}Update the "extra spec" value for "{{ key }}"{% endblocktrans %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
|
||||
<a href="{% url 'horizon:admin:flavors:extras:index' flavor.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -1,15 +0,0 @@
|
||||
{% extends "horizon/common/_modal.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block modal_id %}extra_specs_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Flavor Extra Specs" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
{{ table.render }}
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<a href="{% url 'horizon:admin:flavors:index' %}" class="btn btn-default secondary cancel close">{% trans "Close" %}</a>
|
||||
{% endblock %}
|
||||
|
@ -1,12 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Create Flavor Extra Spec" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/flavors/extras/_create.html" %}
|
||||
{% endblock %}
|
@ -1,12 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Edit Flavor Extra Spec" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/flavors/extras/_edit.html" %}
|
||||
{% endblock %}
|
@ -1,12 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Flavor Extra Specs" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
<h2>{% trans "Flavor" %}: {{flavor.name}} </h2>
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/flavors/extras/_index.html" %}
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Update Flavor Metadata" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Update Flavor Metadata") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/flavors/_update_metadata.html' %}
|
||||
{% endblock %}
|
@ -10,6 +10,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from mox import IsA # noqa
|
||||
@ -19,10 +21,9 @@ from openstack_dashboard.test import helpers as test
|
||||
|
||||
from novaclient.v1_1 import flavors
|
||||
|
||||
from openstack_dashboard.dashboards.admin.flavors import constants
|
||||
from openstack_dashboard.dashboards.admin.flavors import workflows
|
||||
|
||||
INDEX_URL = reverse('horizon:admin:flavors:index')
|
||||
|
||||
|
||||
class FlavorsViewTests(test.BaseAdminViewTests):
|
||||
@test.create_stubs({api.nova: ('flavor_list',),
|
||||
@ -33,8 +34,8 @@ class FlavorsViewTests(test.BaseAdminViewTests):
|
||||
flavors.Flavor.get_keys().MultipleTimes().AndReturn({})
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'admin/flavors/index.html')
|
||||
res = self.client.get(reverse(constants.FLAVORS_INDEX_URL))
|
||||
self.assertTemplateUsed(res, constants.FLAVORS_TEMPLATE_NAME)
|
||||
self.assertItemsEqual(res.context['table'].data, self.flavors.list())
|
||||
|
||||
|
||||
@ -82,9 +83,9 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
False])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.get(url)
|
||||
self.assertTemplateUsed(res, 'admin/flavors/create.html')
|
||||
self.assertTemplateUsed(res, constants.FLAVORS_CREATE_VIEW_TEMPLATE)
|
||||
workflow = res.context['workflow']
|
||||
expected_name = workflows.CreateFlavor.name
|
||||
self.assertEqual(res.context['workflow'].name, expected_name)
|
||||
@ -114,11 +115,11 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
|
||||
workflow_data = self._get_workflow_data(flavor)
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_list',
|
||||
@ -146,11 +147,11 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
|
||||
workflow_data = self._get_workflow_data(flavor, access=projects)
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_list',)})
|
||||
@ -169,7 +170,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
|
||||
workflow_data = self._get_workflow_data(flavor)
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertFormErrors(res)
|
||||
@ -195,7 +196,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
# Flavor id already exists.
|
||||
workflow_data['flavor_id'] = flavor.id
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertFormErrors(res)
|
||||
@ -228,12 +229,12 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
|
||||
workflow_data = self._get_workflow_data(flavor, access=projects)
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(error=1, warning=0)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_list',)})
|
||||
@ -251,7 +252,7 @@ class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
workflow_data = self._get_workflow_data(flavor)
|
||||
workflow_data["name"] = ""
|
||||
|
||||
url = reverse('horizon:admin:flavors:create')
|
||||
url = reverse(constants.FLAVORS_CREATE_URL)
|
||||
res = self.client.post(url, workflow_data)
|
||||
|
||||
self.assertFormErrors(res)
|
||||
@ -278,10 +279,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
# Put all mocks created by mox into replay mode
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(res, 'admin/flavors/update.html')
|
||||
self.assertTemplateUsed(res, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
workflow = res.context['workflow']
|
||||
expected_name = workflows.UpdateFlavor.name
|
||||
@ -314,10 +315,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_get',
|
||||
@ -368,10 +369,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
workflow_data = {'flavor_id': flavor.id,
|
||||
@ -385,7 +386,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
resp = self.client.post(url, workflow_data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(resp,
|
||||
reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_get',
|
||||
@ -438,10 +440,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
workflow_data = {'flavor_id': flavor.id,
|
||||
@ -455,7 +457,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
resp = self.client.post(url, workflow_data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(success=1)
|
||||
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(resp,
|
||||
reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_get',
|
||||
@ -509,10 +512,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
workflow_data = {'flavor_id': flavor.id,
|
||||
@ -526,7 +529,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
resp = self.client.post(url, workflow_data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(error=1)
|
||||
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(resp,
|
||||
reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_get',
|
||||
@ -591,10 +595,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
data = self._get_workflow_data(new_flavor, access=flavor_projects)
|
||||
@ -602,7 +606,8 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
resp = self.client.post(url, data)
|
||||
self.assertNoFormErrors(resp)
|
||||
self.assertMessageCount(error=1, warning=0)
|
||||
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||
self.assertRedirectsNoFollow(resp,
|
||||
reverse(constants.FLAVORS_INDEX_URL))
|
||||
|
||||
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||
api.nova: ('flavor_get',
|
||||
@ -624,10 +629,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
workflow_data = {'flavor_id': flavor.id,
|
||||
@ -673,10 +678,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor_a.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor_a.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# post test
|
||||
data = {'flavor_id': new_flavor.id,
|
||||
@ -710,10 +715,10 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# run get test
|
||||
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||
url = reverse(constants.FLAVORS_UPDATE_URL, args=[flavor.id])
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||
self.assertTemplateUsed(resp, constants.FLAVORS_UPDATE_VIEW_TEMPLATE)
|
||||
|
||||
# run post test
|
||||
workflow_data = {'flavor_id': flavor.id,
|
||||
@ -757,3 +762,94 @@ class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||
data = {'eph_gb': -1}
|
||||
self.generic_update_flavor_invalid_data_form_fails(override_data=data,
|
||||
error_msg=error)
|
||||
|
||||
|
||||
class FlavorUpdateMetadataViewTest(test.BaseAdminViewTests):
|
||||
@test.create_stubs({api.nova: ('flavor_get_extras',),
|
||||
api.glance: ('metadefs_namespace_list',
|
||||
'metadefs_namespace_get')})
|
||||
def test_flavor_metadata_get(self):
|
||||
# <Flavor: m1.metadata>
|
||||
flavor = self.flavors.list()[3]
|
||||
|
||||
namespaces = self.metadata_defs.list()
|
||||
|
||||
api.nova.flavor_get_extras(
|
||||
IsA(http.HttpRequest),
|
||||
flavor.id
|
||||
).AndReturn([flavor.extra_specs])
|
||||
api.glance.metadefs_namespace_list(
|
||||
IsA(http.HttpRequest),
|
||||
filters={
|
||||
'resource_types': ['OS::Nova::Flavor']
|
||||
}
|
||||
).AndReturn((namespaces, False, False))
|
||||
|
||||
for namespace in namespaces:
|
||||
api.glance.metadefs_namespace_get(
|
||||
IsA(http.HttpRequest),
|
||||
namespace.namespace,
|
||||
'OS::Nova::Flavor'
|
||||
).AndReturn(namespace)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
res = self.client.get(
|
||||
reverse(
|
||||
constants.FLAVORS_UPDATE_METADATA_URL,
|
||||
kwargs={'id': flavor.id}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(
|
||||
res,
|
||||
constants.FLAVORS_UPDATE_METADATA_TEMPLATE
|
||||
)
|
||||
self.assertTemplateUsed(
|
||||
res,
|
||||
constants.FLAVORS_UPDATE_METADATA_SUBTEMPLATE
|
||||
)
|
||||
self.assertContains(res, 'namespace_1')
|
||||
self.assertContains(res, 'namespace_2')
|
||||
self.assertContains(res, 'namespace_3')
|
||||
self.assertContains(res, 'namespace_4')
|
||||
|
||||
@test.create_stubs({api.nova: ('flavor_get_extras',
|
||||
'flavor_extra_set',
|
||||
'flavor_extra_delete')})
|
||||
def test_flavor_metadata_update(self):
|
||||
# <Flavor: m1.metadata>
|
||||
flavor = self.flavors.list()[3]
|
||||
|
||||
api.nova.flavor_get_extras(
|
||||
IsA(http.HttpRequest),
|
||||
flavor.id
|
||||
).AndReturn([flavor.extra_specs])
|
||||
api.nova.flavor_extra_set(
|
||||
IsA(http.HttpRequest),
|
||||
flavor.id,
|
||||
{'key_mock': 'value_mock'}
|
||||
).AndReturn(None)
|
||||
api.nova.flavor_extra_delete(
|
||||
IsA(http.HttpRequest),
|
||||
flavor.id,
|
||||
[]
|
||||
).AndReturn(None)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
metadata = [{'value': 'value_mock', 'key': 'key_mock'}]
|
||||
formData = {'metadata': json.dumps(metadata)}
|
||||
|
||||
res = self.client.post(
|
||||
reverse(
|
||||
constants.FLAVORS_UPDATE_METADATA_URL,
|
||||
kwargs={'id': flavor.id}
|
||||
),
|
||||
formData
|
||||
)
|
||||
|
||||
self.assertEqual(res.status_code, 302)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, reverse(constants.FLAVORS_INDEX_URL))
|
||||
self.assertMessageCount(success=1)
|
||||
|
@ -16,18 +16,16 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import include # noqa
|
||||
from django.conf.urls import patterns # noqa
|
||||
from django.conf.urls import url # noqa
|
||||
|
||||
from openstack_dashboard.dashboards.admin.flavors.extras \
|
||||
import urls as extras_urls
|
||||
from openstack_dashboard.dashboards.admin.flavors import views
|
||||
|
||||
|
||||
urlpatterns = patterns('openstack_dashboard.dashboards.admin.flavors.views',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<id>[^/]+)/update_metadata/$',
|
||||
views.UpdateMetadataView.as_view(), name='update_metadata'),
|
||||
url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'),
|
||||
url(r'^(?P<id>[^/]+)/extras/', include(extras_urls, namespace='extras')),
|
||||
)
|
||||
|
@ -16,15 +16,21 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from horizon.utils import memoized
|
||||
from horizon import workflows
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from openstack_dashboard.dashboards.admin.flavors \
|
||||
import forms as project_forms
|
||||
from openstack_dashboard.dashboards.admin.flavors \
|
||||
import tables as project_tables
|
||||
from openstack_dashboard.dashboards.admin.flavors \
|
||||
@ -78,3 +84,60 @@ class UpdateView(workflows.WorkflowView):
|
||||
'disk_gb': flavor.disk,
|
||||
'swap_mb': flavor.swap or 0,
|
||||
'eph_gb': getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral', None)}
|
||||
|
||||
|
||||
class UpdateMetadataView(forms.ModalFormView):
|
||||
template_name = "admin/flavors/update_metadata.html"
|
||||
form_class = project_forms.UpdateMetadataForm
|
||||
success_url = reverse_lazy('horizon:admin:flavors:index')
|
||||
|
||||
def get_initial(self):
|
||||
extra_specs = self.get_object()
|
||||
extra_specs_dict = dict((i.key, i.value)
|
||||
for i in extra_specs)
|
||||
return {'id': self.kwargs["id"], 'metadata': extra_specs_dict}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UpdateMetadataView, self).get_context_data(**kwargs)
|
||||
|
||||
extra_specs = self.get_object()
|
||||
extra_specs_dict = dict((i.key, i.value) for i in extra_specs)
|
||||
|
||||
try:
|
||||
context['existing_metadata'] = json.dumps(extra_specs_dict)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve flavor metadata.'))
|
||||
|
||||
resource_type = 'OS::Nova::Flavor'
|
||||
try:
|
||||
metadata = {}
|
||||
# metadefs_namespace_list() returns a tuple with list as 1st elem
|
||||
metadata["namespaces"] = [
|
||||
api.glance.metadefs_namespace_get(self.request, x.namespace,
|
||||
resource_type)
|
||||
for x in api.glance.metadefs_namespace_list(
|
||||
self.request,
|
||||
filters={"resource_types": [resource_type]}
|
||||
)[0]
|
||||
]
|
||||
|
||||
context['available_metadata'] = json.dumps(metadata)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve available metadata for '
|
||||
'flavors.')
|
||||
exceptions.handle(self.request, msg)
|
||||
|
||||
context['id'] = self.kwargs['id']
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_object(self):
|
||||
flavor_id = self.kwargs['id']
|
||||
try:
|
||||
extra_specs = api.nova.flavor_get_extras(self.request, flavor_id)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve the flavor metadata.')
|
||||
exceptions.handle(self.request, msg)
|
||||
else:
|
||||
return extra_specs
|
||||
|
@ -1281,6 +1281,7 @@ class InstanceTests(helpers.TestCase):
|
||||
if custom_flavor_sort == 'id':
|
||||
# Reverse sorted by id
|
||||
sorted_flavors = (
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
|
||||
@ -1288,6 +1289,7 @@ class InstanceTests(helpers.TestCase):
|
||||
elif custom_flavor_sort == 'name':
|
||||
sorted_flavors = (
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
|
||||
)
|
||||
@ -1296,6 +1298,7 @@ class InstanceTests(helpers.TestCase):
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
|
||||
)
|
||||
else:
|
||||
# Default - sorted by RAM
|
||||
@ -1303,6 +1306,7 @@ class InstanceTests(helpers.TestCase):
|
||||
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
|
||||
('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
|
||||
('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
|
||||
('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
|
||||
)
|
||||
|
||||
select_options = '\n'.join([
|
||||
|
@ -507,22 +507,19 @@ SECURITY_GROUP_RULES = {
|
||||
},
|
||||
}
|
||||
|
||||
FLAVOR_EXTRA_KEYS = {
|
||||
'flavor_keys': [
|
||||
('quota:disk_read_bytes_sec', _('Quota: Read bytes')),
|
||||
('quota:disk_write_bytes_sec', _('Quota: Write bytes')),
|
||||
('quota:cpu_quota', _('Quota: CPU')),
|
||||
('quota:cpu_period', _('Quota: CPU period')),
|
||||
('quota:vif_inbound_average', _('Quota: Inbound average')),
|
||||
('quota:vif_outbound_average', _('Quota: Outbound average')),
|
||||
('hw:cpu_sockets', _('Quota: CPU sockets')),
|
||||
('hw:cpu_cores', _('Quota: CPU cores')),
|
||||
('hw:cpu_threads', _('Quota: CPU threads')),
|
||||
('hw:cpu_max_sockets', _('Quota: Max CPU sockets')),
|
||||
('hw:cpu_max_cores', _('Quota: Max CPU cores')),
|
||||
('hw:cpu_max_threads', _('Quota: Max CPU threads')),
|
||||
]
|
||||
}
|
||||
# Deprecation Notice:
|
||||
#
|
||||
# The setting FLAVOR_EXTRA_KEYS has been deprecated.
|
||||
# Please load extra spec metadata into the Glance Metadata Definition Catalog.
|
||||
#
|
||||
# The sample quota definitions can be found in:
|
||||
# <glance_source>/etc/metadefs/compute-quota.json
|
||||
#
|
||||
# The metadata definition catalog supports CLI and API:
|
||||
# $glance --os-image-api-version 2 help md-namespace-import
|
||||
# $glance-manage db_load_metadefs <directory_with_definition_files>
|
||||
#
|
||||
# See Metadata Definitions on: http://docs.openstack.org/developer/glance/
|
||||
|
||||
# Indicate to the Sahara data processing service whether or not
|
||||
# automatic floating IP allocation is in effect. If it is not
|
||||
|
@ -436,6 +436,7 @@ def my_custom_sort(flavor):
|
||||
'm1.secret': 0,
|
||||
'm1.tiny': 1,
|
||||
'm1.massive': 2,
|
||||
'm1.metadata': 3,
|
||||
}
|
||||
return sort_order[flavor.name]
|
||||
|
||||
|
@ -200,16 +200,5 @@ POLICY_FILES = {
|
||||
'compute': 'nova_policy.json'
|
||||
}
|
||||
|
||||
FLAVOR_EXTRA_KEYS = {
|
||||
'flavor_keys': [
|
||||
('quota:disk_read_bytes_sec', 'Quota: Read bytes'),
|
||||
('quota:disk_write_bytes_sec', 'Quota: Write bytes'),
|
||||
('quota:cpu_quota', 'Quota: CPU'),
|
||||
('quota:cpu_period', 'Quota: CPU period'),
|
||||
('quota:vif_inbound_average', 'Quota: Inbound average'),
|
||||
('quota:vif_outbound_average', 'Quota: Outbound average'),
|
||||
]
|
||||
}
|
||||
|
||||
# The openstack_auth.user.Token object isn't JSON-serializable ATM
|
||||
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
|
||||
|
@ -41,6 +41,17 @@ from openstack_dashboard.usage import quotas as usage_quotas
|
||||
from openstack_dashboard.test.test_data import utils
|
||||
|
||||
|
||||
class FlavorExtraSpecs(dict):
|
||||
def __repr__(self):
|
||||
return "<FlavorExtraSpecs %s>" % self._info
|
||||
|
||||
def __init__(self, info):
|
||||
super(FlavorExtraSpecs, self).__init__()
|
||||
self.__dict__.update(info)
|
||||
self.update(info)
|
||||
self._info = info
|
||||
|
||||
|
||||
SERVER_DATA = """
|
||||
{
|
||||
"server": {
|
||||
@ -271,7 +282,19 @@ def data(TEST):
|
||||
'extra_specs': {},
|
||||
'os-flavor-access:is_public': False,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
TEST.flavors.add(flavor_1, flavor_2, flavor_3)
|
||||
flavor_4 = flavors.Flavor(flavors.FlavorManager(None),
|
||||
{'id': "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
|
||||
'name': 'm1.metadata',
|
||||
'vcpus': 1000,
|
||||
'disk': 1024,
|
||||
'ram': 10000,
|
||||
'swap': 0,
|
||||
'extra_specs': FlavorExtraSpecs(
|
||||
{'key': 'key_mock',
|
||||
'value': 'value_mock'}),
|
||||
'os-flavor-access:is_public': False,
|
||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||
TEST.flavors.add(flavor_1, flavor_2, flavor_3, flavor_4)
|
||||
|
||||
flavor_access_manager = flavor_access.FlavorAccessManager(None)
|
||||
flavor_access_1 = flavor_access.FlavorAccess(flavor_access_manager,
|
||||
|
Loading…
Reference in New Issue
Block a user