
The datastore version object only contains the active attribute when the user is an admin user. The datastore version object for non-admin users does not have the active attribute. The absence of the active attribute should be considered active. Fixed the active attribute check to account for the case when the attribute is missing. Also modified the test data to create a datastore version without the active attribute to test this scenario. Change-Id: Ie134a69f57b92bc5fab97860d2839bd9c7f496ec Closes-Bug: #1573232
460 lines
18 KiB
Python
460 lines
18 KiB
Python
# Copyright 2013 Rackspace Hosting
|
|
#
|
|
# 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.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from horizon import exceptions
|
|
from horizon import forms
|
|
from horizon.utils import memoized
|
|
from horizon import workflows
|
|
from openstack_dashboard import api as dash_api
|
|
from openstack_dashboard.dashboards.project.instances \
|
|
import utils as instance_utils
|
|
|
|
from trove_dashboard import api
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class SetInstanceDetailsAction(workflows.Action):
|
|
name = forms.CharField(max_length=80, label=_("Instance Name"))
|
|
flavor = forms.ChoiceField(label=_("Flavor"),
|
|
help_text=_("Size of image to launch."))
|
|
volume = forms.IntegerField(label=_("Volume Size"),
|
|
min_value=0,
|
|
initial=1,
|
|
help_text=_("Size of the volume in GB."))
|
|
volume_type = forms.ChoiceField(
|
|
label=_("Volume Type"),
|
|
required=False,
|
|
help_text=_("Applicable only if the volume size is specified."))
|
|
datastore = forms.ChoiceField(label=_("Datastore"),
|
|
help_text=_(
|
|
"Type and version of datastore."))
|
|
|
|
class Meta(object):
|
|
name = _("Details")
|
|
help_text_template = "project/databases/_launch_details_help.html"
|
|
|
|
def clean(self):
|
|
if self.data.get("datastore", None) == "select_datastore_type_version":
|
|
msg = _("You must select a datastore type and version.")
|
|
self._errors["datastore"] = self.error_class([msg])
|
|
return self.cleaned_data
|
|
|
|
@memoized.memoized_method
|
|
def flavors(self, request):
|
|
try:
|
|
return api.trove.flavor_list(request)
|
|
except Exception:
|
|
LOG.exception("Exception while obtaining flavors list")
|
|
redirect = reverse("horizon:project:databases:index")
|
|
exceptions.handle(request,
|
|
_('Unable to obtain flavors.'),
|
|
redirect=redirect)
|
|
|
|
def populate_flavor_choices(self, request, context):
|
|
flavors = self.flavors(request)
|
|
if flavors:
|
|
return instance_utils.sort_flavor_list(request, flavors)
|
|
return []
|
|
|
|
@memoized.memoized_method
|
|
def populate_volume_type_choices(self, request, context):
|
|
try:
|
|
volume_types = dash_api.cinder.volume_type_list(request)
|
|
return ([("no_type", _("No volume type"))] +
|
|
[(type.name, type.name)
|
|
for type in volume_types])
|
|
except Exception:
|
|
LOG.exception("Exception while obtaining volume types list")
|
|
self._volume_types = []
|
|
|
|
@memoized.memoized_method
|
|
def datastores(self, request):
|
|
try:
|
|
return api.trove.datastore_list(request)
|
|
except Exception:
|
|
LOG.exception("Exception while obtaining datastores list")
|
|
self._datastores = []
|
|
|
|
@memoized.memoized_method
|
|
def datastore_versions(self, request, datastore):
|
|
try:
|
|
return api.trove.datastore_version_list(request, datastore)
|
|
except Exception:
|
|
LOG.exception("Exception while obtaining datastore version list")
|
|
self._datastore_versions = []
|
|
|
|
def populate_datastore_choices(self, request, context):
|
|
choices = ()
|
|
set_initial = False
|
|
datastores = self.datastores(request)
|
|
if datastores is not None:
|
|
num_datastores_with_one_version = 0
|
|
for ds in datastores:
|
|
versions = self.datastore_versions(request, ds.name)
|
|
if not set_initial:
|
|
if len(versions) >= 2:
|
|
set_initial = True
|
|
elif len(versions) == 1:
|
|
num_datastores_with_one_version += 1
|
|
if num_datastores_with_one_version > 1:
|
|
set_initial = True
|
|
if versions:
|
|
# only add to choices if datastore has at least one version
|
|
version_choices = ()
|
|
for v in versions:
|
|
if hasattr(v, 'active') and not v.active:
|
|
continue
|
|
version_choices = (version_choices +
|
|
((ds.name + ',' + v.name, v.name),))
|
|
datastore_choices = (ds.name, version_choices)
|
|
choices = choices + (datastore_choices,)
|
|
if set_initial:
|
|
# prepend choice to force user to choose
|
|
initial = (('select_datastore_type_version',
|
|
_('Select datastore type and version')))
|
|
choices = (initial,) + choices
|
|
return choices
|
|
|
|
|
|
TROVE_ADD_USER_PERMS = getattr(settings, 'TROVE_ADD_USER_PERMS', [])
|
|
TROVE_ADD_DATABASE_PERMS = getattr(settings, 'TROVE_ADD_DATABASE_PERMS', [])
|
|
TROVE_ADD_PERMS = TROVE_ADD_USER_PERMS + TROVE_ADD_DATABASE_PERMS
|
|
|
|
|
|
class SetInstanceDetails(workflows.Step):
|
|
action_class = SetInstanceDetailsAction
|
|
contributes = ("name", "volume", "volume_type", "flavor", "datastore")
|
|
|
|
|
|
class SetNetworkAction(workflows.Action):
|
|
network = forms.MultipleChoiceField(label=_("Networks"),
|
|
widget=forms.CheckboxSelectMultiple(),
|
|
error_messages={
|
|
'required': _(
|
|
"At least one network must"
|
|
" be specified.")},
|
|
help_text=_("Launch instance with"
|
|
" these networks"))
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(SetNetworkAction, self).__init__(request, *args, **kwargs)
|
|
network_list = self.fields["network"].choices
|
|
if len(network_list) == 1:
|
|
self.fields['network'].initial = [network_list[0][0]]
|
|
|
|
class Meta(object):
|
|
name = _("Networking")
|
|
permissions = ('openstack.services.network',)
|
|
help_text = _("Select networks for your instance.")
|
|
|
|
def populate_network_choices(self, request, context):
|
|
try:
|
|
tenant_id = self.request.user.tenant_id
|
|
networks = dash_api.neutron.network_list_for_tenant(request,
|
|
tenant_id)
|
|
network_list = [(network.id, network.name_or_id)
|
|
for network in networks]
|
|
except Exception:
|
|
network_list = []
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve networks.'))
|
|
return network_list
|
|
|
|
|
|
class SetNetwork(workflows.Step):
|
|
action_class = SetNetworkAction
|
|
template_name = "project/databases/_launch_networks.html"
|
|
contributes = ("network_id",)
|
|
|
|
def contribute(self, data, context):
|
|
if data:
|
|
networks = self.workflow.request.POST.getlist("network")
|
|
# If no networks are explicitly specified, network list
|
|
# contains an empty string, so remove it.
|
|
networks = [n for n in networks if n != '']
|
|
if networks:
|
|
context['network_id'] = networks
|
|
|
|
return context
|
|
|
|
|
|
class AddDatabasesAction(workflows.Action):
|
|
"""Initialize the database with users/databases. This tab will honor
|
|
the settings which should be a list of permissions required:
|
|
|
|
* TROVE_ADD_USER_PERMS = []
|
|
* TROVE_ADD_DATABASE_PERMS = []
|
|
"""
|
|
databases = forms.CharField(label=_('Initial Databases'),
|
|
required=False,
|
|
help_text=_('Comma separated list of '
|
|
'databases to create'))
|
|
user = forms.CharField(label=_('Initial Admin User'),
|
|
required=False,
|
|
help_text=_("Initial admin user to add"))
|
|
password = forms.CharField(widget=forms.PasswordInput(),
|
|
label=_("Password"),
|
|
required=False)
|
|
host = forms.CharField(label=_("Allowed Host (optional)"),
|
|
required=False,
|
|
help_text=_("Host or IP that the user is allowed "
|
|
"to connect through."))
|
|
|
|
class Meta(object):
|
|
name = _("Initialize Databases")
|
|
permissions = TROVE_ADD_PERMS
|
|
help_text_template = "project/databases/_launch_initialize_help.html"
|
|
|
|
def clean(self):
|
|
cleaned_data = super(AddDatabasesAction, self).clean()
|
|
if cleaned_data.get('user'):
|
|
if not cleaned_data.get('password'):
|
|
msg = _('You must specify a password if you create a user.')
|
|
self._errors["password"] = self.error_class([msg])
|
|
if not cleaned_data.get('databases'):
|
|
msg = _('You must specify at least one database if '
|
|
'you create a user.')
|
|
self._errors["databases"] = self.error_class([msg])
|
|
return cleaned_data
|
|
|
|
|
|
class InitializeDatabase(workflows.Step):
|
|
action_class = AddDatabasesAction
|
|
contributes = ["databases", 'user', 'password', 'host']
|
|
|
|
|
|
class AdvancedAction(workflows.Action):
|
|
initial_state = forms.ChoiceField(
|
|
label=_('Source for Initial State'),
|
|
required=False,
|
|
help_text=_("Choose initial state."),
|
|
choices=[
|
|
('', _('None')),
|
|
('backup', _('Restore from Backup')),
|
|
('master', _('Replicate from Instance'))],
|
|
widget=forms.Select(attrs={
|
|
'class': 'switchable',
|
|
'data-slug': 'initial_state'
|
|
}))
|
|
backup = forms.ChoiceField(
|
|
label=_('Backup Name'),
|
|
required=False,
|
|
help_text=_('Select a backup to restore'),
|
|
widget=forms.Select(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'initial_state',
|
|
'data-initial_state-backup': _('Backup Name')
|
|
}))
|
|
master = forms.ChoiceField(
|
|
label=_('Master Instance Name'),
|
|
required=False,
|
|
help_text=_('Select a master instance'),
|
|
widget=forms.Select(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'initial_state',
|
|
'data-initial_state-master': _('Master Instance Name')
|
|
}))
|
|
replica_count = forms.IntegerField(
|
|
label=_('Replica Count'),
|
|
required=False,
|
|
min_value=1,
|
|
initial=1,
|
|
help_text=_('Specify the number of replicas to be created'),
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'initial_state',
|
|
'data-initial_state-master': _('Replica Count')
|
|
}))
|
|
|
|
class Meta(object):
|
|
name = _("Advanced")
|
|
help_text_template = "project/databases/_launch_advanced_help.html"
|
|
|
|
def populate_backup_choices(self, request, context):
|
|
try:
|
|
backups = api.trove.backup_list(request)
|
|
choices = [(b.id, b.name) for b in backups
|
|
if b.status == 'COMPLETED']
|
|
except Exception:
|
|
choices = []
|
|
|
|
if choices:
|
|
choices.insert(0, ("", _("Select backup")))
|
|
else:
|
|
choices.insert(0, ("", _("No backups available")))
|
|
return choices
|
|
|
|
def populate_master_choices(self, request, context):
|
|
try:
|
|
instances = api.trove.instance_list(request)
|
|
choices = [(i.id, i.name) for i in
|
|
instances if i.status == 'ACTIVE']
|
|
except Exception:
|
|
choices = []
|
|
|
|
if choices:
|
|
choices.insert(0, ("", _("Select instance")))
|
|
else:
|
|
choices.insert(0, ("", _("No instances available")))
|
|
return choices
|
|
|
|
def clean(self):
|
|
cleaned_data = super(AdvancedAction, self).clean()
|
|
|
|
initial_state = cleaned_data.get("initial_state")
|
|
|
|
if initial_state == 'backup':
|
|
cleaned_data['replica_count'] = None
|
|
backup = self.cleaned_data['backup']
|
|
if backup:
|
|
try:
|
|
bkup = api.trove.backup_get(self.request, backup)
|
|
self.cleaned_data['backup'] = bkup.id
|
|
except Exception:
|
|
raise forms.ValidationError(_("Unable to find backup!"))
|
|
else:
|
|
raise forms.ValidationError(_("A backup must be selected!"))
|
|
|
|
cleaned_data['master'] = None
|
|
elif initial_state == 'master':
|
|
master = self.cleaned_data['master']
|
|
if master:
|
|
try:
|
|
api.trove.instance_get(self.request, master)
|
|
except Exception:
|
|
raise forms.ValidationError(
|
|
_("Unable to find master instance!"))
|
|
else:
|
|
raise forms.ValidationError(
|
|
_("A master instance must be selected!"))
|
|
|
|
cleaned_data['backup'] = None
|
|
else:
|
|
cleaned_data['master'] = None
|
|
cleaned_data['backup'] = None
|
|
cleaned_data['replica_count'] = None
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class Advanced(workflows.Step):
|
|
action_class = AdvancedAction
|
|
contributes = ['backup', 'master', 'replica_count']
|
|
|
|
|
|
class LaunchInstance(workflows.Workflow):
|
|
slug = "launch_instance"
|
|
name = _("Launch Instance")
|
|
finalize_button_name = _("Launch")
|
|
success_message = _('Launched %(count)s named "%(name)s".')
|
|
failure_message = _('Unable to launch %(count)s named "%(name)s".')
|
|
success_url = "horizon:project:databases:index"
|
|
default_steps = (SetInstanceDetails,
|
|
SetNetwork,
|
|
InitializeDatabase,
|
|
Advanced)
|
|
|
|
def __init__(self, request=None, context_seed=None, entry_point=None,
|
|
*args, **kwargs):
|
|
super(LaunchInstance, self).__init__(request, context_seed,
|
|
entry_point, *args, **kwargs)
|
|
self.attrs['autocomplete'] = (
|
|
settings.HORIZON_CONFIG.get('password_autocomplete'))
|
|
|
|
def format_status_message(self, message):
|
|
name = self.context.get('name', 'unknown instance')
|
|
return message % {"count": _("instance"), "name": name}
|
|
|
|
def _get_databases(self, context):
|
|
"""Returns the initial databases for this instance."""
|
|
databases = None
|
|
if context.get('databases'):
|
|
dbs = context['databases']
|
|
databases = [{'name': d.strip()} for d in dbs.split(',')]
|
|
return databases
|
|
|
|
def _get_users(self, context):
|
|
users = None
|
|
if context.get('user'):
|
|
user = {
|
|
'name': context['user'],
|
|
'password': context['password'],
|
|
'databases': self._get_databases(context),
|
|
}
|
|
if context['host']:
|
|
user['host'] = context['host']
|
|
users = [user]
|
|
return users
|
|
|
|
def _get_backup(self, context):
|
|
backup = None
|
|
if context.get('backup'):
|
|
backup = {'backupRef': context['backup']}
|
|
return backup
|
|
|
|
def _get_nics(self, context):
|
|
netids = context.get('network_id', None)
|
|
if netids:
|
|
return [{"net-id": netid, "v4-fixed-ip": ""}
|
|
for netid in netids]
|
|
else:
|
|
return None
|
|
|
|
def _get_volume_type(self, context):
|
|
volume_type = None
|
|
if context.get('volume_type') != 'no_type':
|
|
volume_type = context['volume_type']
|
|
return volume_type
|
|
|
|
def handle(self, request, context):
|
|
try:
|
|
datastore = self.context['datastore'].split(',')[0]
|
|
datastore_version = self.context['datastore'].split(',')[1]
|
|
LOG.info("Launching database instance with parameters "
|
|
"{name=%s, volume=%s, volume_type=%s, flavor=%s, "
|
|
"datastore=%s, datastore_version=%s, "
|
|
"dbs=%s, users=%s, "
|
|
"backups=%s, nics=%s, replica_of=%s replica_count=%s}",
|
|
context['name'], context['volume'],
|
|
self._get_volume_type(context), context['flavor'],
|
|
datastore, datastore_version,
|
|
self._get_databases(context), self._get_users(context),
|
|
self._get_backup(context), self._get_nics(context),
|
|
context.get('master'), context['replica_count'])
|
|
api.trove.instance_create(request,
|
|
context['name'],
|
|
context['volume'],
|
|
context['flavor'],
|
|
datastore=datastore,
|
|
datastore_version=datastore_version,
|
|
databases=self._get_databases(context),
|
|
users=self._get_users(context),
|
|
restore_point=self._get_backup(context),
|
|
nics=self._get_nics(context),
|
|
replica_of=context.get('master'),
|
|
replica_count=context['replica_count'],
|
|
volume_type=self._get_volume_type(
|
|
context))
|
|
return True
|
|
except Exception:
|
|
exceptions.handle(request)
|
|
return False
|