355 lines
14 KiB
Python
355 lines
14 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright (c) 2015-2017 Wind River Systems, Inc.
|
|
#
|
|
# The right to copy, distribute, modify, or otherwise make use
|
|
# of this software may be licensed only pursuant to the terms
|
|
# of an applicable Wind River license agreement.
|
|
#
|
|
|
|
import logging
|
|
|
|
from django.core.urlresolvers import reverse # noqa
|
|
from django.core import validators # noqa
|
|
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
|
|
from oslo_serialization import jsonutils
|
|
|
|
from openstack_dashboard.api import sysinv
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
NOVA_PARAMS_FIELD_MAP = {
|
|
sysinv.LVG_NOVA_PARAM_BACKING:
|
|
sysinv.LVG_NOVA_PARAM_BACKING,
|
|
sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB:
|
|
sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB,
|
|
sysinv.LVG_NOVA_PARAM_DISK_OPS:
|
|
sysinv.LVG_NOVA_PARAM_DISK_OPS,
|
|
}
|
|
|
|
CINDER_PARAMS_FIELD_MAP = {
|
|
sysinv.LVG_CINDER_PARAM_LVM_TYPE:
|
|
sysinv.LVG_CINDER_PARAM_LVM_TYPE,
|
|
}
|
|
|
|
NOVA_PARAMS_KEY_MAP = (
|
|
(sysinv.LVG_NOVA_PARAM_BACKING,
|
|
_("Instance Backing")),
|
|
(sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB,
|
|
_("Instances LV Size [in MiB]")),
|
|
(sysinv.LVG_NOVA_PARAM_DISK_OPS,
|
|
_("Concurrent Disk Operations")),
|
|
)
|
|
|
|
CINDER_PARAMS_KEY_MAP = (
|
|
(sysinv.LVG_CINDER_PARAM_LVM_TYPE,
|
|
_("LVM Provisioning Type")),
|
|
)
|
|
|
|
PARAMS_HELP = {
|
|
sysinv.LVG_NOVA_PARAM_BACKING:
|
|
'Determines the format and location of instance disks. Local CoW image \
|
|
file backed, local RAW LVM logical volume backed, or remote RAW Ceph \
|
|
storage backed',
|
|
sysinv.LVG_NOVA_PARAM_DISK_OPS:
|
|
'Number of parallel disk I/O intensive operations (glance image downloads, \
|
|
image format conversions, etc.).',
|
|
sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB:
|
|
'An integer specifying the size (in MiB) of the instances logical volume. \
|
|
(.e.g. 10 GiB = 10240). Volume is created from nova-local and will be \
|
|
mounted at /etc/nova/instances.',
|
|
sysinv.LVG_CINDER_PARAM_LVM_TYPE:
|
|
'Cinder configuration setting which determines how the volume group is \
|
|
provisioned. Thick provisioning will be used if the value is set to: \
|
|
default. Thin provisioning will be used in the value is set to: thin',
|
|
}
|
|
|
|
NOVA_PARAMS_KEY_NAMES = dict(NOVA_PARAMS_KEY_MAP)
|
|
|
|
NOVA_PARAMS_CHOICES = NOVA_PARAMS_KEY_MAP
|
|
|
|
CINDER_PARAMS_KEY_NAMES = dict(CINDER_PARAMS_KEY_MAP)
|
|
|
|
CINDER_PARAMS_CHOICES = CINDER_PARAMS_KEY_MAP
|
|
|
|
BACKING_CHOICES = (
|
|
(sysinv.LVG_NOVA_BACKING_LVM, _("Local RAW LVM backed")),
|
|
(sysinv.LVG_NOVA_BACKING_IMAGE, _("Local CoW image backed")),
|
|
(sysinv.LVG_NOVA_BACKING_REMOTE, _("Remote RAW Ceph storage backed")),
|
|
)
|
|
|
|
LVM_TYPE_CHOICES = (
|
|
(sysinv.LVG_CINDER_LVM_TYPE_THICK, _("Thick Provisioning (default)")),
|
|
(sysinv.LVG_CINDER_LVM_TYPE_THIN, _("Thin Provisioning (thin)")),
|
|
)
|
|
|
|
|
|
def get_param_key_name(key):
|
|
name = NOVA_PARAMS_KEY_NAMES.get(key, None)
|
|
if not name:
|
|
name = CINDER_PARAMS_KEY_NAMES.get(key, None)
|
|
return name
|
|
|
|
|
|
class ParamMixin(object):
|
|
|
|
def _host_lvg_get(self, lvg_id):
|
|
try:
|
|
return api.sysinv.host_lvg_get(self.request, lvg_id)
|
|
except Exception:
|
|
exceptions.handle(
|
|
self.request,
|
|
_("Unable to retrieve local volume group data. "
|
|
"lvg=%s") % str(lvg_id))
|
|
|
|
def _host_pv_list(self, host_id):
|
|
try:
|
|
return api.sysinv.host_pv_list(self.request, host_id)
|
|
except Exception:
|
|
exceptions.handle(
|
|
self.request,
|
|
_("Unable to retrieve physical volume list. "
|
|
"host=%s") % str(host_id))
|
|
|
|
def _host_pv_disk_get(self, pv):
|
|
try:
|
|
return api.sysinv.host_disk_get(self.request, pv.disk_or_part_uuid)
|
|
except Exception:
|
|
exceptions.handle(
|
|
self.request,
|
|
_("Unable to retrieve disk %(disk)s for PV %(pv)s.") % {
|
|
'disk': pv.disk_or_part_uuid,
|
|
'pv': pv.uuid})
|
|
|
|
def get_lvg_lvm_info(self, lvg_id):
|
|
lvg = self._host_lvg_get(lvg_id)
|
|
caps = lvg.capabilities
|
|
info = {'lvg': lvg}
|
|
if caps.get(sysinv.LVG_NOVA_PARAM_BACKING) != \
|
|
sysinv.LVG_NOVA_BACKING_LVM:
|
|
return info
|
|
info['total'] = 0
|
|
for pv in self._host_pv_list(info['lvg'].ihost_uuid):
|
|
if pv.lvm_vg_name != lvg.lvm_vg_name:
|
|
continue
|
|
if pv.pv_state == sysinv.PV_DEL:
|
|
continue
|
|
disk = self._host_pv_disk_get(pv)
|
|
if not disk:
|
|
exceptions.handle(
|
|
self.request,
|
|
_("PV %s does not have an associated "
|
|
"disk.") % pv.uuid)
|
|
disk_caps = disk.capabilities
|
|
if 'pv_dev' in disk_caps:
|
|
if 'pv_size_mib' in disk_caps:
|
|
info['total'] += disk_caps['pv_size_mib']
|
|
else:
|
|
exceptions.handle(
|
|
self.request,
|
|
_("PV partition %s does not have a "
|
|
"recorded size.") % disk_caps['pv_dev'])
|
|
else:
|
|
info['total'] += disk.size_mib
|
|
info['used'] = caps[sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB]
|
|
info['free'] = info['total'] - info['used']
|
|
|
|
# Limit the allowed size to provide a usable configuration when
|
|
# provisioned. This is the same range that is enforced in sysinv:
|
|
# sysinv/api/controllers/v1/ipv.py:_instances_lv_min_allowed_mib().
|
|
# Here we calculate the values to display to the end user so that they
|
|
# know the acceptable range to use.
|
|
|
|
# The following comment below is from sysinv to provide context:
|
|
#
|
|
# 80GB is the cutoff in the kickstart files for a virtualbox disk vs. a
|
|
# normal disk. Use a similar cutoff here for the volume group size. If
|
|
# the volume group is large enough then bump the min_mib value. The
|
|
# min_mib value is set to provide a reasonable minimum amount of space
|
|
# for /etc/nova/instances
|
|
if info['total'] < (80 * 1024):
|
|
info['allowed_min'] = 2 * 1024
|
|
else:
|
|
info['allowed_min'] = 5 * 1024
|
|
info['allowed_max'] = info['total'] >> 1
|
|
return info
|
|
|
|
|
|
class ParamForm(ParamMixin, forms.SelfHandlingForm):
|
|
type = forms.ChoiceField(
|
|
label=_("Parameters"),
|
|
required=True,
|
|
widget=forms.Select(attrs={
|
|
'class': 'switchable',
|
|
'data-slug': 'type'}))
|
|
|
|
lvg_id = forms.CharField(widget=forms.widgets.HiddenInput)
|
|
failure_url = 'horizon:admin:inventory:localvolumegroupdetail'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ParamForm, self).__init__(*args, **kwargs)
|
|
self._lvg = self.get_lvg_lvm_info(kwargs['initial']['lvg_id'])
|
|
caps = self._lvg['lvg'].capabilities
|
|
|
|
if self._lvg['lvg'].lvm_vg_name == sysinv.LVG_NOVA_LOCAL:
|
|
self.fields[sysinv.LVG_NOVA_PARAM_BACKING] = forms.ChoiceField(
|
|
label=_("Instance Backing"),
|
|
initial=caps.get(sysinv.LVG_NOVA_PARAM_BACKING),
|
|
required=True,
|
|
choices=BACKING_CHOICES,
|
|
help_text=(_("%s") %
|
|
PARAMS_HELP.get(sysinv.LVG_NOVA_PARAM_BACKING,
|
|
None)),
|
|
widget=forms.Select(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'type',
|
|
'data-type-instance_backing': ''}))
|
|
|
|
self.fields[sysinv.LVG_NOVA_PARAM_DISK_OPS] = forms.IntegerField(
|
|
label=_("Concurrent Disk Operations"),
|
|
initial=caps.get(sysinv.LVG_NOVA_PARAM_DISK_OPS),
|
|
required=True,
|
|
help_text=(_("%s") %
|
|
PARAMS_HELP.get(sysinv.LVG_NOVA_PARAM_DISK_OPS,
|
|
None)),
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'type',
|
|
'data-type-concurrent_disk_operations': ''}))
|
|
|
|
if caps.get(sysinv.LVG_NOVA_PARAM_BACKING) == \
|
|
sysinv.LVG_NOVA_BACKING_LVM:
|
|
inst_size_mib = sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB
|
|
self.fields[inst_size_mib] = \
|
|
forms.IntegerField(
|
|
label=_("Instances Logical Volume Size"),
|
|
initial=caps.get(inst_size_mib),
|
|
required=True,
|
|
help_text=(_("%s") %
|
|
PARAMS_HELP.get(inst_size_mib, None)),
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'type',
|
|
'data-type-instances_lv_size_mib': ''}))
|
|
|
|
elif self._lvg['lvg'].lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
|
|
self.fields[sysinv.LVG_CINDER_PARAM_LVM_TYPE] = forms.ChoiceField(
|
|
label=_("LVM Provisioning Type"),
|
|
initial=caps.get(sysinv.LVG_CINDER_PARAM_LVM_TYPE),
|
|
required=True,
|
|
choices=LVM_TYPE_CHOICES,
|
|
help_text=(_("%s") %
|
|
PARAMS_HELP.get(sysinv.LVG_CINDER_PARAM_LVM_TYPE,
|
|
None)),
|
|
widget=forms.Select(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'type',
|
|
'data-type-lvm_type': ''}))
|
|
|
|
def clean(self):
|
|
cleaned_data = super(ParamForm, self).clean()
|
|
key = cleaned_data.get('type', None)
|
|
if self._lvg['lvg'].lvm_vg_name == sysinv.LVG_NOVA_LOCAL:
|
|
field = NOVA_PARAMS_FIELD_MAP.get(key, None)
|
|
elif self._lvg['lvg'].lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
|
|
field = CINDER_PARAMS_FIELD_MAP.get(key, None)
|
|
|
|
if field is not None:
|
|
value = cleaned_data.get(field)
|
|
|
|
cleaned_data['key'] = key
|
|
cleaned_data['value'] = value
|
|
|
|
return cleaned_data
|
|
|
|
def _clean_required_value(self, key, field):
|
|
"""Validate required fields for a specific key type."""
|
|
keytype = self.cleaned_data.get('type', None)
|
|
if keytype == key:
|
|
value = self.cleaned_data.get(field, None)
|
|
if value in validators.EMPTY_VALUES:
|
|
raise forms.ValidationError(_('This field is required.'))
|
|
return value
|
|
|
|
def clean_instances_lv_size_mib(self):
|
|
data = self.cleaned_data[sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB]
|
|
if self.cleaned_data[sysinv.LVG_NOVA_PARAM_BACKING] == sysinv.LVG_NOVA_BACKING_LVM \
|
|
and 'allowed_min' in self._lvg:
|
|
validators.MinValueValidator(self._lvg['allowed_min'])(
|
|
data)
|
|
validators.MaxValueValidator(self._lvg['allowed_max'])(
|
|
data)
|
|
return data
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(EditParam, self).get_context_data(**kwargs)
|
|
context.update(self._lvg)
|
|
return context
|
|
|
|
|
|
class EditParam(ParamForm):
|
|
def __init__(self, *args, **kwargs):
|
|
super(EditParam, self).__init__(*args, **kwargs)
|
|
|
|
# cannot change the type/key during edit
|
|
self.fields['type'].widget.attrs['readonly'] = True
|
|
|
|
key = self.initial['key']
|
|
value = self.initial['value']
|
|
|
|
# ensure checkboxes receive a boolean value as the initial value
|
|
# so that they don't get an override value attribute
|
|
if isinstance(value, basestring) and value.lower() == 'false':
|
|
value = False
|
|
elif isinstance(value, basestring) and value.lower() == 'true':
|
|
value = True
|
|
|
|
# setup initial values for the fields based on the defined key/value
|
|
if self._lvg['lvg'].lvm_vg_name == sysinv.LVG_NOVA_LOCAL:
|
|
field = NOVA_PARAMS_FIELD_MAP.get(key, None)
|
|
param_choices = NOVA_PARAMS_CHOICES
|
|
elif self._lvg['lvg'].lvm_vg_name == sysinv.LVG_CINDER_VOLUMES:
|
|
field = CINDER_PARAMS_FIELD_MAP.get(key, None)
|
|
param_choices = CINDER_PARAMS_CHOICES
|
|
|
|
if field is not None:
|
|
self.initial['type'] = key
|
|
self.initial[field] = value
|
|
|
|
# instances_lv_size_mib only valid for lvm backing
|
|
if self._lvg['lvg'].capabilities.get(sysinv.LVG_NOVA_PARAM_BACKING) \
|
|
== sysinv.LVG_NOVA_BACKING_IMAGE:
|
|
self.fields['type'].choices = \
|
|
[(k, v) for k, v in param_choices
|
|
if k != sysinv.LVG_NOVA_PARAM_INSTANCES_SIZE_MIB]
|
|
else:
|
|
self.fields['type'].choices = [(k, v) for k, v in param_choices]
|
|
|
|
def handle(self, request, data):
|
|
lvg_id = data['lvg_id']
|
|
try:
|
|
if isinstance(data['value'], bool):
|
|
value = str(data['value'])
|
|
data['value'] = value
|
|
metadata = {data['key']: data['value']}
|
|
|
|
patch = []
|
|
patch.append({'path': '/capabilities',
|
|
'value': jsonutils.dumps(metadata),
|
|
'op': 'replace'})
|
|
|
|
api.sysinv.host_lvg_update(request, lvg_id, patch)
|
|
msg = _('Updated parameter "%s".') % data['key']
|
|
messages.success(request, msg)
|
|
return True
|
|
except Exception as e:
|
|
msg = _('Unable to edit parameter "{0}".'
|
|
' Details: {1}').format(data['key'], e)
|
|
redirect = reverse(self.failure_url, args=[lvg_id])
|
|
exceptions.handle(request, msg, redirect=redirect)
|