Define flavor for each project
Change CreateFlavor/EditFlavor forms to CreateFlavor/UpdateFlavor workflows. Provide access flavor management in each project. Add the filtering function in Flavors page. Implements blueprint define-flavor-for-project Change-Id: I723bbf250a46af8de58de06a907f6efc737778db
This commit is contained in:
parent
5534576f43
commit
9f95409249
|
@ -144,7 +144,7 @@ class Action(forms.Form):
|
||||||
return force_unicode(self.name)
|
return force_unicode(self.name)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: %s>" % (self.__class__.__name_get_step_, self.slug)
|
return "<%s: %s>" % (self.__class__.__name__, self.slug)
|
||||||
|
|
||||||
def _populate_choices(self, request, context):
|
def _populate_choices(self, request, context):
|
||||||
for field_name, bound_field in self.fields.items():
|
for field_name, bound_field in self.fields.items():
|
||||||
|
|
|
@ -368,11 +368,11 @@ def server_spice_console(request, instance_id, console_type='spice-html5'):
|
||||||
|
|
||||||
|
|
||||||
def flavor_create(request, name, memory, vcpu, disk, flavorid='auto',
|
def flavor_create(request, name, memory, vcpu, disk, flavorid='auto',
|
||||||
ephemeral=0, swap=0, metadata=None):
|
ephemeral=0, swap=0, metadata=None, is_public=True):
|
||||||
flavor = novaclient(request).flavors.create(name, memory, vcpu, disk,
|
flavor = novaclient(request).flavors.create(name, memory, vcpu, disk,
|
||||||
flavorid=flavorid,
|
flavorid=flavorid,
|
||||||
ephemeral=ephemeral,
|
ephemeral=ephemeral,
|
||||||
swap=swap)
|
swap=swap, is_public=is_public)
|
||||||
if (metadata):
|
if (metadata):
|
||||||
flavor_extra_set(request, flavor.id, metadata)
|
flavor_extra_set(request, flavor.id, metadata)
|
||||||
return flavor
|
return flavor
|
||||||
|
@ -387,9 +387,27 @@ def flavor_get(request, flavor_id):
|
||||||
|
|
||||||
|
|
||||||
@memoized
|
@memoized
|
||||||
def flavor_list(request):
|
def flavor_list(request, is_public=True):
|
||||||
"""Get the list of available instance sizes (flavors)."""
|
"""Get the list of available instance sizes (flavors)."""
|
||||||
return novaclient(request).flavors.list()
|
return novaclient(request).flavors.list(is_public=is_public)
|
||||||
|
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def flavor_access_list(request, flavor=None):
|
||||||
|
"""Get the list of access instance sizes (flavors)."""
|
||||||
|
return novaclient(request).flavor_access.list(flavor=flavor)
|
||||||
|
|
||||||
|
|
||||||
|
def add_tenant_to_flavor(request, flavor, tenant):
|
||||||
|
"""Add a tenant to the given flavor access list."""
|
||||||
|
return novaclient(request).flavor_access.add_tenant_access(
|
||||||
|
flavor=flavor, tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_tenant_from_flavor(request, flavor, tenant):
|
||||||
|
"""Remove a tenant from the given flavor access list."""
|
||||||
|
return novaclient(request).flavor_access.remove_tenant_access(
|
||||||
|
flavor=flavor, tenant=tenant)
|
||||||
|
|
||||||
|
|
||||||
def flavor_get_extras(request, flavor_id, raw=False):
|
def flavor_get_extras(request, flavor_id, raw=False):
|
||||||
|
|
|
@ -1,144 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
|
||||||
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms
|
|
||||||
from horizon import messages
|
|
||||||
|
|
||||||
from openstack_dashboard import api
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateFlavor(forms.SelfHandlingForm):
|
|
||||||
_flavor_id_regex = (r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-'
|
|
||||||
r'[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9]+|auto$')
|
|
||||||
_flavor_id_help_text = _("Flavor ID should be UUID4 or integer. "
|
|
||||||
"Leave this field blank or use 'auto' to set "
|
|
||||||
"a random UUID4.")
|
|
||||||
name = forms.RegexField(label=_("Name"),
|
|
||||||
max_length=25,
|
|
||||||
regex=r'^[\w\.\- ]+$',
|
|
||||||
error_messages={'invalid': _('Name may only '
|
|
||||||
'contain letters, numbers, underscores, '
|
|
||||||
'periods and hyphens.')})
|
|
||||||
flavor_id = forms.RegexField(label=_("ID"),
|
|
||||||
regex=_flavor_id_regex,
|
|
||||||
required=False,
|
|
||||||
initial='auto',
|
|
||||||
help_text=_flavor_id_help_text)
|
|
||||||
vcpus = forms.IntegerField(label=_("VCPUs"))
|
|
||||||
memory_mb = forms.IntegerField(label=_("RAM MB"))
|
|
||||||
disk_gb = forms.IntegerField(label=_("Root Disk GB"))
|
|
||||||
eph_gb = forms.IntegerField(label=_("Ephemeral Disk GB"))
|
|
||||||
swap_mb = forms.IntegerField(label=_("Swap Disk MB"))
|
|
||||||
|
|
||||||
def clean_name(self):
|
|
||||||
name = self.cleaned_data.get('name')
|
|
||||||
try:
|
|
||||||
flavors = api.nova.flavor_list(self.request)
|
|
||||||
except Exception:
|
|
||||||
flavors = []
|
|
||||||
msg = _('Unable to get flavor list')
|
|
||||||
exceptions.check_message(["Connection", "refused"], msg)
|
|
||||||
raise
|
|
||||||
if flavors is not None:
|
|
||||||
for flavor in flavors:
|
|
||||||
if flavor.name == name:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_('The name "%s" is already used by another flavor.')
|
|
||||||
% name
|
|
||||||
)
|
|
||||||
return name
|
|
||||||
|
|
||||||
def clean_flavor_id(self):
|
|
||||||
flavor_id = self.data.get('flavor_id')
|
|
||||||
try:
|
|
||||||
flavors = api.nova.flavor_list(self.request)
|
|
||||||
except Exception:
|
|
||||||
flavors = []
|
|
||||||
msg = _('Unable to get flavor list')
|
|
||||||
exceptions.check_message(["Connection", "refused"], msg)
|
|
||||||
raise
|
|
||||||
if flavors is not None:
|
|
||||||
for flavor in flavors:
|
|
||||||
if flavor.id == flavor_id:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_('The ID "%s" is already used by another flavor.')
|
|
||||||
% flavor_id
|
|
||||||
)
|
|
||||||
return flavor_id
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
try:
|
|
||||||
flavor = api.nova.flavor_create(request,
|
|
||||||
data['name'],
|
|
||||||
data['memory_mb'],
|
|
||||||
data['vcpus'],
|
|
||||||
data['disk_gb'],
|
|
||||||
flavorid=data["flavor_id"],
|
|
||||||
ephemeral=data['eph_gb'],
|
|
||||||
swap=data['swap_mb'])
|
|
||||||
msg = _('Created flavor "%s".') % data['name']
|
|
||||||
messages.success(request, msg)
|
|
||||||
return flavor
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request, _("Unable to create flavor."))
|
|
||||||
|
|
||||||
|
|
||||||
class EditFlavor(CreateFlavor):
|
|
||||||
flavor_id = forms.CharField(widget=forms.widgets.HiddenInput)
|
|
||||||
|
|
||||||
def clean_flavor_id(self):
|
|
||||||
return self.data.get('flavor_id')
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
try:
|
|
||||||
flavor_id = data['flavor_id']
|
|
||||||
# grab any existing extra specs, because flavor edit currently
|
|
||||||
# implemented as a delete followed by a create
|
|
||||||
extras_dict = api.nova.flavor_get_extras(self.request,
|
|
||||||
flavor_id,
|
|
||||||
raw=True)
|
|
||||||
# First mark the existing flavor as deleted.
|
|
||||||
api.nova.flavor_delete(request, data['flavor_id'])
|
|
||||||
# Then create a new flavor with the same name but a new ID.
|
|
||||||
# This is in the same try/except block as the delete call
|
|
||||||
# because if the delete fails the API will error out because
|
|
||||||
# active flavors can't have the same name.
|
|
||||||
flavor = api.nova.flavor_create(request,
|
|
||||||
data['name'],
|
|
||||||
data['memory_mb'],
|
|
||||||
data['vcpus'],
|
|
||||||
data['disk_gb'],
|
|
||||||
ephemeral=data['eph_gb'],
|
|
||||||
swap=data['swap_mb'])
|
|
||||||
if (extras_dict):
|
|
||||||
api.nova.flavor_extra_set(request, flavor.id, extras_dict)
|
|
||||||
msg = _('Updated flavor "%s".') % data['name']
|
|
||||||
messages.success(request, msg)
|
|
||||||
return flavor
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request, _("Unable to update flavor."))
|
|
|
@ -1,5 +1,28 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse # noqa
|
||||||
|
from django.template import defaultfilters as filters
|
||||||
|
from django.utils.http import urlencode # noqa
|
||||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||||
|
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
|
@ -25,10 +48,10 @@ class CreateFlavor(tables.LinkAction):
|
||||||
classes = ("ajax-modal", "btn-create")
|
classes = ("ajax-modal", "btn-create")
|
||||||
|
|
||||||
|
|
||||||
class EditFlavor(tables.LinkAction):
|
class UpdateFlavor(tables.LinkAction):
|
||||||
name = "edit"
|
name = "update"
|
||||||
verbose_name = _("Edit Flavor")
|
verbose_name = _("Edit Flavor")
|
||||||
url = "horizon:admin:flavors:edit"
|
url = "horizon:admin:flavors:update"
|
||||||
classes = ("ajax-modal", "btn-edit")
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,6 +62,30 @@ class ViewFlavorExtras(tables.LinkAction):
|
||||||
classes = ("btn-edit",)
|
classes = ("btn-edit",)
|
||||||
|
|
||||||
|
|
||||||
|
class ModifyAccess(tables.LinkAction):
|
||||||
|
name = "projects"
|
||||||
|
verbose_name = "Modify Access"
|
||||||
|
url = "horizon:admin:flavors:update"
|
||||||
|
classes = ("ajax-modal", "btn-edit")
|
||||||
|
|
||||||
|
def get_link_url(self, flavor):
|
||||||
|
step = 'update_flavor_access'
|
||||||
|
base_url = reverse(self.url, args=[flavor.id])
|
||||||
|
param = urlencode({"step": step})
|
||||||
|
return "?".join([base_url, param])
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorFilterAction(tables.FilterAction):
|
||||||
|
def filter(self, table, flavors, filter_string):
|
||||||
|
""" Really naive case-insensitive search. """
|
||||||
|
q = filter_string.lower()
|
||||||
|
|
||||||
|
def comp(flavor):
|
||||||
|
return q in flavor.name.lower()
|
||||||
|
|
||||||
|
return filter(comp, flavors)
|
||||||
|
|
||||||
|
|
||||||
def get_size(flavor):
|
def get_size(flavor):
|
||||||
return _("%sMB") % flavor.ram
|
return _("%sMB") % flavor.ram
|
||||||
|
|
||||||
|
@ -60,9 +107,16 @@ class FlavorsTable(tables.DataTable):
|
||||||
verbose_name=_('Swap Disk'),
|
verbose_name=_('Swap Disk'),
|
||||||
attrs={'data-type': 'size'})
|
attrs={'data-type': 'size'})
|
||||||
flavor_id = tables.Column('id', verbose_name=('ID'))
|
flavor_id = tables.Column('id', verbose_name=('ID'))
|
||||||
|
public = tables.Column("is_public",
|
||||||
|
verbose_name=_("Public"),
|
||||||
|
empty_value=False,
|
||||||
|
filters=(filters.yesno, filters.capfirst))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "flavors"
|
name = "flavors"
|
||||||
verbose_name = _("Flavors")
|
verbose_name = _("Flavors")
|
||||||
table_actions = (CreateFlavor, DeleteFlavor)
|
table_actions = (FlavorFilterAction, CreateFlavor, DeleteFlavor)
|
||||||
row_actions = (EditFlavor, ViewFlavorExtras, DeleteFlavor)
|
row_actions = (UpdateFlavor,
|
||||||
|
ModifyAccess,
|
||||||
|
ViewFlavorExtras,
|
||||||
|
DeleteFlavor)
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
{% extends "horizon/common/_modal_form.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load url from future %}
|
|
||||||
|
|
||||||
{% block form_id %}create_flavor_form{% endblock %}
|
|
||||||
{% block form_action %}{% url 'horizon:admin:flavors:create' %}{% endblock %}
|
|
||||||
|
|
||||||
{% block modal_id %}create_flavor_modal{% endblock %}
|
|
||||||
{% block modal-header %}{% trans "Create Flavor" %}{% 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 "From here you can define the sizing of a new flavor." %}</p>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block modal-footer %}
|
|
||||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Flavor" %}" />
|
|
||||||
<a href="{% url 'horizon:admin:flavors:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
|
||||||
{% endblock %}
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% load url from future %}
|
{% load url from future %}
|
||||||
|
|
||||||
{% block form_id %}edit_flavor_form{% endblock %}
|
{% block form_id %}edit_flavor_form{% endblock %}
|
||||||
{% block form_action %}{% url 'horizon:admin:flavors:edit' flavor_id %}{% endblock %}
|
{% block form_action %}{% url 'horizon:admin:flavors:update' flavor_id %}{% endblock %}
|
||||||
|
|
||||||
{% block modal_id %}edit_flavor_modal{% endblock %}
|
{% block modal_id %}edit_flavor_modal{% endblock %}
|
||||||
{% block modal-header %}{% trans "Edit Flavor" %}{% endblock %}
|
{% block modal-header %}{% trans "Edit Flavor" %}{% endblock %}
|
|
@ -7,5 +7,5 @@
|
||||||
{% endblock page_header %}
|
{% endblock page_header %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% include "admin/flavors/_create.html" %}
|
{% include 'horizon/common/_workflow.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
{% endblock page_header %}
|
{% endblock page_header %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
{% include "admin/flavors/_edit.html" %}
|
{% include 'horizon/common/_workflow.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -7,50 +7,314 @@ from openstack_dashboard.test import helpers as test
|
||||||
|
|
||||||
from novaclient.v1_1 import flavors
|
from novaclient.v1_1 import flavors
|
||||||
|
|
||||||
|
from openstack_dashboard.dashboards.admin.flavors import workflows
|
||||||
|
|
||||||
class FlavorsTests(test.BaseAdminViewTests):
|
INDEX_URL = reverse('horizon:admin:flavors:index')
|
||||||
@test.create_stubs({api.nova: ('flavor_list', 'flavor_create'), })
|
|
||||||
def test_create_new_flavor_when_none_exist(self):
|
|
||||||
flavor = self.flavors.first()
|
class FlavorsViewTests(test.BaseAdminViewTests):
|
||||||
|
@test.create_stubs({api.nova: ('flavor_list',), })
|
||||||
|
def test_index(self):
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
res = self.client.get(INDEX_URL)
|
||||||
|
self.assertTemplateUsed(res, 'admin/flavors/index.html')
|
||||||
|
self.assertItemsEqual(res.context['table'].data, self.flavors.list())
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFlavorWorkflowTests(test.BaseAdminViewTests):
|
||||||
|
def _flavor_create_params(self, flavor, id=None):
|
||||||
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
flavor_info = {"name": flavor.name,
|
||||||
|
"vcpu": flavor.vcpus,
|
||||||
|
"memory": flavor.ram,
|
||||||
|
"disk": flavor.disk,
|
||||||
|
"swap": flavor.swap,
|
||||||
|
"ephemeral": eph,
|
||||||
|
"is_public": flavor.is_public}
|
||||||
|
if id:
|
||||||
|
flavor_info["flavorid"] = id
|
||||||
|
return flavor_info
|
||||||
|
|
||||||
# no pre-existing flavors
|
def _get_workflow_fields(self, flavor, id=None, access=None):
|
||||||
api.nova.flavor_create(IsA(http.HttpRequest),
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
flavor.name,
|
flavor_info = {"name": flavor.name,
|
||||||
flavor.ram,
|
"vcpus": flavor.vcpus,
|
||||||
flavor.vcpus,
|
"memory_mb": flavor.ram,
|
||||||
flavor.disk,
|
"disk_gb": flavor.disk,
|
||||||
flavorid=flavor.id,
|
"swap_mb": flavor.swap,
|
||||||
swap=flavor.swap,
|
"eph_gb": eph}
|
||||||
ephemeral=eph).AndReturn(flavor)
|
if access:
|
||||||
api.nova.flavor_list(IsA(http.HttpRequest))
|
access_field_name = 'update_flavor_access_role_member'
|
||||||
api.nova.flavor_list(IsA(http.HttpRequest))
|
flavor_info[access_field_name] = [p.id for p in access]
|
||||||
|
if id:
|
||||||
|
flavor_info['flavor_id'] = id
|
||||||
|
return flavor_info
|
||||||
|
|
||||||
|
def _get_workflow_data(self, flavor, id=None, access=None):
|
||||||
|
flavor_info = self._get_workflow_fields(flavor, access=access,
|
||||||
|
id=id)
|
||||||
|
return flavor_info
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',), })
|
||||||
|
def test_workflow_get(self):
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
url = reverse('horizon:admin:flavors:create')
|
url = reverse('horizon:admin:flavors:create')
|
||||||
resp = self.client.get(url)
|
res = self.client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertTemplateUsed(res, 'admin/flavors/create.html')
|
||||||
self.assertTemplateUsed(resp, "admin/flavors/create.html")
|
workflow = res.context['workflow']
|
||||||
|
expected_name = workflows.CreateFlavor.name
|
||||||
|
self.assertEqual(res.context['workflow'].name, expected_name)
|
||||||
|
self.assertQuerysetEqual(workflow.steps,
|
||||||
|
['<CreateFlavorInfo: createflavorinfoaction>',
|
||||||
|
'<UpdateFlavorAccess: update_flavor_access>'])
|
||||||
|
|
||||||
data = {'name': flavor.name,
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
'flavor_id': flavor.id,
|
api.nova: ('flavor_list',
|
||||||
'vcpus': flavor.vcpus,
|
'flavor_create',)})
|
||||||
'memory_mb': flavor.ram,
|
def test_create_flavor_without_projects_post(self):
|
||||||
'disk_gb': flavor.disk,
|
flavor = self.flavors.first()
|
||||||
'swap_mb': flavor.swap,
|
projects = self.tenants.list()
|
||||||
'eph_gb': eph}
|
|
||||||
resp = self.client.post(url, data)
|
|
||||||
self.assertRedirectsNoFollow(resp,
|
|
||||||
reverse("horizon:admin:flavors:index"))
|
|
||||||
|
|
||||||
# keeping the 2 edit tests separate to aid debug breaks
|
# init
|
||||||
@test.create_stubs({api.nova: ('flavor_list',
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn([])
|
||||||
|
|
||||||
|
# handle
|
||||||
|
params = self._flavor_create_params(flavor, id='auto')
|
||||||
|
api.nova.flavor_create(IsA(http.HttpRequest), **params) \
|
||||||
|
.AndReturn(flavor)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor)
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_list',
|
||||||
'flavor_create',
|
'flavor_create',
|
||||||
'flavor_delete',
|
'add_tenant_to_flavor',)})
|
||||||
|
def test_create_flavor_with_projects_post(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn([])
|
||||||
|
|
||||||
|
# handle
|
||||||
|
params = self._flavor_create_params(flavor, id='auto')
|
||||||
|
params['is_public'] = False
|
||||||
|
api.nova.flavor_create(IsA(http.HttpRequest), **params) \
|
||||||
|
.AndReturn(flavor)
|
||||||
|
for project in projects:
|
||||||
|
api.nova.add_tenant_to_flavor(IsA(http.HttpRequest),
|
||||||
|
flavor.id, project.id)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor, access=projects)
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_list',)})
|
||||||
|
def test_create_existing_flavor_name_error(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
|
||||||
|
# handle
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor)
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertFormErrors(res)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_list',)})
|
||||||
|
def test_create_existing_flavor_id_error(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
|
||||||
|
# handle
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor)
|
||||||
|
# Name is okay.
|
||||||
|
workflow_data['name'] = 'newflavorname'
|
||||||
|
# Flavor id already exists.
|
||||||
|
workflow_data['flavor_id'] = flavor.id
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertFormErrors(res)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_list',
|
||||||
|
'flavor_create',
|
||||||
|
'add_tenant_to_flavor',)})
|
||||||
|
def test_create_flavor_project_update_error(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn([])
|
||||||
|
|
||||||
|
# handle
|
||||||
|
params = self._flavor_create_params(flavor, id='auto')
|
||||||
|
params['is_public'] = False
|
||||||
|
api.nova.flavor_create(IsA(http.HttpRequest), **params) \
|
||||||
|
.AndReturn(flavor)
|
||||||
|
for project in projects:
|
||||||
|
expect = api.nova.add_tenant_to_flavor(IsA(http.HttpRequest),
|
||||||
|
flavor.id, project.id)
|
||||||
|
if project == projects[0]:
|
||||||
|
expect.AndRaise(self.exceptions.nova)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor, access=projects)
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertMessageCount(error=1, warning=0)
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_list',)})
|
||||||
|
def test_create_flavor_missing_field_error(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn([])
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
workflow_data = self._get_workflow_data(flavor)
|
||||||
|
workflow_data["name"] = ""
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:create')
|
||||||
|
res = self.client.post(url, workflow_data)
|
||||||
|
|
||||||
|
self.assertFormErrors(res)
|
||||||
|
self.assertContains(res, "field is required")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavorWorkflowTests(BaseFlavorWorkflowTests):
|
||||||
|
@test.create_stubs({api.nova: ('flavor_get',
|
||||||
|
'flavor_access_list',),
|
||||||
|
api.keystone: ('tenant_list',)})
|
||||||
|
def test_update_flavor_get(self):
|
||||||
|
flavor = self.flavors.list()[2]
|
||||||
|
flavor_access = self.flavor_access.list()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
|
||||||
|
# GET/init, set up expected behavior
|
||||||
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
|
.MultipleTimes().AndReturn(flavor)
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)).AndReturn([projects,
|
||||||
|
False])
|
||||||
|
api.nova.flavor_access_list(IsA(http.HttpRequest), flavor.id) \
|
||||||
|
.AndReturn(flavor_access)
|
||||||
|
|
||||||
|
# Put all mocks created by mox into replay mode
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
|
res = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertTemplateUsed(res, 'admin/flavors/update.html')
|
||||||
|
|
||||||
|
workflow = res.context['workflow']
|
||||||
|
expected_name = workflows.UpdateFlavor.name
|
||||||
|
self.assertEqual(res.context['workflow'].name, expected_name)
|
||||||
|
|
||||||
|
self.assertQuerysetEqual(workflow.steps,
|
||||||
|
['<UpdateFlavorInfo: update_info>',
|
||||||
|
'<UpdateFlavorAccess: update_flavor_access>'])
|
||||||
|
|
||||||
|
step = workflow.get_step("update_info")
|
||||||
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
self.assertEqual(step.action.initial['name'], flavor.name)
|
||||||
|
self.assertEqual(step.action.initial['vcpus'], flavor.vcpus)
|
||||||
|
self.assertEqual(step.action.initial['memory_mb'], flavor.ram)
|
||||||
|
self.assertEqual(step.action.initial['disk_gb'], flavor.disk)
|
||||||
|
self.assertEqual(step.action.initial['swap_mb'], flavor.swap)
|
||||||
|
self.assertEqual(step.action.initial['eph_gb'], eph)
|
||||||
|
|
||||||
|
step = workflow.get_step("update_flavor_access")
|
||||||
|
field_name = step.get_member_field_name('member')
|
||||||
|
self.assertEqual(step.action.fields[field_name].initial,
|
||||||
|
[fa.tenant_id for fa in flavor_access])
|
||||||
|
|
||||||
|
@test.create_stubs({api.nova: ('flavor_get',), })
|
||||||
|
def test_update_flavor_get_flavor_error(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
|
||||||
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
|
.AndRaise(self.exceptions.nova)
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
|
res = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_get',
|
||||||
'flavor_get_extras',
|
'flavor_get_extras',
|
||||||
'flavor_get'), })
|
'flavor_list',
|
||||||
def test_edit_flavor(self):
|
'flavor_delete',
|
||||||
flavor = self.flavors.first() # has no extra specs
|
'flavor_create')})
|
||||||
|
def test_update_flavor_without_extra_specs(self):
|
||||||
|
# The first element has no extra specs
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
extra_specs = getattr(flavor, 'extra_specs')
|
extra_specs = getattr(flavor, 'extra_specs')
|
||||||
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
|
@ -63,172 +327,356 @@ class FlavorsTests(test.BaseAdminViewTests):
|
||||||
'swap': 0,
|
'swap': 0,
|
||||||
'OS-FLV-EXT-DATA:ephemeral': eph,
|
'OS-FLV-EXT-DATA:ephemeral': eph,
|
||||||
'extra_specs': extra_specs})
|
'extra_specs': extra_specs})
|
||||||
# GET
|
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
|
|
||||||
|
|
||||||
# POST
|
# GET/init, set up expected behavior
|
||||||
api.nova.flavor_list(IsA(http.HttpRequest))
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
|
.MultipleTimes().AndReturn(flavor)
|
||||||
api.nova.flavor_get_extras(IsA(http.HttpRequest), flavor.id, raw=True)\
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
.AndReturn(extra_specs)
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
|
# POST/init
|
||||||
|
api.nova.flavor_get_extras(IsA(http.HttpRequest),
|
||||||
|
flavor.id, raw=True) \
|
||||||
|
.AndReturn(extra_specs)
|
||||||
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
||||||
api.nova.flavor_create(IsA(http.HttpRequest),
|
api.nova.flavor_create(IsA(http.HttpRequest),
|
||||||
new_flavor.name,
|
new_flavor.name,
|
||||||
new_flavor.ram,
|
new_flavor.ram,
|
||||||
new_flavor.vcpus,
|
new_flavor.vcpus,
|
||||||
new_flavor.disk,
|
new_flavor.disk,
|
||||||
swap=flavor.swap,
|
swap=new_flavor.swap,
|
||||||
ephemeral=eph).AndReturn(new_flavor)
|
ephemeral=eph,
|
||||||
|
flavorid=flavor.id,
|
||||||
|
is_public=True).AndReturn(new_flavor)
|
||||||
|
|
||||||
|
# Put mocks in replay mode
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# get_test
|
# run get test
|
||||||
url = reverse('horizon:admin:flavors:edit', args=[flavor.id])
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
resp = self.client.get(url)
|
resp = self.client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTemplateUsed(resp, "admin/flavors/edit.html")
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
# post test
|
# run post test
|
||||||
data = {'flavor_id': flavor.id,
|
workflow_data = {'flavor_id': flavor.id,
|
||||||
'name': flavor.name,
|
'name': new_flavor.name,
|
||||||
'vcpus': flavor.vcpus + 1,
|
'vcpus': new_flavor.vcpus,
|
||||||
'memory_mb': flavor.ram,
|
'memory_mb': new_flavor.ram,
|
||||||
'disk_gb': flavor.disk,
|
'disk_gb': new_flavor.disk,
|
||||||
'swap_mb': flavor.swap,
|
'swap_mb': new_flavor.swap,
|
||||||
'eph_gb': eph}
|
'eph_gb': eph,
|
||||||
resp = self.client.post(url, data)
|
'is_public': True}
|
||||||
|
resp = self.client.post(url, workflow_data)
|
||||||
self.assertNoFormErrors(resp)
|
self.assertNoFormErrors(resp)
|
||||||
self.assertMessageCount(success=1)
|
self.assertMessageCount(success=1)
|
||||||
self.assertRedirectsNoFollow(resp,
|
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||||
reverse("horizon:admin:flavors:index"))
|
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('flavor_list',
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
'flavor_create',
|
api.nova: ('flavor_get',
|
||||||
'flavor_delete',
|
|
||||||
'flavor_get_extras',
|
'flavor_get_extras',
|
||||||
'flavor_extra_set',
|
'flavor_list',
|
||||||
'flavor_get'), })
|
'flavor_delete',
|
||||||
def test_edit_flavor_with_extra_specs(self):
|
'flavor_create',
|
||||||
flavor = self.flavors.list()[1] # the second element has extra specs
|
'flavor_extra_set')})
|
||||||
|
def test_update_flavor_with_extra_specs(self):
|
||||||
|
# The second element has extra specs
|
||||||
|
flavor = self.flavors.list()[1]
|
||||||
|
projects = self.tenants.list()
|
||||||
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
extra_specs = getattr(flavor, 'extra_specs')
|
extra_specs = getattr(flavor, 'extra_specs')
|
||||||
new_vcpus = flavor.vcpus + 1
|
|
||||||
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
{'id':
|
{'id':
|
||||||
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
'name': flavor.name,
|
'name': flavor.name,
|
||||||
'vcpus': new_vcpus,
|
'vcpus': flavor.vcpus + 1,
|
||||||
'disk': flavor.disk,
|
'disk': flavor.disk,
|
||||||
'ram': flavor.ram,
|
'ram': flavor.ram,
|
||||||
'swap': flavor.swap,
|
'swap': flavor.swap,
|
||||||
'OS-FLV-EXT-DATA:ephemeral': eph,
|
'OS-FLV-EXT-DATA:ephemeral': eph,
|
||||||
'extra_specs': extra_specs})
|
'extra_specs': extra_specs})
|
||||||
# GET
|
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
|
|
||||||
|
|
||||||
# POST
|
# GET/init, set up expected behavior
|
||||||
api.nova.flavor_list(IsA(http.HttpRequest))
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id).AndReturn(flavor)
|
.MultipleTimes().AndReturn(flavor)
|
||||||
api.nova.flavor_get_extras(IsA(http.HttpRequest), flavor.id, raw=True)\
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
.AndReturn(extra_specs)
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
|
||||||
|
# POST/init
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
api.nova.flavor_get_extras(IsA(http.HttpRequest),
|
||||||
|
flavor.id, raw=True) \
|
||||||
|
.AndReturn(extra_specs)
|
||||||
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
||||||
api.nova.flavor_create(IsA(http.HttpRequest),
|
api.nova.flavor_create(IsA(http.HttpRequest),
|
||||||
flavor.name,
|
new_flavor.name,
|
||||||
flavor.ram,
|
new_flavor.ram,
|
||||||
new_vcpus,
|
new_flavor.vcpus,
|
||||||
flavor.disk,
|
new_flavor.disk,
|
||||||
swap=flavor.swap,
|
swap=new_flavor.swap,
|
||||||
ephemeral=eph).AndReturn(new_flavor)
|
ephemeral=eph,
|
||||||
|
flavorid=flavor.id,
|
||||||
|
is_public=True).AndReturn(new_flavor)
|
||||||
api.nova.flavor_extra_set(IsA(http.HttpRequest),
|
api.nova.flavor_extra_set(IsA(http.HttpRequest),
|
||||||
new_flavor.id,
|
new_flavor.id, extra_specs)
|
||||||
extra_specs)
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
#get_test
|
#run get test
|
||||||
url = reverse('horizon:admin:flavors:edit', args=[flavor.id])
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
resp = self.client.get(url)
|
resp = self.client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTemplateUsed(resp, "admin/flavors/edit.html")
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
#post test
|
#run post test
|
||||||
data = {'flavor_id': flavor.id,
|
workflow_data = {'flavor_id': flavor.id,
|
||||||
'name': flavor.name,
|
'name': new_flavor.name,
|
||||||
'vcpus': new_vcpus,
|
'vcpus': new_flavor.vcpus,
|
||||||
'memory_mb': flavor.ram,
|
'memory_mb': new_flavor.ram,
|
||||||
'disk_gb': flavor.disk,
|
'disk_gb': new_flavor.disk,
|
||||||
'swap_mb': flavor.swap,
|
'swap_mb': new_flavor.swap,
|
||||||
'eph_gb': eph}
|
'eph_gb': eph,
|
||||||
resp = self.client.post(url, data)
|
'is_public': True}
|
||||||
|
resp = self.client.post(url, workflow_data)
|
||||||
self.assertNoFormErrors(resp)
|
self.assertNoFormErrors(resp)
|
||||||
self.assertMessageCount(success=1)
|
self.assertMessageCount(success=1)
|
||||||
self.assertRedirectsNoFollow(resp,
|
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||||
reverse("horizon:admin:flavors:index"))
|
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('flavor_list',
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
'flavor_get'), })
|
api.nova: ('flavor_get',
|
||||||
def test_edit_flavor_set_invalid_name(self):
|
'flavor_get_extras',
|
||||||
flavor_a = self.flavors.list()[0]
|
'flavor_list',
|
||||||
eph = getattr(flavor_a, 'OS-FLV-EXT-DATA:ephemeral')
|
'flavor_delete',
|
||||||
invalid_flavor_name = "m1.tiny()"
|
'flavor_create')})
|
||||||
|
def test_update_flavor_update_flavor_error(self):
|
||||||
|
# The first element has no extra specs
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
extra_specs = getattr(flavor, 'extra_specs')
|
||||||
|
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
|
{'id':
|
||||||
|
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
|
'name': flavor.name,
|
||||||
|
'vcpus': flavor.vcpus + 1,
|
||||||
|
'disk': flavor.disk,
|
||||||
|
'ram': flavor.ram,
|
||||||
|
'swap': 0,
|
||||||
|
'OS-FLV-EXT-DATA:ephemeral': eph,
|
||||||
|
'extra_specs': extra_specs})
|
||||||
|
|
||||||
# GET
|
# GET/init, set up expected behavior
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest),
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
flavor_a.id).AndReturn(flavor_a)
|
.MultipleTimes().AndReturn(flavor)
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
|
||||||
# POST
|
# POST
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest),
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
flavor_a.id).AndReturn(flavor_a)
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
|
# POST/init
|
||||||
|
api.nova.flavor_get_extras(IsA(http.HttpRequest),
|
||||||
|
flavor.id, raw=True) \
|
||||||
|
.AndReturn(extra_specs)
|
||||||
|
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
||||||
|
api.nova.flavor_create(IsA(http.HttpRequest),
|
||||||
|
new_flavor.name,
|
||||||
|
new_flavor.ram,
|
||||||
|
new_flavor.vcpus,
|
||||||
|
new_flavor.disk,
|
||||||
|
swap=new_flavor.swap,
|
||||||
|
ephemeral=eph,
|
||||||
|
flavorid=flavor.id,
|
||||||
|
is_public=True)\
|
||||||
|
.AndRaise(self.exceptions.nova)
|
||||||
|
|
||||||
|
# Put mocks in replay mode
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# get_test
|
# run get test
|
||||||
url = reverse('horizon:admin:flavors:edit', args=[flavor_a.id])
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
resp = self.client.get(url)
|
resp = self.client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTemplateUsed(resp, "admin/flavors/edit.html")
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
# post test
|
# run post test
|
||||||
data = {'flavor_id': flavor_a.id,
|
workflow_data = {'flavor_id': flavor.id,
|
||||||
'name': invalid_flavor_name,
|
'name': new_flavor.name,
|
||||||
'vcpus': flavor_a.vcpus + 1,
|
'vcpus': new_flavor.vcpus,
|
||||||
'memory_mb': flavor_a.ram,
|
'memory_mb': new_flavor.ram,
|
||||||
'disk_gb': flavor_a.disk,
|
'disk_gb': new_flavor.disk,
|
||||||
'swap_mb': flavor_a.swap,
|
'swap_mb': new_flavor.swap,
|
||||||
'eph_gb': eph}
|
'eph_gb': eph,
|
||||||
|
'is_public': True}
|
||||||
|
resp = self.client.post(url, workflow_data)
|
||||||
|
self.assertNoFormErrors(resp)
|
||||||
|
self.assertMessageCount(error=1)
|
||||||
|
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_get',
|
||||||
|
'flavor_get_extras',
|
||||||
|
'flavor_list',
|
||||||
|
'flavor_delete',
|
||||||
|
'flavor_create',
|
||||||
|
'flavor_access_list',
|
||||||
|
'remove_tenant_from_flavor',
|
||||||
|
'add_tenant_to_flavor')})
|
||||||
|
def test_update_flavor_update_projects_error(self):
|
||||||
|
# The first element has no extra specs
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
flavor_projects = [self.tenants.first()]
|
||||||
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
extra_specs = getattr(flavor, 'extra_specs')
|
||||||
|
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
|
{'id':
|
||||||
|
"cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||||
|
'name': flavor.name,
|
||||||
|
'vcpus': flavor.vcpus + 1,
|
||||||
|
'disk': flavor.disk,
|
||||||
|
'ram': flavor.ram,
|
||||||
|
'swap': 0,
|
||||||
|
'OS-FLV-EXT-DATA:ephemeral': eph,
|
||||||
|
'os-flavor-access:is_public': False,
|
||||||
|
'extra_specs': extra_specs})
|
||||||
|
|
||||||
|
# GET/init, set up expected behavior
|
||||||
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
|
.MultipleTimes().AndReturn(flavor)
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
|
||||||
|
# POST/init
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
api.nova.flavor_get_extras(IsA(http.HttpRequest),
|
||||||
|
flavor.id, raw=True) \
|
||||||
|
.AndReturn(extra_specs)
|
||||||
|
|
||||||
|
api.nova.flavor_delete(IsA(http.HttpRequest), flavor.id)
|
||||||
|
api.nova.flavor_create(IsA(http.HttpRequest),
|
||||||
|
new_flavor.name,
|
||||||
|
new_flavor.ram,
|
||||||
|
new_flavor.vcpus,
|
||||||
|
new_flavor.disk,
|
||||||
|
swap=new_flavor.swap,
|
||||||
|
ephemeral=eph,
|
||||||
|
flavorid=flavor.id,
|
||||||
|
is_public=new_flavor.is_public) \
|
||||||
|
.AndReturn(new_flavor)
|
||||||
|
|
||||||
|
new_flavor_projects = flavor_projects
|
||||||
|
for project in new_flavor_projects:
|
||||||
|
expect = api.nova.add_tenant_to_flavor(IsA(http.HttpRequest),
|
||||||
|
new_flavor.id, project.id)
|
||||||
|
if project == projects[0]:
|
||||||
|
expect.AndRaise(self.exceptions.nova)
|
||||||
|
|
||||||
|
# Put mocks in replay mode
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
# run get test
|
||||||
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
|
# run post test
|
||||||
|
data = self._get_workflow_data(new_flavor, access=flavor_projects)
|
||||||
|
data['flavor_id'] = flavor.id
|
||||||
resp = self.client.post(url, data)
|
resp = self.client.post(url, data)
|
||||||
|
self.assertNoFormErrors(resp)
|
||||||
|
self.assertMessageCount(error=1, warning=0)
|
||||||
|
self.assertRedirectsNoFollow(resp, INDEX_URL)
|
||||||
|
|
||||||
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
|
api.nova: ('flavor_get',
|
||||||
|
'flavor_list',)})
|
||||||
|
def test_update_flavor_set_invalid_name(self):
|
||||||
|
flavor = self.flavors.first()
|
||||||
|
projects = self.tenants.list()
|
||||||
|
eph = getattr(flavor, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
invalid_flavor_name = "m1.tiny()"
|
||||||
|
|
||||||
|
# init
|
||||||
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor.id) \
|
||||||
|
.MultipleTimes().AndReturn(flavor)
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
|
.AndReturn(self.flavors.list())
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
# run get test
|
||||||
|
url = reverse('horizon:admin:flavors:update', args=[flavor.id])
|
||||||
|
resp = self.client.get(url)
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
|
# run post test
|
||||||
|
workflow_data = {'flavor_id': flavor.id,
|
||||||
|
'name': invalid_flavor_name,
|
||||||
|
'vcpus': flavor.vcpus + 1,
|
||||||
|
'memory_mb': flavor.ram,
|
||||||
|
'disk_gb': flavor.disk,
|
||||||
|
'swap_mb': flavor.swap,
|
||||||
|
'eph_gb': eph,
|
||||||
|
'is_public': True}
|
||||||
|
resp = self.client.post(url, workflow_data)
|
||||||
self.assertFormErrors(resp, 1, 'Name may only contain letters, '
|
self.assertFormErrors(resp, 1, 'Name may only contain letters, '
|
||||||
'numbers, underscores, periods and hyphens.')
|
'numbers, underscores, periods and hyphens.')
|
||||||
|
|
||||||
@test.create_stubs({api.nova: ('flavor_list',
|
@test.create_stubs({api.keystone: ('tenant_list',),
|
||||||
'flavor_get'), })
|
api.nova: ('flavor_get',
|
||||||
def test_edit_flavor_set_existing_name(self):
|
'flavor_list',)})
|
||||||
|
def test_update_flavor_set_existing_name(self):
|
||||||
flavor_a = self.flavors.list()[0]
|
flavor_a = self.flavors.list()[0]
|
||||||
flavor_b = self.flavors.list()[1]
|
flavor_b = self.flavors.list()[1]
|
||||||
|
projects = self.tenants.list()
|
||||||
eph = getattr(flavor_a, 'OS-FLV-EXT-DATA:ephemeral')
|
eph = getattr(flavor_a, 'OS-FLV-EXT-DATA:ephemeral')
|
||||||
|
extra_specs = getattr(flavor_a, 'extra_specs')
|
||||||
|
new_flavor = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
|
{'id': flavor_a.id,
|
||||||
|
'name': flavor_b.name,
|
||||||
|
'vcpus': flavor_a.vcpus,
|
||||||
|
'disk': flavor_a.disk,
|
||||||
|
'ram': flavor_a.ram,
|
||||||
|
'swap': flavor_a.swap,
|
||||||
|
'OS-FLV-EXT-DATA:ephemeral': eph,
|
||||||
|
'extra_specs': extra_specs})
|
||||||
|
|
||||||
# GET
|
# GET
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest),
|
api.nova.flavor_get(IsA(http.HttpRequest), flavor_a.id) \
|
||||||
flavor_a.id).AndReturn(flavor_a)
|
.MultipleTimes().AndReturn(flavor_a)
|
||||||
|
api.keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||||
|
.MultipleTimes().AndReturn([projects, False])
|
||||||
|
|
||||||
# POST
|
# POST
|
||||||
api.nova.flavor_list(IsA(http.HttpRequest)) \
|
api.nova.flavor_list(IsA(http.HttpRequest), None) \
|
||||||
.AndReturn(self.flavors.list())
|
.AndReturn(self.flavors.list())
|
||||||
api.nova.flavor_get(IsA(http.HttpRequest),
|
|
||||||
flavor_a.id).AndReturn(flavor_a)
|
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# get_test
|
# get test
|
||||||
url = reverse('horizon:admin:flavors:edit', args=[flavor_a.id])
|
url = reverse('horizon:admin:flavors:update', args=[flavor_a.id])
|
||||||
resp = self.client.get(url)
|
resp = self.client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTemplateUsed(resp, "admin/flavors/edit.html")
|
self.assertTemplateUsed(resp, "admin/flavors/update.html")
|
||||||
|
|
||||||
# post test
|
# post test
|
||||||
data = {'flavor_id': flavor_a.id,
|
data = {'flavor_id': new_flavor.id,
|
||||||
'name': flavor_b.name,
|
'name': new_flavor.name,
|
||||||
'vcpus': flavor_a.vcpus + 1,
|
'vcpus': new_flavor.vcpus,
|
||||||
'memory_mb': flavor_a.ram,
|
'memory_mb': new_flavor.ram,
|
||||||
'disk_gb': flavor_a.disk,
|
'disk_gb': new_flavor.disk,
|
||||||
'swap_mb': flavor_a.swap,
|
'swap_mb': new_flavor.swap,
|
||||||
'eph_gb': eph}
|
'eph_gb': eph,
|
||||||
|
'is_public': True}
|
||||||
resp = self.client.post(url, data)
|
resp = self.client.post(url, data)
|
||||||
self.assertFormErrors(resp, 1, 'The name "m1.massive" '
|
self.assertFormErrors(resp, 1, 'The name "m1.massive" '
|
||||||
'is already used by another flavor.')
|
'is already used by another flavor.')
|
||||||
|
|
|
@ -26,9 +26,10 @@ from openstack_dashboard.dashboards.admin.flavors.extras \
|
||||||
import urls as extras_urls
|
import urls as extras_urls
|
||||||
from openstack_dashboard.dashboards.admin.flavors import views
|
from openstack_dashboard.dashboards.admin.flavors import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('openstack_dashboard.dashboards.admin.flavors.views',
|
urlpatterns = patterns('openstack_dashboard.dashboards.admin.flavors.views',
|
||||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||||
url(r'^(?P<id>[^/]+)/edit/$', views.EditView.as_view(), name='edit'),
|
url(r'^(?P<id>[^/]+)/update/$', views.UpdateView.as_view(), name='update'),
|
||||||
url(r'^(?P<id>[^/]+)/extras/', include(extras_urls, namespace='extras')),
|
url(r'^(?P<id>[^/]+)/extras/', include(extras_urls, namespace='extras')),
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,19 +24,21 @@ from django.core.urlresolvers import reverse_lazy # noqa
|
||||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||||
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
from horizon import forms
|
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
|
from horizon import workflows
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
|
|
||||||
from openstack_dashboard.dashboards.admin.flavors \
|
|
||||||
import forms as project_forms
|
|
||||||
from openstack_dashboard.dashboards.admin.flavors \
|
from openstack_dashboard.dashboards.admin.flavors \
|
||||||
import tables as project_tables
|
import tables as project_tables
|
||||||
|
from openstack_dashboard.dashboards.admin.flavors \
|
||||||
|
import workflows as flavor_workflows
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
INDEX_URL = "horizon:admin:flavors:index"
|
||||||
|
|
||||||
|
|
||||||
class IndexView(tables.DataTableView):
|
class IndexView(tables.DataTableView):
|
||||||
table_class = project_tables.FlavorsTable
|
table_class = project_tables.FlavorsTable
|
||||||
|
@ -46,7 +48,8 @@ class IndexView(tables.DataTableView):
|
||||||
request = self.request
|
request = self.request
|
||||||
flavors = []
|
flavors = []
|
||||||
try:
|
try:
|
||||||
flavors = api.nova.flavor_list(request)
|
# "is_public=None" will return all flavors.
|
||||||
|
flavors = api.nova.flavor_list(request, None)
|
||||||
except Exception:
|
except Exception:
|
||||||
exceptions.handle(request,
|
exceptions.handle(request,
|
||||||
_('Unable to retrieve flavor list.'))
|
_('Unable to retrieve flavor list.'))
|
||||||
|
@ -55,28 +58,25 @@ class IndexView(tables.DataTableView):
|
||||||
return flavors
|
return flavors
|
||||||
|
|
||||||
|
|
||||||
class CreateView(forms.ModalFormView):
|
class CreateView(workflows.WorkflowView):
|
||||||
form_class = project_forms.CreateFlavor
|
workflow_class = flavor_workflows.CreateFlavor
|
||||||
template_name = 'admin/flavors/create.html'
|
template_name = 'admin/flavors/create.html'
|
||||||
success_url = reverse_lazy('horizon:admin:flavors:index')
|
|
||||||
|
|
||||||
|
|
||||||
class EditView(forms.ModalFormView):
|
class UpdateView(workflows.WorkflowView):
|
||||||
form_class = project_forms.EditFlavor
|
workflow_class = flavor_workflows.UpdateFlavor
|
||||||
template_name = 'admin/flavors/edit.html'
|
template_name = 'admin/flavors/update.html'
|
||||||
success_url = reverse_lazy('horizon:admin:flavors:index')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EditView, self).get_context_data(**kwargs)
|
|
||||||
context['flavor_id'] = self.kwargs['id']
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
|
flavor_id = self.kwargs['id']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
flavor = api.nova.flavor_get(self.request, self.kwargs['id'])
|
# Get initial flavor information
|
||||||
|
flavor = api.nova.flavor_get(self.request, flavor_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_("Unable to retrieve flavor data."))
|
_('Unable to retrieve flavor data.'),
|
||||||
|
redirect=reverse_lazy(INDEX_URL))
|
||||||
return {'flavor_id': flavor.id,
|
return {'flavor_id': flavor.id,
|
||||||
'name': flavor.name,
|
'name': flavor.name,
|
||||||
'vcpus': flavor.vcpus,
|
'vcpus': flavor.vcpus,
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
# 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.utils.translation import ugettext_lazy as _ # noqa
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
|
from horizon import forms
|
||||||
|
from horizon import workflows
|
||||||
|
|
||||||
|
from openstack_dashboard import api
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFlavorInfoAction(workflows.Action):
|
||||||
|
_flavor_id_regex = (r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-'
|
||||||
|
r'[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9]+|auto$')
|
||||||
|
_flavor_id_help_text = _("Flavor ID should be UUID4 or integer. "
|
||||||
|
"Leave this field blank or use 'auto' to set "
|
||||||
|
"a random UUID4.")
|
||||||
|
name = forms.RegexField(label=_("Name"),
|
||||||
|
max_length=25,
|
||||||
|
regex=r'^[\w\.\- ]+$',
|
||||||
|
error_messages={'invalid': _('Name may only '
|
||||||
|
'contain letters, numbers, underscores, '
|
||||||
|
'periods and hyphens.')})
|
||||||
|
flavor_id = forms.RegexField(label=_("ID"),
|
||||||
|
regex=_flavor_id_regex,
|
||||||
|
required=False,
|
||||||
|
initial='auto',
|
||||||
|
help_text=_flavor_id_help_text)
|
||||||
|
vcpus = forms.IntegerField(label=_("VCPUs"))
|
||||||
|
memory_mb = forms.IntegerField(label=_("RAM MB"))
|
||||||
|
disk_gb = forms.IntegerField(label=_("Root Disk GB"))
|
||||||
|
eph_gb = forms.IntegerField(label=_("Ephemeral Disk GB"))
|
||||||
|
swap_mb = forms.IntegerField(label=_("Swap Disk MB"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = _("Flavor Info")
|
||||||
|
help_text = _("From here you can create a new "
|
||||||
|
"flavor to organize projects.")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(CreateFlavorInfoAction, self).clean()
|
||||||
|
name = cleaned_data.get('name')
|
||||||
|
flavor_id = cleaned_data.get('flavor_id')
|
||||||
|
|
||||||
|
try:
|
||||||
|
flavors = api.nova.flavor_list(self.request, None)
|
||||||
|
except Exception:
|
||||||
|
flavors = []
|
||||||
|
msg = _('Unable to get flavor list')
|
||||||
|
exceptions.check_message(["Connection", "refused"], msg)
|
||||||
|
raise
|
||||||
|
if flavors is not None:
|
||||||
|
for flavor in flavors:
|
||||||
|
if flavor.name == name:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_('The name "%s" is already used by another flavor.')
|
||||||
|
% name
|
||||||
|
)
|
||||||
|
if flavor.id == flavor_id:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_('The ID "%s" is already used by another flavor.')
|
||||||
|
% flavor_id
|
||||||
|
)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFlavorInfo(workflows.Step):
|
||||||
|
action_class = CreateFlavorInfoAction
|
||||||
|
contributes = ("flavor_id",
|
||||||
|
"name",
|
||||||
|
"vcpus",
|
||||||
|
"memory_mb",
|
||||||
|
"disk_gb",
|
||||||
|
"eph_gb",
|
||||||
|
"swap_mb")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavorAccessAction(workflows.MembershipAction):
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
super(UpdateFlavorAccessAction, self).__init__(request,
|
||||||
|
*args,
|
||||||
|
**kwargs)
|
||||||
|
err_msg = _('Unable to retrieve flavor access list. '
|
||||||
|
'Please try again later.')
|
||||||
|
context = args[0]
|
||||||
|
|
||||||
|
default_role_field_name = self.get_default_role_field_name()
|
||||||
|
self.fields[default_role_field_name] = forms.CharField(required=False)
|
||||||
|
self.fields[default_role_field_name].initial = 'member'
|
||||||
|
|
||||||
|
field_name = self.get_member_field_name('member')
|
||||||
|
self.fields[field_name] = forms.MultipleChoiceField(required=False)
|
||||||
|
|
||||||
|
# Get list of available projects.
|
||||||
|
all_projects = []
|
||||||
|
try:
|
||||||
|
all_projects, has_more = api.keystone.tenant_list(request)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, err_msg)
|
||||||
|
projects_list = [(project.id, project.name)
|
||||||
|
for project in all_projects]
|
||||||
|
|
||||||
|
self.fields[field_name].choices = projects_list
|
||||||
|
|
||||||
|
# If we have a POST from the CreateFlavor workflow, the flavor id
|
||||||
|
# isn't an existing flavor. For the UpdateFlavor case, we don't care
|
||||||
|
# about the access list for the current flavor anymore as we're about
|
||||||
|
# to replace it.
|
||||||
|
if request.method == 'POST':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get list of flavor projects if the flavor is not public.
|
||||||
|
flavor_id = context.get('flavor_id')
|
||||||
|
flavor_access = []
|
||||||
|
try:
|
||||||
|
if flavor_id:
|
||||||
|
flavor = api.nova.flavor_get(request, flavor_id)
|
||||||
|
if not flavor.is_public:
|
||||||
|
flavor_access = [project.tenant_id for project in
|
||||||
|
api.nova.flavor_access_list(request, flavor_id)]
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, err_msg)
|
||||||
|
|
||||||
|
self.fields[field_name].initial = flavor_access
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = _("Flavor Access")
|
||||||
|
slug = "update_flavor_access"
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavorAccess(workflows.UpdateMembersStep):
|
||||||
|
action_class = UpdateFlavorAccessAction
|
||||||
|
help_text = _("You can control access to this flavor by moving projects "
|
||||||
|
"from the left column to the right column. Only projects "
|
||||||
|
"in the right column can use the flavor. If there are no "
|
||||||
|
"projects in the right column, all projects can use the "
|
||||||
|
"flavor.")
|
||||||
|
available_list_title = _("All Projects")
|
||||||
|
members_list_title = _("Selected projects")
|
||||||
|
no_available_text = _("No projects found.")
|
||||||
|
no_members_text = _("No projects selected. "
|
||||||
|
"All projects can use the flavor.")
|
||||||
|
show_roles = False
|
||||||
|
depends_on = ("flavor_id",)
|
||||||
|
contributes = ("flavor_access",)
|
||||||
|
|
||||||
|
def contribute(self, data, context):
|
||||||
|
if data:
|
||||||
|
member_field_name = self.get_member_field_name('member')
|
||||||
|
context['flavor_access'] = data.get(member_field_name, [])
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CreateFlavor(workflows.Workflow):
|
||||||
|
slug = "create_flavor"
|
||||||
|
name = _("Create Flavor")
|
||||||
|
finalize_button_name = _("Create Flavor")
|
||||||
|
success_message = _('Created new flavor "%s".')
|
||||||
|
failure_message = _('Unable to create flavor "%s".')
|
||||||
|
success_url = "horizon:admin:flavors:index"
|
||||||
|
default_steps = (CreateFlavorInfo,
|
||||||
|
UpdateFlavorAccess)
|
||||||
|
|
||||||
|
def format_status_message(self, message):
|
||||||
|
return message % self.context['name']
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
flavor_id = data.get('flavor_id') or 'auto'
|
||||||
|
flavor_access = data['flavor_access']
|
||||||
|
is_public = not flavor_access
|
||||||
|
|
||||||
|
# Create the flavor
|
||||||
|
try:
|
||||||
|
self.object = api.nova.flavor_create(request,
|
||||||
|
name=data['name'],
|
||||||
|
memory=data['memory_mb'],
|
||||||
|
vcpu=data['vcpus'],
|
||||||
|
disk=data['disk_gb'],
|
||||||
|
ephemeral=data['eph_gb'],
|
||||||
|
swap=data['swap_mb'],
|
||||||
|
flavorid=flavor_id,
|
||||||
|
is_public=is_public)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _('Unable to create flavor.'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Update flavor access if the new flavor is not public
|
||||||
|
flavor_id = self.object.id
|
||||||
|
for project in flavor_access:
|
||||||
|
try:
|
||||||
|
api.nova.add_tenant_to_flavor(
|
||||||
|
request, flavor_id, project)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request,
|
||||||
|
_('Unable to set flavor access for project %s.') % project)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavorInfoAction(CreateFlavorInfoAction):
|
||||||
|
flavor_id = forms.CharField(widget=forms.widgets.HiddenInput)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = _("Flavor Info")
|
||||||
|
slug = 'update_info'
|
||||||
|
help_text = _("From here you can edit the flavor details.")
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
name = self.cleaned_data.get('name')
|
||||||
|
flavor_id = self.cleaned_data.get('flavor_id')
|
||||||
|
try:
|
||||||
|
flavors = api.nova.flavor_list(self.request, None)
|
||||||
|
except Exception:
|
||||||
|
flavors = []
|
||||||
|
msg = _('Unable to get flavor list')
|
||||||
|
exceptions.check_message(["Connection", "refused"], msg)
|
||||||
|
raise
|
||||||
|
# Check if there is no flavor with the same name
|
||||||
|
if flavors is not None:
|
||||||
|
for flavor in flavors:
|
||||||
|
if flavor.name == name and flavor.id != flavor_id:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_('The name "%s" is already used by another '
|
||||||
|
'flavor.') % name)
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavorInfo(workflows.Step):
|
||||||
|
action_class = UpdateFlavorInfoAction
|
||||||
|
depends_on = ("flavor_id",)
|
||||||
|
contributes = ("name",
|
||||||
|
"vcpus",
|
||||||
|
"memory_mb",
|
||||||
|
"disk_gb",
|
||||||
|
"eph_gb",
|
||||||
|
"swap_mb")
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateFlavor(workflows.Workflow):
|
||||||
|
slug = "update_flavor"
|
||||||
|
name = _("Edit Flavor")
|
||||||
|
finalize_button_name = _("Save")
|
||||||
|
success_message = _('Modified flavor "%s".')
|
||||||
|
failure_message = _('Unable to modify flavor "%s".')
|
||||||
|
success_url = "horizon:admin:flavors:index"
|
||||||
|
default_steps = (UpdateFlavorInfo,
|
||||||
|
UpdateFlavorAccess)
|
||||||
|
|
||||||
|
def format_status_message(self, message):
|
||||||
|
return message % self.context['name']
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
flavor_projects = data["flavor_access"]
|
||||||
|
is_public = not flavor_projects
|
||||||
|
|
||||||
|
# Update flavor information
|
||||||
|
try:
|
||||||
|
flavor_id = data['flavor_id']
|
||||||
|
# Grab any existing extra specs, because flavor edit is currently
|
||||||
|
# implemented as a delete followed by a create.
|
||||||
|
extras_dict = api.nova.flavor_get_extras(self.request,
|
||||||
|
flavor_id,
|
||||||
|
raw=True)
|
||||||
|
# Mark the existing flavor as deleted.
|
||||||
|
api.nova.flavor_delete(request, flavor_id)
|
||||||
|
# Then create a new flavor with the same name but a new ID.
|
||||||
|
# This is in the same try/except block as the delete call
|
||||||
|
# because if the delete fails the API will error out because
|
||||||
|
# active flavors can't have the same name.
|
||||||
|
flavor = api.nova.flavor_create(request,
|
||||||
|
data['name'],
|
||||||
|
data['memory_mb'],
|
||||||
|
data['vcpus'],
|
||||||
|
data['disk_gb'],
|
||||||
|
ephemeral=data['eph_gb'],
|
||||||
|
swap=data['swap_mb'],
|
||||||
|
flavorid=flavor_id,
|
||||||
|
is_public=is_public)
|
||||||
|
if (extras_dict):
|
||||||
|
api.nova.flavor_extra_set(request, flavor.id, extras_dict)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, ignore=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Add flavor access if the flavor is not public.
|
||||||
|
for project in flavor_projects:
|
||||||
|
try:
|
||||||
|
api.nova.add_tenant_to_flavor(request, flavor.id, project)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _('Modified flavor information, '
|
||||||
|
'but unable to modify flavor '
|
||||||
|
'access.'))
|
||||||
|
return True
|
|
@ -18,6 +18,7 @@ import uuid
|
||||||
from novaclient.v1_1 import aggregates
|
from novaclient.v1_1 import aggregates
|
||||||
from novaclient.v1_1 import availability_zones
|
from novaclient.v1_1 import availability_zones
|
||||||
from novaclient.v1_1 import certs
|
from novaclient.v1_1 import certs
|
||||||
|
from novaclient.v1_1 import flavor_access
|
||||||
from novaclient.v1_1 import flavors
|
from novaclient.v1_1 import flavors
|
||||||
from novaclient.v1_1 import floating_ips
|
from novaclient.v1_1 import floating_ips
|
||||||
from novaclient.v1_1 import hypervisors
|
from novaclient.v1_1 import hypervisors
|
||||||
|
@ -149,6 +150,7 @@ USAGE_DATA = """
|
||||||
def data(TEST):
|
def data(TEST):
|
||||||
TEST.servers = utils.TestDataContainer()
|
TEST.servers = utils.TestDataContainer()
|
||||||
TEST.flavors = utils.TestDataContainer()
|
TEST.flavors = utils.TestDataContainer()
|
||||||
|
TEST.flavor_access = utils.TestDataContainer()
|
||||||
TEST.keypairs = utils.TestDataContainer()
|
TEST.keypairs = utils.TestDataContainer()
|
||||||
TEST.security_groups = utils.TestDataContainer()
|
TEST.security_groups = utils.TestDataContainer()
|
||||||
TEST.security_groups_uuid = utils.TestDataContainer()
|
TEST.security_groups_uuid = utils.TestDataContainer()
|
||||||
|
@ -228,6 +230,7 @@ def data(TEST):
|
||||||
'ram': 512,
|
'ram': 512,
|
||||||
'swap': 0,
|
'swap': 0,
|
||||||
'extra_specs': {},
|
'extra_specs': {},
|
||||||
|
'os-flavor-access:is_public': True,
|
||||||
'OS-FLV-EXT-DATA:ephemeral': 0})
|
'OS-FLV-EXT-DATA:ephemeral': 0})
|
||||||
flavor_2 = flavors.Flavor(flavors.FlavorManager(None),
|
flavor_2 = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
{'id': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
{'id': "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||||
|
@ -237,8 +240,28 @@ def data(TEST):
|
||||||
'ram': 10000,
|
'ram': 10000,
|
||||||
'swap': 0,
|
'swap': 0,
|
||||||
'extra_specs': {'Trusted': True, 'foo': 'bar'},
|
'extra_specs': {'Trusted': True, 'foo': 'bar'},
|
||||||
|
'os-flavor-access:is_public': True,
|
||||||
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||||
TEST.flavors.add(flavor_1, flavor_2)
|
flavor_3 = flavors.Flavor(flavors.FlavorManager(None),
|
||||||
|
{'id': "dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||||
|
'name': 'm1.secret',
|
||||||
|
'vcpus': 1000,
|
||||||
|
'disk': 1024,
|
||||||
|
'ram': 10000,
|
||||||
|
'swap': 0,
|
||||||
|
'extra_specs': {},
|
||||||
|
'os-flavor-access:is_public': False,
|
||||||
|
'OS-FLV-EXT-DATA:ephemeral': 2048})
|
||||||
|
TEST.flavors.add(flavor_1, flavor_2, flavor_3)
|
||||||
|
|
||||||
|
flavor_access_manager = flavor_access.FlavorAccessManager(None)
|
||||||
|
flavor_access_1 = flavor_access.FlavorAccess(flavor_access_manager,
|
||||||
|
{"tenant_id": "1",
|
||||||
|
"flavor_id": "dddddddd-dddd-dddd-dddd-dddddddddddd"})
|
||||||
|
flavor_access_2 = flavor_access.FlavorAccess(flavor_access_manager,
|
||||||
|
{"tenant_id": "2",
|
||||||
|
"flavor_id": "dddddddd-dddd-dddd-dddd-dddddddddddd"})
|
||||||
|
TEST.flavor_access.add(flavor_access_1, flavor_access_2)
|
||||||
|
|
||||||
# Keypairs
|
# Keypairs
|
||||||
keypair = keypairs.Keypair(keypairs.KeypairManager(None),
|
keypair = keypairs.Keypair(keypairs.KeypairManager(None),
|
||||||
|
|
Loading…
Reference in New Issue