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 tabs
|
||||
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
|
||||
import six
|
||||
|
||||
@ -358,6 +361,8 @@ class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
||||
storage = attributes.setdefault('?', {}).setdefault(
|
||||
consts.DASHBOARD_ATTRS_KEY, {})
|
||||
storage['name'] = app_name
|
||||
attributes['?']['resourceUsages'] = self.aggregate_usages(
|
||||
self.init_usages()[1:])
|
||||
|
||||
do_redirect = self.get_wizard_flag('do_redirect')
|
||||
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)
|
||||
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):
|
||||
context = super(Wizard, self).get_context_data(form=form, **kwargs)
|
||||
mc = api.muranoclient(self.request)
|
||||
@ -477,6 +598,8 @@ class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
||||
'field_descriptions': field_descr,
|
||||
'extended_descriptions': extended_descr,
|
||||
})
|
||||
with helpers.current_region(self.request, form.region):
|
||||
context = self.update_usages(form, context)
|
||||
return context
|
||||
|
||||
|
||||
|
@ -39,6 +39,7 @@ from yaql import legacy
|
||||
|
||||
from muranodashboard.api import packages as pkg_api
|
||||
from muranodashboard.common import net
|
||||
from muranodashboard.dynamic_ui import helpers
|
||||
from muranodashboard.environments import api as env_api
|
||||
|
||||
|
||||
@ -106,13 +107,14 @@ def wrap_regex_validator(validator, message):
|
||||
return _validator
|
||||
|
||||
|
||||
def get_murano_images(request):
|
||||
def get_murano_images(request, region=None):
|
||||
images = []
|
||||
try:
|
||||
# https://bugs.launchpad.net/murano/+bug/1339261 - glance
|
||||
# client version change alters the API. Other tuple values
|
||||
# are _more and _prev (in recent glance client)
|
||||
images = glance.image_list_detailed(request)[0]
|
||||
with helpers.current_region(request, region):
|
||||
images = glance.image_list_detailed(request)[0]
|
||||
except Exception:
|
||||
LOG.error("Error to request image list from glance ")
|
||||
exceptions.handle(request, _("Unable to retrieve public images."))
|
||||
@ -355,20 +357,31 @@ class DynamicChoiceField(hz_forms.DynamicChoiceField, CustomPropertiesField):
|
||||
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):
|
||||
widget = FlavorWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'requirements' in kwargs:
|
||||
self.requirements = kwargs.pop('requirements')
|
||||
super(FlavorChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
@with_request
|
||||
def update(self, request, **kwargs):
|
||||
def update(self, request, form=None, **kwargs):
|
||||
choices = []
|
||||
flavors = nova.novaclient(request).flavors.list()
|
||||
with helpers.current_region(request,
|
||||
getattr(form, 'region', None)):
|
||||
flavors = nova.novaclient(request).flavors.list()
|
||||
|
||||
# If no requirements are present, return all the flavors.
|
||||
if not hasattr(self, 'requirements'):
|
||||
choices = [(flavor.name, flavor.name) for flavor in flavors]
|
||||
choices = [(flavor.id, flavor.name) for flavor in flavors]
|
||||
else:
|
||||
for flavor in flavors:
|
||||
# If a flavor doesn't meet a minimum requirement,
|
||||
@ -389,7 +402,7 @@ class FlavorChoiceField(ChoiceField):
|
||||
if 'max_memory_mb' in self.requirements:
|
||||
if flavor.ram > self.requirements['max_memory_mb']:
|
||||
continue
|
||||
choices.append((flavor.name, flavor.name))
|
||||
choices.append((flavor.id, flavor.name))
|
||||
|
||||
choices.sort(key=lambda e: e[1])
|
||||
self.choices = choices
|
||||
@ -401,20 +414,27 @@ class FlavorChoiceField(ChoiceField):
|
||||
self.initial = kwargs["form"]["flavor"].value()
|
||||
else:
|
||||
# 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:
|
||||
self.initial = flavor_name
|
||||
self.initial = flavor_id
|
||||
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):
|
||||
"""This widget allows to select keypair for VMs"""
|
||||
@with_request
|
||||
def update(self, request, **kwargs):
|
||||
def update(self, request, form=None, **kwargs):
|
||||
self.choices = [('', _('No keypair'))]
|
||||
for keypair in sorted(
|
||||
nova.novaclient(request).keypairs.list(),
|
||||
key=lambda e: e.name):
|
||||
with helpers.current_region(request,
|
||||
getattr(form, 'region', None)):
|
||||
keypairs = nova.novaclient(request).keypairs.list()
|
||||
for keypair in sorted(keypairs, key=lambda e: e.name):
|
||||
self.choices.append((keypair.name, keypair.name))
|
||||
|
||||
|
||||
@ -450,9 +470,10 @@ class ImageChoiceField(ChoiceField):
|
||||
super(ImageChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
@with_request
|
||||
def update(self, request, **kwargs):
|
||||
def update(self, request, form=None, **kwargs):
|
||||
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:
|
||||
murano_data = image.murano_property
|
||||
title = murano_data.get('title', image.name)
|
||||
@ -528,10 +549,12 @@ class NetworkChoiceField(ChoiceField):
|
||||
|
||||
class AZoneChoiceField(ChoiceField):
|
||||
@with_request
|
||||
def update(self, request, **kwargs):
|
||||
def update(self, request, form=None, **kwargs):
|
||||
try:
|
||||
availability_zones = nova.novaclient(
|
||||
request).availability_zones.list(detailed=False)
|
||||
with helpers.current_region(request,
|
||||
getattr(form, 'region', None)):
|
||||
availability_zones = nova.novaclient(
|
||||
request).availability_zones.list(detailed=False)
|
||||
except Exception:
|
||||
availability_zones = []
|
||||
exceptions.handle(request,
|
||||
|
@ -223,10 +223,11 @@ class ServiceConfigurationForm(UpdatableFieldsForm):
|
||||
field.compare(name, cleaned_data)
|
||||
|
||||
if hasattr(field, 'postclean'):
|
||||
value = field.postclean(self, cleaned_data)
|
||||
value = field.postclean(self, name, cleaned_data)
|
||||
if 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)
|
||||
return cleaned_data
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import string
|
||||
import types
|
||||
@ -159,3 +160,14 @@ def to_str(text):
|
||||
elif isinstance(text, six.binary_type):
|
||||
text = text.decode('utf-8')
|
||||
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)
|
||||
|
||||
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
|
||||
# however it also works as str under python3
|
||||
name = helpers.to_str(name)
|
||||
self._add_form(name, field_specs, validators)
|
||||
self._add_form(name, field_specs, validators, region)
|
||||
|
||||
# Add ManageWorkflowForm
|
||||
workflow_form = catalog_forms.WorkflowManagementForm()
|
||||
@ -104,7 +105,8 @@ class Service(object):
|
||||
workflow_form.field_specs,
|
||||
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
|
||||
|
||||
class Form(six.with_metaclass(forms.DynamicFormMetaclass,
|
||||
@ -114,14 +116,15 @@ class Service(object):
|
||||
verbose_name = _verbose_name
|
||||
field_specs = _specs
|
||||
validators = _validators
|
||||
region = _region
|
||||
|
||||
self.forms.append(Form)
|
||||
|
||||
@staticmethod
|
||||
def extract_form_data(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):
|
||||
context = self.context.create_child_context()
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n humanize %}
|
||||
{% load i18n horizon humanize bootstrap %}
|
||||
{% block form_action %}
|
||||
{% url 'horizon:app-catalog:catalog:add' app_id environment_id do_redirect drop_wm_form %}
|
||||
{% endblock %}
|
||||
@ -60,6 +60,63 @@
|
||||
</p>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
|
||||
{% block modal-footer %}
|
||||
@ -115,14 +172,14 @@
|
||||
$button = elem.tagName == 'SELECT' && $elem.next().find('a'),
|
||||
bindHandler = function($el) {
|
||||
$el.blur(function() {
|
||||
$descEntry.children('i').remove()
|
||||
$descEntry.removeClass('selected-field')
|
||||
$descEntry.children('i').remove();
|
||||
$descEntry.removeClass('selected-field');
|
||||
}).focus(function() {
|
||||
// remove <i> if previous form without submit
|
||||
$descEntry.children('i').remove()
|
||||
$descEntry.addClass('selected-field')
|
||||
$descEntry.children('i').remove();
|
||||
$descEntry.addClass('selected-field');
|
||||
$descEntry.prepend(
|
||||
"<i class='fa fa-chevron-circle-right'></i>")
|
||||
"<i class='fa fa-chevron-circle-right'></i>");
|
||||
})
|
||||
};
|
||||
bindHandler($elem);
|
||||
@ -131,6 +188,56 @@
|
||||
bindHandler($button);
|
||||
}
|
||||
}).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
|
||||
$('.modal-dialog h3').each(function () {
|
||||
|
@ -24,19 +24,20 @@ class TestFlavorField(helpers.APITestCase):
|
||||
super(TestFlavorField, self).setUp()
|
||||
|
||||
class FlavorFlave(object):
|
||||
def __init__(self, name, vcpus, disk, ram):
|
||||
def __init__(self, id, name, vcpus, disk, ram):
|
||||
self.name = name
|
||||
self.vcpus = vcpus
|
||||
self.disk = disk
|
||||
self.ram = ram
|
||||
self.id = id
|
||||
|
||||
novaclient = self.stub_novaclient()
|
||||
novaclient.flavors = self.mox.CreateMockAnything()
|
||||
# Set up the Flavor list
|
||||
novaclient.flavors.list().MultipleTimes().AndReturn(
|
||||
[FlavorFlave('small', vcpus=1, disk=50, ram=1000),
|
||||
FlavorFlave('medium', vcpus=2, disk=100, ram=2000),
|
||||
FlavorFlave('large', vcpus=3, disk=750, ram=4000)])
|
||||
[FlavorFlave('id1', 'small', vcpus=1, disk=50, ram=1000),
|
||||
FlavorFlave('id2', 'medium', vcpus=2, disk=100, ram=2000),
|
||||
FlavorFlave('id3', 'large', vcpus=3, disk=750, ram=4000)])
|
||||
|
||||
def test_no_filter(self):
|
||||
"""Check that all flavors are returned."""
|
||||
@ -48,9 +49,9 @@ class TestFlavorField(helpers.APITestCase):
|
||||
initial_request = {}
|
||||
f.update(initial_request, self.request)
|
||||
self.assertEqual([
|
||||
('large', 'large'),
|
||||
('medium', 'medium'),
|
||||
('small', 'small')
|
||||
('id3', 'large'),
|
||||
('id2', 'medium'),
|
||||
('id1', 'small')
|
||||
], f.choices)
|
||||
|
||||
def test_multiple_filter(self):
|
||||
@ -60,9 +61,8 @@ class TestFlavorField(helpers.APITestCase):
|
||||
|
||||
# Fake a requirement for 2 CPUs, should return medium and large
|
||||
f = fields.FlavorChoiceField(requirements={'min_vcpus': 2})
|
||||
initial_request = {}
|
||||
f.update(initial_request, self.request)
|
||||
self.assertEqual([('large', 'large'), ('medium', 'medium')], f.choices)
|
||||
f.update({}, self.request)
|
||||
self.assertEqual([('id3', 'large'), ('id2', 'medium')], f.choices)
|
||||
|
||||
def test_single_filter(self):
|
||||
"""Check that one flavor is returned."""
|
||||
@ -73,7 +73,7 @@ class TestFlavorField(helpers.APITestCase):
|
||||
requirements={'min_vcpus': 2, 'min_disk': 200})
|
||||
initial_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):
|
||||
"""Check that no flavors are returned."""
|
||||
|
@ -365,13 +365,31 @@ class TestWizard(testtools.TestCase):
|
||||
for key, val in expected.items():
|
||||
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, '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_services.get_app_field_descriptions.return_value = [
|
||||
'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()
|
||||
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
||||
@ -380,7 +398,7 @@ class TestWizard(testtools.TestCase):
|
||||
self.wizard.request.GET = {}
|
||||
self.wizard.request.POST = {}
|
||||
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.kwargs['do_redirect'] = 'foo_do_redirect'
|
||||
self.wizard.kwargs['drop_wm_form'] = 'foo_drop_wm_form'
|
||||
@ -407,13 +425,17 @@ class TestWizard(testtools.TestCase):
|
||||
'foo_env_id')
|
||||
mock_services.get_app_field_descriptions.assert_called_once_with(
|
||||
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, 'utils')
|
||||
@mock.patch.object(views, 'services')
|
||||
@mock.patch.object(views, 'api')
|
||||
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()
|
||||
app = mock.Mock(fully_qualified_name='foo_app_fqn')
|
||||
app.configure_mock(name='foo_app')
|
||||
@ -425,11 +447,18 @@ class TestWizard(testtools.TestCase):
|
||||
]
|
||||
mock_utils.ensure_python_obj.return_value = None
|
||||
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.POST = {'wizard_id': 'foo_wizard_id'}
|
||||
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'
|
||||
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_services.get_app_field_descriptions.assert_called_once_with(
|
||||
self.wizard.request, 'foo_app_id', 'foo_step_index')
|
||||
mock_nova.flavor_list.assert_called_once_with(self.wizard.request)
|
||||
|
||||
|
||||
class TestIndexView(testtools.TestCase):
|
||||
|
@ -27,7 +27,9 @@ class TestFields(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
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)
|
||||
|
||||
@mock.patch.object(fields, 'LOG')
|
||||
@ -220,7 +222,7 @@ class TestFields(testtools.TestCase):
|
||||
self.assertEqual('DynamicSelect', dynamic_select_cls.__name__)
|
||||
|
||||
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(
|
||||
hasattr(dynamic_select.widget.add_item_link, '__call__'))
|
||||
@ -228,9 +230,9 @@ class TestFields(testtools.TestCase):
|
||||
self.assertIsNone(dynamic_select.initial)
|
||||
|
||||
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(
|
||||
self.request['request'], 'foo_env_id',
|
||||
self.request, 'foo_env_id',
|
||||
['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 = 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('foo_app_id', dynamic_select.initial)
|
||||
|
||||
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(
|
||||
self.request['request'], 'foo_env_id', ['foo_class_fqn']
|
||||
self.request, 'foo_env_id', ['foo_class_fqn']
|
||||
)
|
||||
|
||||
@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 = 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.assertIsNone(dynamic_select.initial)
|
||||
|
||||
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(
|
||||
self.request['request'], 'foo_env_id', [])
|
||||
self.request, 'foo_env_id', [])
|
||||
|
||||
@mock.patch.object(fields, 'reverse')
|
||||
@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 = 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()
|
||||
self.assertEqual('', result)
|
||||
@ -304,7 +306,7 @@ class TestFields(testtools.TestCase):
|
||||
mock_pkg = mock.Mock(fully_qualified_name='foo_class_fqn')
|
||||
mock_pkg.configure_mock(name='foo_class_name')
|
||||
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()
|
||||
expected = '[["foo_class_name", "foo_url"]]'
|
||||
@ -492,11 +494,11 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||
|
||||
self.request = {'request': 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.configure_mock(name='m1.small')
|
||||
self.small_flavor.configure_mock(id='id2', name='m1.small')
|
||||
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)
|
||||
|
||||
@ -507,7 +509,7 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||
self.tiny_flavor, self.small_flavor, self.medium_flavor
|
||||
]
|
||||
expected_choices = [
|
||||
('m1.medium', 'm1.medium'), ('m1.small', 'm1.small')
|
||||
('id3', 'm1.medium'), ('id2', 'm1.small')
|
||||
]
|
||||
valid_requirements = [
|
||||
('vcpus', 2), ('disk', 101), ('ram', 501)
|
||||
@ -529,7 +531,7 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||
self.flavor_choice_field.update(self.request)
|
||||
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)
|
||||
|
||||
@mock.patch.object(fields, 'nova')
|
||||
def test_update_without_requirements(self, mock_nova):
|
||||
@ -539,14 +541,14 @@ class TestFlavorChoiceField(testtools.TestCase):
|
||||
del self.flavor_choice_field.requirements
|
||||
|
||||
expected_choices = [
|
||||
('m1.medium', 'm1.medium'),
|
||||
('m1.small', 'm1.small'),
|
||||
('m1.tiny', 'm1.tiny')
|
||||
('id3', 'm1.medium'),
|
||||
('id2', 'm1.small'),
|
||||
('id1', 'm1.tiny')
|
||||
]
|
||||
|
||||
self.flavor_choice_field.update(self.request)
|
||||
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):
|
||||
|
@ -181,10 +181,10 @@ class TestServiceConfigurationForm(testtools.TestCase):
|
||||
# below, rather than `{'foo': 'bar', 'baz': 'qux'}` because
|
||||
# `cleaned_data[name] = value` in clean() appears to also change the
|
||||
# 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)
|
||||
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(
|
||||
mock.ANY, form=self.form)
|
||||
|
||||
|
6
releasenotes/notes/show-resource-91a1f73cdb5d74ab.yaml
Normal file
6
releasenotes/notes/show-resource-91a1f73cdb5d74ab.yaml
Normal file
@ -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…
x
Reference in New Issue
Block a user