trove-dashboard/trove_dashboard/content/databases/workflows/create_instance.py
Akihiro Motoki ddaf10204a Django 2.0 support
Replace django.core.urlresolves with django.urls

(In Django 2.0) The django.core.urlresolvers module is removed
in favor of its new location, django.urls.
It was deprecated in Django 1.10:
https://docs.djangoproject.com/en/2.0/releases/1.10/#id3

The arguments of include() has been changed in Django 1.9
and the older style was dropped in Django 2.0.
https://docs.djangoproject.com/en/2.0/releases/1.9/#passing-a-3-tuple-or-an-app-name-to-include

Add py35dj20 job to test Django 2.0 integration.
Also drops older Django unit tests from tox.ini
as horizon dropped Django <=1.10 support in Rocky.

Depends-On: https://review.openstack.org/#/c/571061/
Change-Id: I122f8ad81807386517149f37aa8d63c76daac533
2018-06-08 13:27:11 +09:00

626 lines
25 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.
from django.conf import settings
from django.urls 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 openstack_dashboard.dashboards.project.instances.workflows \
import create_instance as dash_create_instance
from oslo_log import log as logging
from trove_dashboard import api
from trove_dashboard.utils import common as common_utils
LOG = logging.getLogger(__name__)
def parse_datastore_and_version_text(datastore_and_version):
if datastore_and_version:
datastore, datastore_version = datastore_and_version.split('-', 1)
return datastore.strip(), datastore_version.strip()
return None, None
class SetInstanceDetailsAction(workflows.Action):
availability_zone = forms.ChoiceField(
label=_("Availability Zone"),
required=False)
name = forms.CharField(max_length=80, label=_("Instance Name"))
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."),
widget=forms.Select(attrs={
'class': 'switchable',
'data-slug': 'datastore'
}))
def __init__(self, request, *args, **kwargs):
if args:
self.backup_id = args[0].get('backup', None)
else:
self.backup_id = None
super(SetInstanceDetailsAction, self).__init__(request,
*args,
**kwargs)
# Add this field to the end after the dynamic fields
self.fields['locality'] = forms.ChoiceField(
label=_("Locality"),
choices=[("", "None"),
("affinity", "affinity"),
("anti-affinity", "anti-affinity")],
required=False,
help_text=_("Specify whether future replicated instances will "
"be created on the same hypervisor (affinity) or on "
"different hypervisors (anti-affinity). "
"This value is ignored if the instance to be "
"launched is a replica.")
)
class Meta(object):
name = _("Details")
help_text_template = "project/databases/_launch_details_help.html"
def clean(self):
datastore_and_version = self.data.get("datastore", None)
if not datastore_and_version:
msg = _("You must select a datastore type and version.")
self._errors["datastore"] = self.error_class([msg])
else:
datastore, datastore_version = parse_datastore_and_version_text(
common_utils.unhexlify(datastore_and_version))
field_name = self._build_flavor_field_name(datastore,
datastore_version)
flavor = self.data.get(field_name, None)
if not flavor:
msg = _("You must select a flavor.")
self._errors[field_name] = self.error_class([msg])
if not self.data.get("locality", None):
self.cleaned_data["locality"] = None
return self.cleaned_data
def handle(self, request, context):
datastore_and_version = context["datastore"]
if datastore_and_version:
datastore, datastore_version = parse_datastore_and_version_text(
common_utils.unhexlify(context["datastore"]))
field_name = self._build_flavor_field_name(datastore,
datastore_version)
flavor = self.data[field_name]
if flavor:
context["flavor"] = flavor
return context
return None
@memoized.memoized_method
def availability_zones(self, request):
try:
return dash_api.nova.availability_zone_list(request)
except Exception:
LOG.exception("Exception while obtaining availablity zones")
self._availability_zones = []
def populate_availability_zone_choices(self, request, context):
try:
zones = self.availability_zones(request)
except Exception:
zones = []
redirect = reverse('horizon:project:databases:index')
exceptions.handle(request,
_('Unable to retrieve availability zones.'),
redirect=redirect)
zone_list = [(zone.zoneName, zone.zoneName)
for zone in zones if zone.zoneState['available']]
zone_list.sort()
if not zone_list:
zone_list.insert(0, ("", _("No availability zones found")))
elif len(zone_list) > 1:
zone_list.insert(0, ("", _("Any Availability Zone")))
return zone_list
@memoized.memoized_method
def datastore_flavors(self, request, datastore_name, datastore_version):
try:
return api.trove.datastore_flavors(
request, datastore_name, datastore_version)
except Exception:
LOG.exception("Exception while obtaining flavors list")
redirect = reverse("horizon:project:databases:index")
exceptions.handle(request,
_('Unable to obtain flavors.'),
redirect=redirect)
@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 = []
@memoized.memoized_method
def get_backup(self, request, backup_id):
try:
return api.trove.backup_get(request, backup_id)
except Exception:
LOG.exception("Exception while obtaining backup information")
return None
def populate_datastore_choices(self, request, context):
choices = ()
datastores = self.datastores(request)
if datastores is not None:
if self.backup_id:
backup = self.get_backup(request, self.backup_id)
for ds in datastores:
if self.backup_id:
if ds.name != backup.datastore['type']:
continue
versions = self.datastore_versions(request, ds.name)
if versions:
# only add to choices if datastore has at least one version
version_choices = ()
for v in versions:
# NOTE(zhaochao): please refer to the comment about
# the same change for 'populate_datastore_choices'
# of 'LaunchForm' in
# trove_dashboard/content/database_clusters/forms.py
# for details.
if not v.to_dict().get('active', True):
continue
if self.backup_id:
if v.id != backup.datastore['version_id']:
continue
selection_text = self._build_datastore_display_text(
ds.name, v.name)
widget_text = self._build_widget_field_name(
ds.name, v.name)
version_choices = (version_choices +
((widget_text, selection_text),))
self._add_datastore_flavor_field(request,
ds.name,
v.name)
choices = choices + version_choices
return choices
def _add_datastore_flavor_field(self,
request,
datastore,
datastore_version):
name = self._build_widget_field_name(datastore, datastore_version)
attr_key = 'data-datastore-' + name
field_name = self._build_flavor_field_name(datastore,
datastore_version)
self.fields[field_name] = forms.ChoiceField(
label=_("Flavor"),
help_text=_("Size of image to launch."),
required=False,
widget=forms.Select(attrs={
'class': 'switched',
'data-switch-on': 'datastore',
attr_key: _("Flavor")
}))
valid_flavors = self.datastore_flavors(request,
datastore,
datastore_version)
if valid_flavors:
self.fields[field_name].choices = instance_utils.sort_flavor_list(
request, valid_flavors)
def _build_datastore_display_text(self, datastore, datastore_version):
return datastore + ' - ' + datastore_version
def _build_widget_field_name(self, datastore, datastore_version):
# Since the fieldnames cannot contain an uppercase character
# we generate a hex encoded string representation of the
# datastore and version as the fieldname
return common_utils.hexlify(
self._build_datastore_display_text(datastore, datastore_version))
def _build_flavor_field_name(self, datastore, datastore_version):
return self._build_widget_field_name(datastore,
datastore_version)
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",
"locality", "availability_zone")
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):
config = forms.ChoiceField(
label=_("Configuration Group"),
required=False,
help_text=_('Select a configuration group'))
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')
}))
def __init__(self, request, *args, **kwargs):
if args[0]:
self.backup_id = args[0].get('backup', None)
else:
self.backup_id = None
super(AdvancedAction, self).__init__(request, *args, **kwargs)
if self.backup_id:
self.fields['initial_state'].choices = [('backup',
_('Restore from Backup'))]
class Meta(object):
name = _("Advanced")
help_text_template = "project/databases/_launch_advanced_help.html"
def populate_config_choices(self, request, context):
try:
configs = api.trove.configuration_list(request)
config_name = "%(name)s (%(datastore)s - %(version)s)"
choices = [(c.id,
config_name % {'name': c.name,
'datastore': c.datastore_name,
'version': c.datastore_version_name})
for c in configs]
except Exception:
choices = []
if choices:
choices.insert(0, ("", _("Select configuration")))
else:
choices.insert(0, ("", _("No configurations available")))
return choices
def populate_backup_choices(self, request, context):
try:
choices = []
backups = api.trove.backup_list(request)
for b in backups:
if self.backup_id and b.id != self.backup_id:
continue
if b.status == 'COMPLETED':
choices.append((b.id, b.name))
except Exception:
choices = []
if choices:
choices.insert(0, ("", _("Select backup")))
else:
choices.insert(0, ("", _("No backups available")))
return choices
def _get_instances(self):
instances = []
try:
instances = api.trove.instance_list_all(self.request)
except Exception:
msg = _('Unable to retrieve database instances.')
exceptions.handle(self.request, msg)
return instances
def populate_master_choices(self, request, context):
try:
instances = self._get_instances()
choices = sorted([(i.id, i.name) for i in
instances if i.status == 'ACTIVE'],
key=lambda i: i[1])
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()
config = self.cleaned_data['config']
if config:
try:
# Make sure the user is not "hacking" the form
# and that they have access to this configuration
cfg = api.trove.configuration_get(self.request, config)
self.cleaned_data['config'] = cfg.id
except Exception:
raise forms.ValidationError(_("Unable to find configuration "
"group!"))
else:
self.cleaned_data['config'] = None
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 = ['config', '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,
dash_create_instance.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 _get_locality(self, context):
# If creating a replica from a master then always set to None
if context.get('master'):
return None
locality = None
if context.get('locality'):
locality = context['locality']
return locality
def handle(self, request, context):
try:
datastore, datastore_version = parse_datastore_and_version_text(
common_utils.unhexlify(self.context['datastore']))
avail_zone = context.get('availability_zone', None)
LOG.info("Launching database instance with parameters "
"{name=%s, volume=%s, volume_type=%s, flavor=%s, "
"datastore=%s, datastore_version=%s, "
"dbs=%s, "
"backups=%s, nics=%s, replica_of=%s replica_count=%s, "
"configuration=%s, locality=%s, "
"availability_zone=%s}",
context['name'], context['volume'],
self._get_volume_type(context), context['flavor'],
datastore, datastore_version,
self._get_databases(context),
self._get_backup(context), self._get_nics(context),
context.get('master'), context['replica_count'],
context.get('config'), self._get_locality(context),
avail_zone)
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),
configuration=context.get('config'),
locality=self._get_locality(context),
availability_zone=avail_zone)
return True
except Exception:
exceptions.handle(request)
return False