Show resource usages for application
Show them in the description section right under the Flavor field title (as quota usages + predicted increment progress bar). Co-Authored-By: Timur Sufiev <tsufiev@gmail.com> Co-Authored-By: Artem Tiumentcev <darland.maik@gmail.com> Change-Id: I842cbce209ea90ab715d2e50824296a19c202a76
This commit is contained in:
parent
9e93ef3dbb
commit
93d8a1e160
|
@ -44,6 +44,9 @@ from horizon.forms import views
|
||||||
from horizon import messages
|
from horizon import messages
|
||||||
from horizon import tabs
|
from horizon import tabs
|
||||||
from horizon import views as generic_views
|
from horizon import views as generic_views
|
||||||
|
from novaclient import exceptions as nova_exceptions
|
||||||
|
from openstack_dashboard.api import nova
|
||||||
|
from openstack_dashboard.usage import quotas
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -358,6 +361,8 @@ class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
||||||
storage = attributes.setdefault('?', {}).setdefault(
|
storage = attributes.setdefault('?', {}).setdefault(
|
||||||
consts.DASHBOARD_ATTRS_KEY, {})
|
consts.DASHBOARD_ATTRS_KEY, {})
|
||||||
storage['name'] = app_name
|
storage['name'] = app_name
|
||||||
|
attributes['?']['resourceUsages'] = self.aggregate_usages(
|
||||||
|
self.init_usages()[1:])
|
||||||
|
|
||||||
do_redirect = self.get_wizard_flag('do_redirect')
|
do_redirect = self.get_wizard_flag('do_redirect')
|
||||||
wm_form_data = service.cleaned_data.get('workflowManagement')
|
wm_form_data = service.cleaned_data.get('workflowManagement')
|
||||||
|
@ -440,6 +445,122 @@ class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
||||||
value = self._get_wizard_param(key)
|
value = self._get_wizard_param(key)
|
||||||
return utils.ensure_python_obj(value)
|
return utils.ensure_python_obj(value)
|
||||||
|
|
||||||
|
def get_flavors(self):
|
||||||
|
try:
|
||||||
|
flavors = nova.flavor_list(self.request)
|
||||||
|
except nova_exceptions.ClientException:
|
||||||
|
message = _("Failed to get list of flavors.")
|
||||||
|
exceptions.handle(self.request, message)
|
||||||
|
LOG.exception(message)
|
||||||
|
flavors = []
|
||||||
|
|
||||||
|
def extract(flavor):
|
||||||
|
info = flavor._info
|
||||||
|
return {k: v for (k, v) in info.items() if k != 'links'}
|
||||||
|
flavors = [extract(f) for f in flavors]
|
||||||
|
self.storage.extra_data['flavors'] = flavors
|
||||||
|
return json.dumps(flavors)
|
||||||
|
|
||||||
|
def get_flavor_usages(self, form):
|
||||||
|
selected_flavor = form.cleaned_data['flavor']
|
||||||
|
for flavor in self.storage.extra_data['flavors']:
|
||||||
|
if flavor['name'] == selected_flavor:
|
||||||
|
return {'ram': flavor['ram'],
|
||||||
|
'vcpus': flavor['vcpus'],
|
||||||
|
'instances': 1}
|
||||||
|
|
||||||
|
def init_usages(self):
|
||||||
|
stored_data = self.storage.extra_data
|
||||||
|
step_usages = stored_data.get('step_usages')
|
||||||
|
if step_usages is None:
|
||||||
|
step_usages = [
|
||||||
|
collections.defaultdict(dict)
|
||||||
|
for step in self.steps.all
|
||||||
|
]
|
||||||
|
stored_data['step_usages'] = step_usages
|
||||||
|
|
||||||
|
environment_id = self.kwargs.get('environment_id')
|
||||||
|
environment_id = utils.ensure_python_obj(environment_id)
|
||||||
|
if environment_id is not None:
|
||||||
|
session_id = env_api.Session.get(self.request, environment_id)
|
||||||
|
client = api.muranoclient(self.request)
|
||||||
|
all_services = client.environments.get(
|
||||||
|
environment_id, session_id).services
|
||||||
|
env_usages = self.aggregate_usages(map(
|
||||||
|
lambda svc: svc['?'].get('resourceUsages', {}),
|
||||||
|
all_services))
|
||||||
|
else:
|
||||||
|
env_usages = collections.defaultdict(dict)
|
||||||
|
step_usages.insert(0, env_usages)
|
||||||
|
|
||||||
|
return step_usages
|
||||||
|
|
||||||
|
def process_step(self, form):
|
||||||
|
data = super(Wizard, self).process_step(form)
|
||||||
|
region = form.region or self.request.user.services_region
|
||||||
|
step_usages = self.init_usages()
|
||||||
|
if 'flavor' in form.cleaned_data:
|
||||||
|
usages = self.get_flavor_usages(form)
|
||||||
|
step_usages[self.steps.step0 + 1][region].update({
|
||||||
|
'ram': usages['ram'],
|
||||||
|
'vcpus': usages['vcpus'],
|
||||||
|
'instances': usages['instances']
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
step_usages[self.steps.step0 + 1][region].update({
|
||||||
|
'ram': 0,
|
||||||
|
'vcpus': 0,
|
||||||
|
'instances': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def update_usages(self, form, context):
|
||||||
|
data = self.init_usages()
|
||||||
|
usages = quotas.tenant_quota_usages(self.request).usages
|
||||||
|
region = self.request.user.services_region
|
||||||
|
inf = float('inf')
|
||||||
|
|
||||||
|
def get_usage(group, name, default):
|
||||||
|
return usages.get(group, {}).get(name, default)
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'usages': {
|
||||||
|
'maxTotalInstances': get_usage('instances', 'quota', inf),
|
||||||
|
'totalInstancesUsed': get_usage('instances', 'used', 0),
|
||||||
|
'maxTotalCores': get_usage('cores', 'quota', inf),
|
||||||
|
'totalCoresUsed': get_usage('cores', 'used', 0),
|
||||||
|
'maxTotalRAMSize': get_usage('ram', 'quota', inf),
|
||||||
|
'totalRAMUsed': get_usage('ram', 'used', 0),
|
||||||
|
},
|
||||||
|
'other_usages': {},
|
||||||
|
'flavors': self.get_flavors(),
|
||||||
|
'contexts': ['', 'info', 'success']
|
||||||
|
})
|
||||||
|
for step in range(self.steps.step0 + 1):
|
||||||
|
|
||||||
|
def sum_usage(context_key, data_key):
|
||||||
|
if context_key not in context['other_usages']:
|
||||||
|
context['other_usages'][context_key] = 0
|
||||||
|
context['other_usages'][context_key] += \
|
||||||
|
data[step][region].get(data_key, 0)
|
||||||
|
sum_usage('totalInstancesUsed', 'instances')
|
||||||
|
sum_usage('totalCoresUsed', 'vcpus')
|
||||||
|
sum_usage('totalRAMUsed', 'ram')
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def aggregate_usages(steps):
|
||||||
|
result = collections.defaultdict(dict)
|
||||||
|
for step in steps:
|
||||||
|
for region, region_usages in six.iteritems(step):
|
||||||
|
for metric, value in six.iteritems(region_usages):
|
||||||
|
if metric not in result[region]:
|
||||||
|
result[region][metric] = 0
|
||||||
|
result[region][metric] += value
|
||||||
|
return result
|
||||||
|
|
||||||
def get_context_data(self, form, **kwargs):
|
def get_context_data(self, form, **kwargs):
|
||||||
context = super(Wizard, self).get_context_data(form=form, **kwargs)
|
context = super(Wizard, self).get_context_data(form=form, **kwargs)
|
||||||
mc = api.muranoclient(self.request)
|
mc = api.muranoclient(self.request)
|
||||||
|
@ -477,6 +598,8 @@ class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
||||||
'field_descriptions': field_descr,
|
'field_descriptions': field_descr,
|
||||||
'extended_descriptions': extended_descr,
|
'extended_descriptions': extended_descr,
|
||||||
})
|
})
|
||||||
|
with helpers.current_region(self.request, form.region):
|
||||||
|
context = self.update_usages(form, context)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ from yaql import legacy
|
||||||
|
|
||||||
from muranodashboard.api import packages as pkg_api
|
from muranodashboard.api import packages as pkg_api
|
||||||
from muranodashboard.common import net
|
from muranodashboard.common import net
|
||||||
|
from muranodashboard.dynamic_ui import helpers
|
||||||
from muranodashboard.environments import api as env_api
|
from muranodashboard.environments import api as env_api
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,12 +107,13 @@ def wrap_regex_validator(validator, message):
|
||||||
return _validator
|
return _validator
|
||||||
|
|
||||||
|
|
||||||
def get_murano_images(request):
|
def get_murano_images(request, region=None):
|
||||||
images = []
|
images = []
|
||||||
try:
|
try:
|
||||||
# https://bugs.launchpad.net/murano/+bug/1339261 - glance
|
# https://bugs.launchpad.net/murano/+bug/1339261 - glance
|
||||||
# client version change alters the API. Other tuple values
|
# client version change alters the API. Other tuple values
|
||||||
# are _more and _prev (in recent glance client)
|
# are _more and _prev (in recent glance client)
|
||||||
|
with helpers.current_region(request, region):
|
||||||
images = glance.image_list_detailed(request)[0]
|
images = glance.image_list_detailed(request)[0]
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.error("Error to request image list from glance ")
|
LOG.error("Error to request image list from glance ")
|
||||||
|
@ -355,20 +357,31 @@ class DynamicChoiceField(hz_forms.DynamicChoiceField, CustomPropertiesField):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FlavorWidget(widgets.Select):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(FlavorWidget, self).__init__(*args, **kwargs)
|
||||||
|
self.attrs['class'] = self.attrs.get('class', '') + ' flavor'
|
||||||
|
self.attrs['id'] = 'id_flavor'
|
||||||
|
|
||||||
|
|
||||||
class FlavorChoiceField(ChoiceField):
|
class FlavorChoiceField(ChoiceField):
|
||||||
|
widget = FlavorWidget
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if 'requirements' in kwargs:
|
if 'requirements' in kwargs:
|
||||||
self.requirements = kwargs.pop('requirements')
|
self.requirements = kwargs.pop('requirements')
|
||||||
super(FlavorChoiceField, self).__init__(*args, **kwargs)
|
super(FlavorChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@with_request
|
@with_request
|
||||||
def update(self, request, **kwargs):
|
def update(self, request, form=None, **kwargs):
|
||||||
choices = []
|
choices = []
|
||||||
|
with helpers.current_region(request,
|
||||||
|
getattr(form, 'region', None)):
|
||||||
flavors = nova.novaclient(request).flavors.list()
|
flavors = nova.novaclient(request).flavors.list()
|
||||||
|
|
||||||
# If no requirements are present, return all the flavors.
|
# If no requirements are present, return all the flavors.
|
||||||
if not hasattr(self, 'requirements'):
|
if not hasattr(self, 'requirements'):
|
||||||
choices = [(flavor.name, flavor.name) for flavor in flavors]
|
choices = [(flavor.id, flavor.name) for flavor in flavors]
|
||||||
else:
|
else:
|
||||||
for flavor in flavors:
|
for flavor in flavors:
|
||||||
# If a flavor doesn't meet a minimum requirement,
|
# If a flavor doesn't meet a minimum requirement,
|
||||||
|
@ -389,7 +402,7 @@ class FlavorChoiceField(ChoiceField):
|
||||||
if 'max_memory_mb' in self.requirements:
|
if 'max_memory_mb' in self.requirements:
|
||||||
if flavor.ram > self.requirements['max_memory_mb']:
|
if flavor.ram > self.requirements['max_memory_mb']:
|
||||||
continue
|
continue
|
||||||
choices.append((flavor.name, flavor.name))
|
choices.append((flavor.id, flavor.name))
|
||||||
|
|
||||||
choices.sort(key=lambda e: e[1])
|
choices.sort(key=lambda e: e[1])
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
@ -401,20 +414,27 @@ class FlavorChoiceField(ChoiceField):
|
||||||
self.initial = kwargs["form"]["flavor"].value()
|
self.initial = kwargs["form"]["flavor"].value()
|
||||||
else:
|
else:
|
||||||
# Search through selected flavors
|
# Search through selected flavors
|
||||||
for flavor_name, flavor_name in self.choices:
|
for flavor_id, flavor_name in self.choices:
|
||||||
if 'medium' in flavor_name:
|
if 'medium' in flavor_name:
|
||||||
self.initial = flavor_name
|
self.initial = flavor_id
|
||||||
break
|
break
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
for flavor_id, flavor_name in self.choices:
|
||||||
|
if flavor_id == value:
|
||||||
|
return flavor_name
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
class KeyPairChoiceField(DynamicChoiceField):
|
class KeyPairChoiceField(DynamicChoiceField):
|
||||||
"""This widget allows to select keypair for VMs"""
|
"""This widget allows to select keypair for VMs"""
|
||||||
@with_request
|
@with_request
|
||||||
def update(self, request, **kwargs):
|
def update(self, request, form=None, **kwargs):
|
||||||
self.choices = [('', _('No keypair'))]
|
self.choices = [('', _('No keypair'))]
|
||||||
for keypair in sorted(
|
with helpers.current_region(request,
|
||||||
nova.novaclient(request).keypairs.list(),
|
getattr(form, 'region', None)):
|
||||||
key=lambda e: e.name):
|
keypairs = nova.novaclient(request).keypairs.list()
|
||||||
|
for keypair in sorted(keypairs, key=lambda e: e.name):
|
||||||
self.choices.append((keypair.name, keypair.name))
|
self.choices.append((keypair.name, keypair.name))
|
||||||
|
|
||||||
|
|
||||||
|
@ -450,9 +470,10 @@ class ImageChoiceField(ChoiceField):
|
||||||
super(ImageChoiceField, self).__init__(*args, **kwargs)
|
super(ImageChoiceField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
@with_request
|
@with_request
|
||||||
def update(self, request, **kwargs):
|
def update(self, request, form=None, **kwargs):
|
||||||
image_map, image_choices = {}, []
|
image_map, image_choices = {}, []
|
||||||
murano_images = get_murano_images(request)
|
murano_images = get_murano_images(
|
||||||
|
request, getattr(form, 'region', None))
|
||||||
for image in murano_images:
|
for image in murano_images:
|
||||||
murano_data = image.murano_property
|
murano_data = image.murano_property
|
||||||
title = murano_data.get('title', image.name)
|
title = murano_data.get('title', image.name)
|
||||||
|
@ -528,8 +549,10 @@ class NetworkChoiceField(ChoiceField):
|
||||||
|
|
||||||
class AZoneChoiceField(ChoiceField):
|
class AZoneChoiceField(ChoiceField):
|
||||||
@with_request
|
@with_request
|
||||||
def update(self, request, **kwargs):
|
def update(self, request, form=None, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
with helpers.current_region(request,
|
||||||
|
getattr(form, 'region', None)):
|
||||||
availability_zones = nova.novaclient(
|
availability_zones = nova.novaclient(
|
||||||
request).availability_zones.list(detailed=False)
|
request).availability_zones.list(detailed=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -223,10 +223,11 @@ class ServiceConfigurationForm(UpdatableFieldsForm):
|
||||||
field.compare(name, cleaned_data)
|
field.compare(name, cleaned_data)
|
||||||
|
|
||||||
if hasattr(field, 'postclean'):
|
if hasattr(field, 'postclean'):
|
||||||
value = field.postclean(self, cleaned_data)
|
value = field.postclean(self, name, cleaned_data)
|
||||||
if value:
|
if value:
|
||||||
cleaned_data[name] = value
|
cleaned_data[name] = value
|
||||||
LOG.debug("Update cleaned data in postclean method")
|
LOG.debug("Update '%s' data in postclean method" %
|
||||||
|
name)
|
||||||
|
|
||||||
self.service.update_cleaned_data(cleaned_data, form=self)
|
self.service.update_cleaned_data(cleaned_data, form=self)
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import types
|
import types
|
||||||
|
@ -159,3 +160,14 @@ def to_str(text):
|
||||||
elif isinstance(text, six.binary_type):
|
elif isinstance(text, six.binary_type):
|
||||||
text = text.decode('utf-8')
|
text = text.decode('utf-8')
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def current_region(request, region):
|
||||||
|
orig_region = request.user.services_region
|
||||||
|
if region is not None:
|
||||||
|
request.user.services_region = region
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
request.user.services_region = orig_region
|
||||||
|
|
|
@ -87,11 +87,12 @@ class Service(object):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
for form in forms:
|
for form in forms:
|
||||||
name, field_specs, validators = self.extract_form_data(form)
|
(name, field_specs, validators,
|
||||||
|
region) = self.extract_form_data(form)
|
||||||
# NOTE(kzaitsev) should be str (not unicode) under python2
|
# NOTE(kzaitsev) should be str (not unicode) under python2
|
||||||
# however it also works as str under python3
|
# however it also works as str under python3
|
||||||
name = helpers.to_str(name)
|
name = helpers.to_str(name)
|
||||||
self._add_form(name, field_specs, validators)
|
self._add_form(name, field_specs, validators, region)
|
||||||
|
|
||||||
# Add ManageWorkflowForm
|
# Add ManageWorkflowForm
|
||||||
workflow_form = catalog_forms.WorkflowManagementForm()
|
workflow_form = catalog_forms.WorkflowManagementForm()
|
||||||
|
@ -104,7 +105,8 @@ class Service(object):
|
||||||
workflow_form.field_specs,
|
workflow_form.field_specs,
|
||||||
workflow_form.validators)
|
workflow_form.validators)
|
||||||
|
|
||||||
def _add_form(self, _name, _specs, _validators, _verbose_name=None):
|
def _add_form(self, _name, _specs, _validators, _verbose_name=None,
|
||||||
|
_region=None):
|
||||||
import muranodashboard.dynamic_ui.forms as forms
|
import muranodashboard.dynamic_ui.forms as forms
|
||||||
|
|
||||||
class Form(six.with_metaclass(forms.DynamicFormMetaclass,
|
class Form(six.with_metaclass(forms.DynamicFormMetaclass,
|
||||||
|
@ -114,14 +116,15 @@ class Service(object):
|
||||||
verbose_name = _verbose_name
|
verbose_name = _verbose_name
|
||||||
field_specs = _specs
|
field_specs = _specs
|
||||||
validators = _validators
|
validators = _validators
|
||||||
|
region = _region
|
||||||
|
|
||||||
self.forms.append(Form)
|
self.forms.append(Form)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_form_data(data):
|
def extract_form_data(data):
|
||||||
for form_name, form_data in six.iteritems(data):
|
for form_name, form_data in six.iteritems(data):
|
||||||
return form_name, form_data['fields'], form_data.get('validators',
|
return (form_name, form_data['fields'],
|
||||||
[])
|
form_data.get('validators', []), form_data.get('region'))
|
||||||
|
|
||||||
def extract_attributes(self):
|
def extract_attributes(self):
|
||||||
context = self.context.create_child_context()
|
context = self.context.create_child_context()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "horizon/common/_modal_form.html" %}
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
{% load i18n humanize %}
|
{% load i18n horizon humanize bootstrap %}
|
||||||
{% block form_action %}
|
{% block form_action %}
|
||||||
{% url 'horizon:app-catalog:catalog:add' app_id environment_id do_redirect drop_wm_form %}
|
{% url 'horizon:app-catalog:catalog:add' app_id environment_id do_redirect drop_wm_form %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -60,6 +60,63 @@
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if usages %}
|
||||||
|
<script type="text/html" id="quota_bars">
|
||||||
|
<div class="quota_title">
|
||||||
|
<strong class="pull-left">{% trans "Number of Instances" %}</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{% blocktrans with used=usages.totalInstancesUsed|intcomma other_used=other_usages.totalInstancesUsed|intcomma quota=usages.maxTotalInstances|intcomma|quotainf %}
|
||||||
|
{{ used }} + {{ other_used }} of {{ quota }} used
|
||||||
|
{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="quota_instances"
|
||||||
|
class="quota_bar"
|
||||||
|
data-progress-indicator-flavor
|
||||||
|
data-quota-limit="{{ usages.maxTotalInstances }}"
|
||||||
|
data-quota-used="{{ usages.totalInstancesUsed }}">
|
||||||
|
{% widthratio usages.totalInstancesUsed usages.maxTotalInstances 100 as instance_percent %}
|
||||||
|
{% widthratio other_usages.totalInstancesUsed usages.maxTotalInstances 100 as instance_other_percent %}
|
||||||
|
{% bs_progress_bar instance_percent instance_other_percent 0 contexts=contexts %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quota_title">
|
||||||
|
<strong class="pull-left">{% trans "Number of VCPUs" %}</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{% blocktrans with used=usages.totalCoresUsed|intcomma other_used=other_usages.totalCoresUsed|intcomma quota=usages.maxTotalCores|intcomma|quotainf %}
|
||||||
|
{{ used }} + {{ other_used }} of {{ quota }} used
|
||||||
|
{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="quota_vcpus"
|
||||||
|
class="quota_bar"
|
||||||
|
data-progress-indicator-flavor
|
||||||
|
data-quota-limit="{{ usages.maxTotalCores }}"
|
||||||
|
data-quota-used="{{ usages.totalCoresUsed }}">
|
||||||
|
{% widthratio usages.totalCoresUsed usages.maxTotalCores 100 as vcpu_percent %}
|
||||||
|
{% widthratio other_usages.totalCoresUsed usages.maxTotalCores 100 as vcpu_other_percent %}
|
||||||
|
{% bs_progress_bar vcpu_percent vcpu_other_percent 0 contexts=contexts %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quota_title">
|
||||||
|
<strong class="pull-left">{% trans "Total RAM" %}</strong>
|
||||||
|
<span class="pull-right">
|
||||||
|
{% blocktrans with used=usages.totalRAMUsed|intcomma other_used=other_usages.totalRAMUsed|intcomma quota=usages.maxTotalRAMSize|intcomma|quotainf %}
|
||||||
|
{{ used }} + {{ other_used }} of {{ quota }} MB used
|
||||||
|
{% endblocktrans %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="quota_ram"
|
||||||
|
class="quota_bar"
|
||||||
|
data-progress-indicator-flavor
|
||||||
|
data-quota-limit="{{ usages.maxTotalRAMSize }}"
|
||||||
|
data-quota-used="{{ usages.totalRAMUsed }}">
|
||||||
|
{% widthratio usages.totalRAMUsed usages.maxTotalRAMSize 100 as ram_percent %}
|
||||||
|
{% widthratio other_usages.totalRAMUsed usages.maxTotalRAMSize 100 as ram_other_percent %}
|
||||||
|
{% bs_progress_bar ram_percent ram_other_percent 0 contexts=contexts %}
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block modal-footer %}
|
{% block modal-footer %}
|
||||||
|
@ -115,14 +172,14 @@
|
||||||
$button = elem.tagName == 'SELECT' && $elem.next().find('a'),
|
$button = elem.tagName == 'SELECT' && $elem.next().find('a'),
|
||||||
bindHandler = function($el) {
|
bindHandler = function($el) {
|
||||||
$el.blur(function() {
|
$el.blur(function() {
|
||||||
$descEntry.children('i').remove()
|
$descEntry.children('i').remove();
|
||||||
$descEntry.removeClass('selected-field')
|
$descEntry.removeClass('selected-field');
|
||||||
}).focus(function() {
|
}).focus(function() {
|
||||||
// remove <i> if previous form without submit
|
// remove <i> if previous form without submit
|
||||||
$descEntry.children('i').remove()
|
$descEntry.children('i').remove();
|
||||||
$descEntry.addClass('selected-field')
|
$descEntry.addClass('selected-field');
|
||||||
$descEntry.prepend(
|
$descEntry.prepend(
|
||||||
"<i class='fa fa-chevron-circle-right'></i>")
|
"<i class='fa fa-chevron-circle-right'></i>");
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
bindHandler($elem);
|
bindHandler($elem);
|
||||||
|
@ -131,6 +188,56 @@
|
||||||
bindHandler($button);
|
bindHandler($button);
|
||||||
}
|
}
|
||||||
}).filter(':first').trigger('focus');
|
}).filter(':first').trigger('focus');
|
||||||
|
|
||||||
|
// Update flavor specs in a description area
|
||||||
|
var $flavorElem = $modal.find('.form-group select.flavor');
|
||||||
|
if ($flavorElem.length) {
|
||||||
|
var name = $flavorElem.attr('name'),
|
||||||
|
$flavorTitle = $modal.find('strong[data-field-name*="'+name+'"]').closest('p'),
|
||||||
|
$flavorSpecs = $flavorTitle.find('.flavor-specs');
|
||||||
|
|
||||||
|
if ($flavorSpecs.length == 0) {
|
||||||
|
$flavorTitle.append('<div class="flavor-specs"></div>');
|
||||||
|
$flavorSpecs = $flavorTitle.find('.flavor-specs');
|
||||||
|
}
|
||||||
|
var flavors = {{ flavors|safe|default:"{}" }};
|
||||||
|
if (!$flavorSpecs.find('.progress').length && flavors.length) {
|
||||||
|
$flavorSpecs.append($('#quota_bars').html());
|
||||||
|
horizon.Quota.initWithFlavors(flavors);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update quota titles according to the selected flavor
|
||||||
|
var updateQuotaTitles = function() {
|
||||||
|
var appendVal = function(elem, value) {
|
||||||
|
var origTitle = elem.data('orig-title');
|
||||||
|
if (!origTitle) {
|
||||||
|
elem.data('orig-title', origTitle = elem.text());
|
||||||
|
}
|
||||||
|
elem.text(value + ' + ' + origTitle);
|
||||||
|
};
|
||||||
|
var selFlavor = $.grep(flavors, function(flavor) {
|
||||||
|
return flavor.id === $flavorElem.val();
|
||||||
|
})[0];
|
||||||
|
$flavorSpecs.find('.quota_title span').each(function(idx) {
|
||||||
|
switch (idx) {
|
||||||
|
// instance count title case
|
||||||
|
case 0:
|
||||||
|
appendVal($(this), 1);
|
||||||
|
break;
|
||||||
|
// VCPU count title case
|
||||||
|
case 1:
|
||||||
|
appendVal($(this), selFlavor.vcpus);
|
||||||
|
break;
|
||||||
|
// RAM amount title case
|
||||||
|
case 2:
|
||||||
|
appendVal($(this), selFlavor.ram);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
updateQuotaTitles();
|
||||||
|
$flavorElem.on('change', updateQuotaTitles);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// show full name on text overflow
|
// show full name on text overflow
|
||||||
$('.modal-dialog h3').each(function () {
|
$('.modal-dialog h3').each(function () {
|
||||||
|
|
|
@ -24,19 +24,20 @@ class TestFlavorField(helpers.APITestCase):
|
||||||
super(TestFlavorField, self).setUp()
|
super(TestFlavorField, self).setUp()
|
||||||
|
|
||||||
class FlavorFlave(object):
|
class FlavorFlave(object):
|
||||||
def __init__(self, name, vcpus, disk, ram):
|
def __init__(self, id, name, vcpus, disk, ram):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.vcpus = vcpus
|
self.vcpus = vcpus
|
||||||
self.disk = disk
|
self.disk = disk
|
||||||
self.ram = ram
|
self.ram = ram
|
||||||
|
self.id = id
|
||||||
|
|
||||||
novaclient = self.stub_novaclient()
|
novaclient = self.stub_novaclient()
|
||||||
novaclient.flavors = self.mox.CreateMockAnything()
|
novaclient.flavors = self.mox.CreateMockAnything()
|
||||||
# Set up the Flavor list
|
# Set up the Flavor list
|
||||||
novaclient.flavors.list().MultipleTimes().AndReturn(
|
novaclient.flavors.list().MultipleTimes().AndReturn(
|
||||||
[FlavorFlave('small', vcpus=1, disk=50, ram=1000),
|
[FlavorFlave('id1', 'small', vcpus=1, disk=50, ram=1000),
|
||||||
FlavorFlave('medium', vcpus=2, disk=100, ram=2000),
|
FlavorFlave('id2', 'medium', vcpus=2, disk=100, ram=2000),
|
||||||
FlavorFlave('large', vcpus=3, disk=750, ram=4000)])
|
FlavorFlave('id3', 'large', vcpus=3, disk=750, ram=4000)])
|
||||||
|
|
||||||
def test_no_filter(self):
|
def test_no_filter(self):
|
||||||
"""Check that all flavors are returned."""
|
"""Check that all flavors are returned."""
|
||||||
|
@ -48,9 +49,9 @@ class TestFlavorField(helpers.APITestCase):
|
||||||
initial_request = {}
|
initial_request = {}
|
||||||
f.update(initial_request, self.request)
|
f.update(initial_request, self.request)
|
||||||
self.assertEqual([
|
self.assertEqual([
|
||||||
('large', 'large'),
|
('id3', 'large'),
|
||||||
('medium', 'medium'),
|
('id2', 'medium'),
|
||||||
('small', 'small')
|
('id1', 'small')
|
||||||
], f.choices)
|
], f.choices)
|
||||||
|
|
||||||
def test_multiple_filter(self):
|
def test_multiple_filter(self):
|
||||||
|
@ -60,9 +61,8 @@ class TestFlavorField(helpers.APITestCase):
|
||||||
|
|
||||||
# Fake a requirement for 2 CPUs, should return medium and large
|
# Fake a requirement for 2 CPUs, should return medium and large
|
||||||
f = fields.FlavorChoiceField(requirements={'min_vcpus': 2})
|
f = fields.FlavorChoiceField(requirements={'min_vcpus': 2})
|
||||||
initial_request = {}
|
f.update({}, self.request)
|
||||||
f.update(initial_request, self.request)
|
self.assertEqual([('id3', 'large'), ('id2', 'medium')], f.choices)
|
||||||
self.assertEqual([('large', 'large'), ('medium', 'medium')], f.choices)
|
|
||||||
|
|
||||||
def test_single_filter(self):
|
def test_single_filter(self):
|
||||||
"""Check that one flavor is returned."""
|
"""Check that one flavor is returned."""
|
||||||
|
@ -73,7 +73,7 @@ class TestFlavorField(helpers.APITestCase):
|
||||||
requirements={'min_vcpus': 2, 'min_disk': 200})
|
requirements={'min_vcpus': 2, 'min_disk': 200})
|
||||||
initial_request = {}
|
initial_request = {}
|
||||||
f.update(initial_request, self.request)
|
f.update(initial_request, self.request)
|
||||||
self.assertEqual([('large', 'large')], f.choices)
|
self.assertEqual([('id3', 'large')], f.choices)
|
||||||
|
|
||||||
def test_no_matches_filter(self):
|
def test_no_matches_filter(self):
|
||||||
"""Check that no flavors are returned."""
|
"""Check that no flavors are returned."""
|
||||||
|
|
|
@ -365,13 +365,31 @@ class TestWizard(testtools.TestCase):
|
||||||
for key, val in expected.items():
|
for key, val in expected.items():
|
||||||
self.assertEqual(val, result[key])
|
self.assertEqual(val, result[key])
|
||||||
|
|
||||||
|
@mock.patch.object(
|
||||||
|
views, 'nova',
|
||||||
|
mock.MagicMock(side_effect=views.nova_exceptions.ClientException))
|
||||||
|
def test_get_flavors(self):
|
||||||
|
result = self.wizard.get_flavors()
|
||||||
|
|
||||||
|
self.assertEqual('[]', result)
|
||||||
|
views.nova.flavor_list.assert_called_once_with(self.wizard.request)
|
||||||
|
|
||||||
|
@mock.patch.object(views, 'nova')
|
||||||
|
@mock.patch.object(views, 'quotas')
|
||||||
@mock.patch.object(views, 'services')
|
@mock.patch.object(views, 'services')
|
||||||
@mock.patch.object(views, 'api')
|
@mock.patch.object(views, 'api')
|
||||||
def test_get_context_data(self, mock_api, mock_services):
|
def test_get_context_data(self, mock_api, mock_services, mock_quotas,
|
||||||
|
mock_nova):
|
||||||
mock_api.muranoclient().environments.get().name = 'foo_env_name'
|
mock_api.muranoclient().environments.get().name = 'foo_env_name'
|
||||||
mock_services.get_app_field_descriptions.return_value = [
|
mock_services.get_app_field_descriptions.return_value = [
|
||||||
'foo_field_descr', 'foo_extended_descr'
|
'foo_field_descr', 'foo_extended_descr'
|
||||||
]
|
]
|
||||||
|
mock_nova.flavor_list.return_value = [
|
||||||
|
type('FakeFlavor%s' % k, (object, ),
|
||||||
|
{'id': 'fake_id_%s' % k, 'name': 'fake_name_%s' % k,
|
||||||
|
'_info': {'foo': 'bar'}})
|
||||||
|
for k in (1, 2)
|
||||||
|
]
|
||||||
|
|
||||||
form = mock.Mock()
|
form = mock.Mock()
|
||||||
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
||||||
|
@ -380,7 +398,7 @@ class TestWizard(testtools.TestCase):
|
||||||
self.wizard.request.GET = {}
|
self.wizard.request.GET = {}
|
||||||
self.wizard.request.POST = {}
|
self.wizard.request.POST = {}
|
||||||
self.wizard.storage.extra_data.get.return_value = app
|
self.wizard.storage.extra_data.get.return_value = app
|
||||||
self.wizard.steps = mock.Mock(index='foo_step_index')
|
self.wizard.steps = mock.Mock(index='foo_step_index', step0=-1)
|
||||||
self.wizard.prefix = 'foo_prefix'
|
self.wizard.prefix = 'foo_prefix'
|
||||||
self.wizard.kwargs['do_redirect'] = 'foo_do_redirect'
|
self.wizard.kwargs['do_redirect'] = 'foo_do_redirect'
|
||||||
self.wizard.kwargs['drop_wm_form'] = 'foo_drop_wm_form'
|
self.wizard.kwargs['drop_wm_form'] = 'foo_drop_wm_form'
|
||||||
|
@ -407,13 +425,17 @@ class TestWizard(testtools.TestCase):
|
||||||
'foo_env_id')
|
'foo_env_id')
|
||||||
mock_services.get_app_field_descriptions.assert_called_once_with(
|
mock_services.get_app_field_descriptions.assert_called_once_with(
|
||||||
self.wizard.request, 'foo_app_id', 'foo_step_index')
|
self.wizard.request, 'foo_app_id', 'foo_step_index')
|
||||||
|
mock_nova.flavor_list.assert_called_once_with(self.wizard.request)
|
||||||
|
|
||||||
|
@mock.patch.object(views, 'nova')
|
||||||
|
@mock.patch.object(views, 'quotas')
|
||||||
@mock.patch.object(views, 'env_api')
|
@mock.patch.object(views, 'env_api')
|
||||||
@mock.patch.object(views, 'utils')
|
@mock.patch.object(views, 'utils')
|
||||||
@mock.patch.object(views, 'services')
|
@mock.patch.object(views, 'services')
|
||||||
@mock.patch.object(views, 'api')
|
@mock.patch.object(views, 'api')
|
||||||
def test_get_context_data_alternate_control_flow(
|
def test_get_context_data_alternate_control_flow(
|
||||||
self, mock_api, mock_services, mock_utils, mock_env_api):
|
self, mock_api, mock_services, mock_utils, mock_env_api,
|
||||||
|
mock_quatas, mock_nova):
|
||||||
form = mock.Mock()
|
form = mock.Mock()
|
||||||
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
||||||
app.configure_mock(name='foo_app')
|
app.configure_mock(name='foo_app')
|
||||||
|
@ -425,11 +447,18 @@ class TestWizard(testtools.TestCase):
|
||||||
]
|
]
|
||||||
mock_utils.ensure_python_obj.return_value = None
|
mock_utils.ensure_python_obj.return_value = None
|
||||||
mock_env_api.environments_list.return_value = []
|
mock_env_api.environments_list.return_value = []
|
||||||
|
mock_nova.flavor_list.return_value = [
|
||||||
|
type('FakeFlavor%s' % k, (object, ),
|
||||||
|
{'id': 'fake_id_%s' % k, 'name': 'fake_name_%s' % k,
|
||||||
|
'_info': {'foo': 'bar'}})
|
||||||
|
for k in (1, 2)
|
||||||
|
]
|
||||||
|
|
||||||
self.wizard.request.GET = {}
|
self.wizard.request.GET = {}
|
||||||
self.wizard.request.POST = {'wizard_id': 'foo_wizard_id'}
|
self.wizard.request.POST = {'wizard_id': 'foo_wizard_id'}
|
||||||
self.wizard.storage.extra_data = {}
|
self.wizard.storage.extra_data = {}
|
||||||
self.wizard.steps = mock.Mock(index='foo_step_index')
|
self.wizard.steps = mock.Mock(index='foo_step_index', step0=0)
|
||||||
|
self.wizard.steps.all = []
|
||||||
self.wizard.prefix = 'foo_prefix'
|
self.wizard.prefix = 'foo_prefix'
|
||||||
context = self.wizard.get_context_data(form)
|
context = self.wizard.get_context_data(form)
|
||||||
|
|
||||||
|
@ -456,6 +485,7 @@ class TestWizard(testtools.TestCase):
|
||||||
mock_api.muranoclient().environments.get.assert_called_once_with()
|
mock_api.muranoclient().environments.get.assert_called_once_with()
|
||||||
mock_services.get_app_field_descriptions.assert_called_once_with(
|
mock_services.get_app_field_descriptions.assert_called_once_with(
|
||||||
self.wizard.request, 'foo_app_id', 'foo_step_index')
|
self.wizard.request, 'foo_app_id', 'foo_step_index')
|
||||||
|
mock_nova.flavor_list.assert_called_once_with(self.wizard.request)
|
||||||
|
|
||||||
|
|
||||||
class TestIndexView(testtools.TestCase):
|
class TestIndexView(testtools.TestCase):
|
||||||
|
|
|
@ -27,7 +27,9 @@ class TestFields(testtools.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestFields, self).setUp()
|
super(TestFields, self).setUp()
|
||||||
self.request = {'request': mock.Mock()}
|
self.request = mock.Mock()
|
||||||
|
self.request.user.service_region = None
|
||||||
|
self.request.is_ajax = mock.Mock(side_effect=False)
|
||||||
self.addCleanup(mock.patch.stopall)
|
self.addCleanup(mock.patch.stopall)
|
||||||
|
|
||||||
@mock.patch.object(fields, 'LOG')
|
@mock.patch.object(fields, 'LOG')
|
||||||
|
@ -220,7 +222,7 @@ class TestFields(testtools.TestCase):
|
||||||
self.assertEqual('DynamicSelect', dynamic_select_cls.__name__)
|
self.assertEqual('DynamicSelect', dynamic_select_cls.__name__)
|
||||||
|
|
||||||
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
||||||
dynamic_select.update(self.request, environment_id='foo_env_id')
|
dynamic_select.update({}, self.request, environment_id='foo_env_id')
|
||||||
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
hasattr(dynamic_select.widget.add_item_link, '__call__'))
|
hasattr(dynamic_select.widget.add_item_link, '__call__'))
|
||||||
|
@ -228,9 +230,9 @@ class TestFields(testtools.TestCase):
|
||||||
self.assertIsNone(dynamic_select.initial)
|
self.assertIsNone(dynamic_select.initial)
|
||||||
|
|
||||||
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
||||||
self.request['request'], 'foo_class_fqn')
|
self.request, 'foo_class_fqn')
|
||||||
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
||||||
self.request['request'], 'foo_env_id',
|
self.request, 'foo_env_id',
|
||||||
['foo_class_fqn', 'bar_class_fqn']
|
['foo_class_fqn', 'bar_class_fqn']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -252,15 +254,15 @@ class TestFields(testtools.TestCase):
|
||||||
|
|
||||||
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
||||||
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
||||||
dynamic_select.update(self.request, environment_id='foo_env_id')
|
dynamic_select.update({}, self.request, environment_id='foo_env_id')
|
||||||
|
|
||||||
self.assertEqual(expected_choices, dynamic_select.choices)
|
self.assertEqual(expected_choices, dynamic_select.choices)
|
||||||
self.assertEqual('foo_app_id', dynamic_select.initial)
|
self.assertEqual('foo_app_id', dynamic_select.initial)
|
||||||
|
|
||||||
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
||||||
self.request['request'], 'foo_class_fqn')
|
self.request, 'foo_class_fqn')
|
||||||
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
||||||
self.request['request'], 'foo_env_id', ['foo_class_fqn']
|
self.request, 'foo_env_id', ['foo_class_fqn']
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock.patch.object(fields, 'env_api')
|
@mock.patch.object(fields, 'env_api')
|
||||||
|
@ -274,15 +276,15 @@ class TestFields(testtools.TestCase):
|
||||||
|
|
||||||
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
||||||
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
||||||
dynamic_select.update(self.request, environment_id='foo_env_id')
|
dynamic_select.update({}, self.request, environment_id='foo_env_id')
|
||||||
|
|
||||||
self.assertEqual(expected_choices, dynamic_select.choices)
|
self.assertEqual(expected_choices, dynamic_select.choices)
|
||||||
self.assertIsNone(dynamic_select.initial)
|
self.assertIsNone(dynamic_select.initial)
|
||||||
|
|
||||||
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
mock_pkg_api.app_by_fqn.assert_called_once_with(
|
||||||
self.request['request'], 'foo_class_fqn')
|
self.request, 'foo_class_fqn')
|
||||||
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
mock_env_api.service_list_by_fqns.assert_called_once_with(
|
||||||
self.request['request'], 'foo_env_id', [])
|
self.request, 'foo_env_id', [])
|
||||||
|
|
||||||
@mock.patch.object(fields, 'reverse')
|
@mock.patch.object(fields, 'reverse')
|
||||||
@mock.patch.object(fields, 'env_api')
|
@mock.patch.object(fields, 'env_api')
|
||||||
|
@ -296,7 +298,7 @@ class TestFields(testtools.TestCase):
|
||||||
|
|
||||||
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
dynamic_select_cls = fields.make_select_cls('foo_class_fqn')
|
||||||
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
dynamic_select = dynamic_select_cls(empty_value_message='Foo')
|
||||||
dynamic_select.update(self.request, environment_id='foo_env_id')
|
dynamic_select.update({}, self.request, environment_id='foo_env_id')
|
||||||
|
|
||||||
result = dynamic_select.widget.add_item_link()
|
result = dynamic_select.widget.add_item_link()
|
||||||
self.assertEqual('', result)
|
self.assertEqual('', result)
|
||||||
|
@ -304,7 +306,7 @@ class TestFields(testtools.TestCase):
|
||||||
mock_pkg = mock.Mock(fully_qualified_name='foo_class_fqn')
|
mock_pkg = mock.Mock(fully_qualified_name='foo_class_fqn')
|
||||||
mock_pkg.configure_mock(name='foo_class_name')
|
mock_pkg.configure_mock(name='foo_class_name')
|
||||||
mock_pkg_api.app_by_fqn.return_value = mock_pkg
|
mock_pkg_api.app_by_fqn.return_value = mock_pkg
|
||||||
dynamic_select.update(self.request, environment_id='foo_env_id')
|
dynamic_select.update({}, self.request, environment_id='foo_env_id')
|
||||||
|
|
||||||
result = dynamic_select.widget.add_item_link()
|
result = dynamic_select.widget.add_item_link()
|
||||||
expected = '[["foo_class_name", "foo_url"]]'
|
expected = '[["foo_class_name", "foo_url"]]'
|
||||||
|
@ -492,11 +494,11 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||||
|
|
||||||
self.request = {'request': mock.Mock()}
|
self.request = {'request': mock.Mock()}
|
||||||
self.tiny_flavor = mock.Mock()
|
self.tiny_flavor = mock.Mock()
|
||||||
self.tiny_flavor.configure_mock(name='m1.tiny')
|
self.tiny_flavor.configure_mock(id='id1', name='m1.tiny')
|
||||||
self.small_flavor = mock.Mock()
|
self.small_flavor = mock.Mock()
|
||||||
self.small_flavor.configure_mock(name='m1.small')
|
self.small_flavor.configure_mock(id='id2', name='m1.small')
|
||||||
self.medium_flavor = mock.Mock()
|
self.medium_flavor = mock.Mock()
|
||||||
self.medium_flavor.configure_mock(name='m1.medium')
|
self.medium_flavor.configure_mock(id='id3', name='m1.medium')
|
||||||
|
|
||||||
self.addCleanup(mock.patch.stopall)
|
self.addCleanup(mock.patch.stopall)
|
||||||
|
|
||||||
|
@ -507,7 +509,7 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||||
self.tiny_flavor, self.small_flavor, self.medium_flavor
|
self.tiny_flavor, self.small_flavor, self.medium_flavor
|
||||||
]
|
]
|
||||||
expected_choices = [
|
expected_choices = [
|
||||||
('m1.medium', 'm1.medium'), ('m1.small', 'm1.small')
|
('id3', 'm1.medium'), ('id2', 'm1.small')
|
||||||
]
|
]
|
||||||
valid_requirements = [
|
valid_requirements = [
|
||||||
('vcpus', 2), ('disk', 101), ('ram', 501)
|
('vcpus', 2), ('disk', 101), ('ram', 501)
|
||||||
|
@ -529,7 +531,7 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||||
self.flavor_choice_field.update(self.request)
|
self.flavor_choice_field.update(self.request)
|
||||||
self.assertEqual(expected_choices,
|
self.assertEqual(expected_choices,
|
||||||
self.flavor_choice_field.choices)
|
self.flavor_choice_field.choices)
|
||||||
self.assertEqual('m1.medium', self.flavor_choice_field.initial)
|
self.assertEqual('id3', self.flavor_choice_field.initial)
|
||||||
|
|
||||||
@mock.patch.object(fields, 'nova')
|
@mock.patch.object(fields, 'nova')
|
||||||
def test_update_without_requirements(self, mock_nova):
|
def test_update_without_requirements(self, mock_nova):
|
||||||
|
@ -539,14 +541,14 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||||
del self.flavor_choice_field.requirements
|
del self.flavor_choice_field.requirements
|
||||||
|
|
||||||
expected_choices = [
|
expected_choices = [
|
||||||
('m1.medium', 'm1.medium'),
|
('id3', 'm1.medium'),
|
||||||
('m1.small', 'm1.small'),
|
('id2', 'm1.small'),
|
||||||
('m1.tiny', 'm1.tiny')
|
('id1', 'm1.tiny')
|
||||||
]
|
]
|
||||||
|
|
||||||
self.flavor_choice_field.update(self.request)
|
self.flavor_choice_field.update(self.request)
|
||||||
self.assertEqual(expected_choices, self.flavor_choice_field.choices)
|
self.assertEqual(expected_choices, self.flavor_choice_field.choices)
|
||||||
self.assertEqual('m1.medium', self.flavor_choice_field.initial)
|
self.assertEqual('id3', self.flavor_choice_field.initial)
|
||||||
|
|
||||||
|
|
||||||
class TestKeyPairChoiceField(testtools.TestCase):
|
class TestKeyPairChoiceField(testtools.TestCase):
|
||||||
|
|
|
@ -181,10 +181,10 @@ class TestServiceConfigurationForm(testtools.TestCase):
|
||||||
# below, rather than `{'foo': 'bar', 'baz': 'qux'}` because
|
# below, rather than `{'foo': 'bar', 'baz': 'qux'}` because
|
||||||
# `cleaned_data[name] = value` in clean() appears to also change the
|
# `cleaned_data[name] = value` in clean() appears to also change the
|
||||||
# dict that was passed in to mock objects in previous lines of code.
|
# dict that was passed in to mock objects in previous lines of code.
|
||||||
foo_field.postclean.assert_called_once_with(self.form, mock.ANY)
|
foo_field.postclean.assert_called_once_with(self.form, 'foo', mock.ANY)
|
||||||
password_field.compare.assert_called_once_with('password', mock.ANY)
|
password_field.compare.assert_called_once_with('password', mock.ANY)
|
||||||
mock_log.debug.assert_called_once_with(
|
mock_log.debug.assert_called_once_with(
|
||||||
"Update cleaned data in postclean method")
|
"Update 'foo' data in postclean method")
|
||||||
self.form.service.update_cleaned_data.assert_called_with(
|
self.form.service.update_cleaned_data.assert_called_with(
|
||||||
mock.ANY, form=self.form)
|
mock.ANY, form=self.form)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Show resource usages in the description section right under the Flavor field
|
||||||
|
title (as quota usages + predicted increment progress bar).
|
Loading…
Reference in New Issue