882 lines
37 KiB
Python
882 lines
37 KiB
Python
# Copyright 2012 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
#
|
|
# Copyright 2012 Nebula, Inc.
|
|
#
|
|
# 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 json
|
|
import logging
|
|
import operator
|
|
|
|
from django.template.defaultfilters import filesizeformat # noqa
|
|
from django.utils.text import normalize_newlines # noqa
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils.translation import ungettext_lazy
|
|
from django.views.decorators.debug import sensitive_variables # noqa
|
|
|
|
from horizon import exceptions
|
|
from horizon import forms
|
|
from horizon.utils import functions
|
|
from horizon.utils import validators
|
|
from horizon import workflows
|
|
|
|
from openstack_dashboard import api
|
|
from openstack_dashboard.api import base
|
|
from openstack_dashboard.api import cinder
|
|
from openstack_dashboard.usage import quotas
|
|
|
|
from openstack_dashboard.dashboards.project.images \
|
|
import utils as image_utils
|
|
from openstack_dashboard.dashboards.project.instances \
|
|
import utils as instance_utils
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class SelectProjectUserAction(workflows.Action):
|
|
project_id = forms.ChoiceField(label=_("Project"))
|
|
user_id = forms.ChoiceField(label=_("User"))
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(SelectProjectUserAction, self).__init__(request, *args, **kwargs)
|
|
# Set our project choices
|
|
projects = [(tenant.id, tenant.name)
|
|
for tenant in request.user.authorized_tenants]
|
|
self.fields['project_id'].choices = projects
|
|
|
|
# Set our user options
|
|
users = [(request.user.id, request.user.username)]
|
|
self.fields['user_id'].choices = users
|
|
|
|
class Meta:
|
|
name = _("Project & User")
|
|
# Unusable permission so this is always hidden. However, we
|
|
# keep this step in the workflow for validation/verification purposes.
|
|
permissions = ("!",)
|
|
|
|
|
|
class SelectProjectUser(workflows.Step):
|
|
action_class = SelectProjectUserAction
|
|
contributes = ("project_id", "user_id")
|
|
|
|
|
|
class SetInstanceDetailsAction(workflows.Action):
|
|
availability_zone = forms.ChoiceField(label=_("Availability Zone"),
|
|
required=False)
|
|
|
|
name = forms.CharField(label=_("Instance Name"),
|
|
max_length=255)
|
|
|
|
flavor = forms.ChoiceField(label=_("Flavor"),
|
|
help_text=_("Size of image to launch."))
|
|
|
|
count = forms.IntegerField(label=_("Instance Count"),
|
|
min_value=1,
|
|
initial=1,
|
|
help_text=_("Number of instances to launch."))
|
|
|
|
source_type = forms.ChoiceField(label=_("Instance Boot Source"),
|
|
help_text=_("Choose Your Boot Source "
|
|
"Type."))
|
|
|
|
instance_snapshot_id = forms.ChoiceField(label=_("Instance Snapshot"),
|
|
required=False)
|
|
|
|
volume_id = forms.ChoiceField(label=_("Volume"), required=False)
|
|
|
|
volume_snapshot_id = forms.ChoiceField(label=_("Volume Snapshot"),
|
|
required=False)
|
|
|
|
image_id = forms.ChoiceField(
|
|
label=_("Image Name"),
|
|
required=False,
|
|
widget=forms.SelectWidget(
|
|
data_attrs=('volume_size',),
|
|
transform=lambda x: ("%s (%s)" % (x.name,
|
|
filesizeformat(x.bytes)))))
|
|
|
|
volume_size = forms.IntegerField(label=_("Device size (GB)"),
|
|
initial=1,
|
|
min_value=1,
|
|
required=False,
|
|
help_text=_("Volume size in gigabytes "
|
|
"(integer value)."))
|
|
|
|
device_name = forms.CharField(label=_("Device Name"),
|
|
required=False,
|
|
initial="vda",
|
|
help_text=_("Volume mount point (e.g. 'vda' "
|
|
"mounts at '/dev/vda')."))
|
|
|
|
delete_on_terminate = forms.BooleanField(label=_("Delete on Terminate"),
|
|
initial=False,
|
|
required=False,
|
|
help_text=_("Delete volume on "
|
|
"instance terminate"))
|
|
|
|
class Meta:
|
|
name = _("Details")
|
|
help_text_template = ("project/instances/"
|
|
"_launch_details_help.html")
|
|
|
|
def __init__(self, request, context, *args, **kwargs):
|
|
self._init_images_cache()
|
|
self.request = request
|
|
self.context = context
|
|
super(SetInstanceDetailsAction, self).__init__(
|
|
request, context, *args, **kwargs)
|
|
source_type_choices = [
|
|
('', _("Select source")),
|
|
("image_id", _("Boot from image")),
|
|
("instance_snapshot_id", _("Boot from snapshot")),
|
|
]
|
|
if base.is_service_enabled(request, 'volume'):
|
|
source_type_choices.append(("volume_id", _("Boot from volume")))
|
|
|
|
try:
|
|
if api.nova.extension_supported("BlockDeviceMappingV2Boot",
|
|
request):
|
|
source_type_choices.append(("volume_image_id",
|
|
_("Boot from image (creates a new volume)")))
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to retrieve extensions '
|
|
'information.'))
|
|
|
|
source_type_choices.append(("volume_snapshot_id",
|
|
_("Boot from volume snapshot (creates a new volume)")))
|
|
self.fields['source_type'].choices = source_type_choices
|
|
|
|
def clean(self):
|
|
cleaned_data = super(SetInstanceDetailsAction, self).clean()
|
|
|
|
count = cleaned_data.get('count', 1)
|
|
# Prevent launching more instances than the quota allows
|
|
usages = quotas.tenant_quota_usages(self.request)
|
|
available_count = usages['instances']['available']
|
|
if available_count < count:
|
|
error_message = ungettext_lazy('The requested instance '
|
|
'cannot be launched as you only '
|
|
'have %(avail)i of your quota '
|
|
'available. ',
|
|
'The requested %(req)i instances '
|
|
'cannot be launched as you only '
|
|
'have %(avail)i of your quota '
|
|
'available.',
|
|
count)
|
|
params = {'req': count,
|
|
'avail': available_count}
|
|
raise forms.ValidationError(error_message % params)
|
|
try:
|
|
flavor_id = cleaned_data.get('flavor')
|
|
# We want to retrieve details for a given flavor,
|
|
# however flavor_list uses a memoized decorator
|
|
# so it is used instead of flavor_get to reduce the number
|
|
# of API calls.
|
|
flavors = instance_utils.flavor_list(self.request)
|
|
flavor = [x for x in flavors if x.id == flavor_id][0]
|
|
except IndexError:
|
|
flavor = None
|
|
|
|
count_error = []
|
|
# Validate cores and ram.
|
|
available_cores = usages['cores']['available']
|
|
if flavor and available_cores < count * flavor.vcpus:
|
|
count_error.append(_("Cores(Available: %(avail)s, "
|
|
"Requested: %(req)s)")
|
|
% {'avail': available_cores,
|
|
'req': count * flavor.vcpus})
|
|
|
|
available_ram = usages['ram']['available']
|
|
if flavor and available_ram < count * flavor.ram:
|
|
count_error.append(_("RAM(Available: %(avail)s, "
|
|
"Requested: %(req)s)")
|
|
% {'avail': available_ram,
|
|
'req': count * flavor.ram})
|
|
|
|
if count_error:
|
|
value_str = ", ".join(count_error)
|
|
msg = (_('The requested instance cannot be launched. '
|
|
'The following requested resource(s) exceed '
|
|
'quota(s): %s.') % value_str)
|
|
if count == 1:
|
|
self._errors['flavor'] = self.error_class([msg])
|
|
else:
|
|
self._errors['count'] = self.error_class([msg])
|
|
|
|
# Validate our instance source.
|
|
source_type = self.data.get('source_type', None)
|
|
|
|
if source_type in ('image_id', 'volume_image_id'):
|
|
if source_type == 'volume_image_id':
|
|
if not self.data.get('volume_size', None):
|
|
msg = _("You must set volume size")
|
|
self._errors['volume_size'] = self.error_class([msg])
|
|
if not cleaned_data.get('device_name'):
|
|
msg = _("You must set device name")
|
|
self._errors['device_name'] = self.error_class([msg])
|
|
if not cleaned_data.get('image_id'):
|
|
msg = _("You must select an image.")
|
|
self._errors['image_id'] = self.error_class([msg])
|
|
else:
|
|
# Prevents trying to launch an image needing more resources.
|
|
try:
|
|
image_id = cleaned_data.get('image_id')
|
|
# We want to retrieve details for a given image,
|
|
# however get_available_images uses a cache of image list,
|
|
# so it is used instead of image_get to reduce the number
|
|
# of API calls.
|
|
images = image_utils.get_available_images(
|
|
self.request,
|
|
self.context.get('project_id'),
|
|
self._images_cache)
|
|
image = [x for x in images if x.id == image_id][0]
|
|
except IndexError:
|
|
image = None
|
|
|
|
if image and flavor:
|
|
props_mapping = (("min_ram", "ram"), ("min_disk", "disk"))
|
|
for iprop, fprop in props_mapping:
|
|
if getattr(image, iprop) > 0 and \
|
|
getattr(image, iprop) > getattr(flavor, fprop):
|
|
msg = (_("The flavor '%(flavor)s' is too small "
|
|
"for requested image.\n"
|
|
"Minimum requirements: "
|
|
"%(min_ram)s MB of RAM and "
|
|
"%(min_disk)s GB of Root Disk.") %
|
|
{'flavor': flavor.name,
|
|
'min_ram': image.min_ram,
|
|
'min_disk': image.min_disk})
|
|
self._errors['image_id'] = self.error_class([msg])
|
|
break # Not necessary to continue the tests.
|
|
|
|
volume_size = cleaned_data.get('volume_size')
|
|
if volume_size and source_type == 'volume_image_id':
|
|
volume_size = int(volume_size)
|
|
img_gigs = functions.bytes_to_gigabytes(image.size)
|
|
smallest_size = max(img_gigs, image.min_disk)
|
|
if volume_size < smallest_size:
|
|
msg = (_("The Volume size is too small for the"
|
|
" '%(image_name)s' image and has to be"
|
|
" greater than or equal to "
|
|
"'%(smallest_size)d' GB.") %
|
|
{'image_name': image.name,
|
|
'smallest_size': smallest_size})
|
|
self._errors['volume_size'] = self.error_class(
|
|
[msg])
|
|
|
|
elif source_type == 'instance_snapshot_id':
|
|
if not cleaned_data['instance_snapshot_id']:
|
|
msg = _("You must select a snapshot.")
|
|
self._errors['instance_snapshot_id'] = self.error_class([msg])
|
|
|
|
elif source_type == 'volume_id':
|
|
if not cleaned_data.get('volume_id'):
|
|
msg = _("You must select a volume.")
|
|
self._errors['volume_id'] = self.error_class([msg])
|
|
# Prevent launching multiple instances with the same volume.
|
|
# TODO(gabriel): is it safe to launch multiple instances with
|
|
# a snapshot since it should be cloned to new volumes?
|
|
if count > 1:
|
|
msg = _('Launching multiple instances is only supported for '
|
|
'images and instance snapshots.')
|
|
raise forms.ValidationError(msg)
|
|
|
|
elif source_type == 'volume_snapshot_id':
|
|
if not cleaned_data.get('volume_snapshot_id'):
|
|
msg = _("You must select a snapshot.")
|
|
self._errors['volume_snapshot_id'] = self.error_class([msg])
|
|
if not cleaned_data.get('device_name'):
|
|
msg = _("You must set device name")
|
|
self._errors['device_name'] = self.error_class([msg])
|
|
|
|
return cleaned_data
|
|
|
|
def populate_flavor_choices(self, request, context):
|
|
flavors = instance_utils.flavor_list(request)
|
|
if flavors:
|
|
return instance_utils.sort_flavor_list(request, flavors)
|
|
return []
|
|
|
|
def populate_availability_zone_choices(self, request, context):
|
|
try:
|
|
zones = api.nova.availability_zone_list(request)
|
|
except Exception:
|
|
zones = []
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve availability zones.'))
|
|
|
|
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
|
|
|
|
def get_help_text(self, extra_context=None):
|
|
extra = extra_context or {}
|
|
try:
|
|
extra['usages'] = api.nova.tenant_absolute_limits(self.request)
|
|
extra['usages_json'] = json.dumps(extra['usages'])
|
|
flavors = json.dumps([f._info for f in
|
|
instance_utils.flavor_list(self.request)])
|
|
extra['flavors'] = flavors
|
|
images = image_utils.get_available_images(self.request,
|
|
self.initial['project_id'],
|
|
self._images_cache)
|
|
if images is not None:
|
|
attrs = [{'id': i.id,
|
|
'min_disk': getattr(i, 'min_disk', 0),
|
|
'min_ram': getattr(i, 'min_ram', 0)}
|
|
for i in images]
|
|
extra['images'] = json.dumps(attrs)
|
|
|
|
except Exception:
|
|
exceptions.handle(self.request,
|
|
_("Unable to retrieve quota information."))
|
|
return super(SetInstanceDetailsAction, self).get_help_text(extra)
|
|
|
|
def _init_images_cache(self):
|
|
if not hasattr(self, '_images_cache'):
|
|
self._images_cache = {}
|
|
|
|
def _get_volume_display_name(self, volume):
|
|
if hasattr(volume, "volume_id"):
|
|
vol_type = "snap"
|
|
visible_label = _("Snapshot")
|
|
else:
|
|
vol_type = "vol"
|
|
visible_label = _("Volume")
|
|
return (("%s:%s" % (volume.id, vol_type)),
|
|
(_("%(name)s - %(size)s GB (%(label)s)") %
|
|
{'name': volume.name,
|
|
'size': volume.size,
|
|
'label': visible_label}))
|
|
|
|
def populate_image_id_choices(self, request, context):
|
|
choices = []
|
|
images = image_utils.get_available_images(request,
|
|
context.get('project_id'),
|
|
self._images_cache)
|
|
for image in images:
|
|
image.bytes = image.size
|
|
image.volume_size = max(
|
|
image.min_disk, functions.bytes_to_gigabytes(image.bytes))
|
|
choices.append((image.id, image))
|
|
if context.get('image_id') == image.id and \
|
|
'volume_size' not in context:
|
|
context['volume_size'] = image.volume_size
|
|
if choices:
|
|
choices.sort(key=lambda c: c[1].name)
|
|
choices.insert(0, ("", _("Select Image")))
|
|
else:
|
|
choices.insert(0, ("", _("No images available")))
|
|
return choices
|
|
|
|
def populate_instance_snapshot_id_choices(self, request, context):
|
|
images = image_utils.get_available_images(request,
|
|
context.get('project_id'),
|
|
self._images_cache)
|
|
choices = [(image.id, image.name)
|
|
for image in images
|
|
if image.properties.get("image_type", '') == "snapshot"]
|
|
if choices:
|
|
choices.sort(key=operator.itemgetter(1))
|
|
choices.insert(0, ("", _("Select Instance Snapshot")))
|
|
else:
|
|
choices.insert(0, ("", _("No snapshots available")))
|
|
return choices
|
|
|
|
def populate_volume_id_choices(self, request, context):
|
|
try:
|
|
volumes = [self._get_volume_display_name(v)
|
|
for v in cinder.volume_list(self.request)
|
|
if v.status == api.cinder.VOLUME_STATE_AVAILABLE
|
|
and v.bootable == 'true']
|
|
except Exception:
|
|
volumes = []
|
|
exceptions.handle(self.request,
|
|
_('Unable to retrieve list of volumes.'))
|
|
if volumes:
|
|
volumes.insert(0, ("", _("Select Volume")))
|
|
else:
|
|
volumes.insert(0, ("", _("No volumes available")))
|
|
return volumes
|
|
|
|
def populate_volume_snapshot_id_choices(self, request, context):
|
|
try:
|
|
snapshots = cinder.volume_snapshot_list(self.request)
|
|
snapshots = [self._get_volume_display_name(s) for s in snapshots
|
|
if s.status == api.cinder.VOLUME_STATE_AVAILABLE]
|
|
except Exception:
|
|
snapshots = []
|
|
exceptions.handle(self.request,
|
|
_('Unable to retrieve list of volume '
|
|
'snapshots.'))
|
|
if snapshots:
|
|
snapshots.insert(0, ("", _("Select Volume Snapshot")))
|
|
else:
|
|
snapshots.insert(0, ("", _("No volume snapshots available")))
|
|
return snapshots
|
|
|
|
|
|
class SetInstanceDetails(workflows.Step):
|
|
action_class = SetInstanceDetailsAction
|
|
depends_on = ("project_id", "user_id")
|
|
contributes = ("source_type", "source_id",
|
|
"availability_zone", "name", "count", "flavor",
|
|
"device_name", # Can be None for an image.
|
|
"delete_on_terminate")
|
|
|
|
def prepare_action_context(self, request, context):
|
|
if 'source_type' in context and 'source_id' in context:
|
|
context[context['source_type']] = context['source_id']
|
|
return context
|
|
|
|
def contribute(self, data, context):
|
|
context = super(SetInstanceDetails, self).contribute(data, context)
|
|
# Allow setting the source dynamically.
|
|
if ("source_type" in context and "source_id" in context
|
|
and context["source_type"] not in context):
|
|
context[context["source_type"]] = context["source_id"]
|
|
|
|
# Translate form input to context for source values.
|
|
if "source_type" in data:
|
|
if data["source_type"] in ["image_id", "volume_image_id"]:
|
|
context["source_id"] = data.get("image_id", None)
|
|
else:
|
|
context["source_id"] = data.get(data["source_type"], None)
|
|
|
|
if "volume_size" in data:
|
|
context["volume_size"] = data["volume_size"]
|
|
|
|
return context
|
|
|
|
|
|
KEYPAIR_IMPORT_URL = "horizon:project:access_and_security:keypairs:import"
|
|
|
|
|
|
class SetAccessControlsAction(workflows.Action):
|
|
keypair = forms.DynamicChoiceField(label=_("Key Pair"),
|
|
required=False,
|
|
help_text=_("Key pair to use for "
|
|
"authentication."),
|
|
add_item_link=KEYPAIR_IMPORT_URL)
|
|
admin_pass = forms.RegexField(
|
|
label=_("Admin Pass"),
|
|
required=False,
|
|
widget=forms.PasswordInput(render_value=False),
|
|
regex=validators.password_validator(),
|
|
error_messages={'invalid': validators.password_validator_msg()})
|
|
confirm_admin_pass = forms.CharField(
|
|
label=_("Confirm Admin Pass"),
|
|
required=False,
|
|
widget=forms.PasswordInput(render_value=False))
|
|
groups = forms.MultipleChoiceField(label=_("Security Groups"),
|
|
initial=["default"],
|
|
widget=forms.CheckboxSelectMultiple(),
|
|
help_text=_("Launch instance in these "
|
|
"security groups."))
|
|
|
|
class Meta:
|
|
name = _("Access & Security")
|
|
help_text = _("Control access to your instance via key pairs, "
|
|
"security groups, and other mechanisms.")
|
|
|
|
def __init__(self, request, *args, **kwargs):
|
|
super(SetAccessControlsAction, self).__init__(request, *args, **kwargs)
|
|
if not api.nova.can_set_server_password():
|
|
del self.fields['admin_pass']
|
|
del self.fields['confirm_admin_pass']
|
|
|
|
def populate_keypair_choices(self, request, context):
|
|
try:
|
|
keypairs = api.nova.keypair_list(request)
|
|
keypair_list = [(kp.name, kp.name) for kp in keypairs]
|
|
except Exception:
|
|
keypair_list = []
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve key pairs.'))
|
|
if keypair_list:
|
|
if len(keypair_list) == 1:
|
|
self.fields['keypair'].initial = keypair_list[0][0]
|
|
keypair_list.insert(0, ("", _("Select a key pair")))
|
|
else:
|
|
keypair_list = (("", _("No key pairs available")),)
|
|
return keypair_list
|
|
|
|
def populate_groups_choices(self, request, context):
|
|
try:
|
|
groups = api.network.security_group_list(request)
|
|
security_group_list = [(sg.name, sg.name) for sg in groups]
|
|
except Exception:
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve list of security groups'))
|
|
security_group_list = []
|
|
return security_group_list
|
|
|
|
def clean(self):
|
|
'''Check to make sure password fields match.'''
|
|
cleaned_data = super(SetAccessControlsAction, self).clean()
|
|
if 'admin_pass' in cleaned_data:
|
|
if cleaned_data['admin_pass'] != cleaned_data.get(
|
|
'confirm_admin_pass', None):
|
|
raise forms.ValidationError(_('Passwords do not match.'))
|
|
return cleaned_data
|
|
|
|
|
|
class SetAccessControls(workflows.Step):
|
|
action_class = SetAccessControlsAction
|
|
depends_on = ("project_id", "user_id")
|
|
contributes = ("keypair_id", "security_group_ids",
|
|
"admin_pass", "confirm_admin_pass")
|
|
|
|
def contribute(self, data, context):
|
|
if data:
|
|
post = self.workflow.request.POST
|
|
context['security_group_ids'] = post.getlist("groups")
|
|
context['keypair_id'] = data.get("keypair", "")
|
|
context['admin_pass'] = data.get("admin_pass", "")
|
|
context['confirm_admin_pass'] = data.get("confirm_admin_pass", "")
|
|
return context
|
|
|
|
|
|
class CustomizeAction(workflows.Action):
|
|
class Meta:
|
|
name = _("Post-Creation")
|
|
help_text_template = ("project/instances/"
|
|
"_launch_customize_help.html")
|
|
|
|
source_choices = [('raw', _('Direct Input')),
|
|
('file', _('File'))]
|
|
|
|
attributes = {'class': 'switchable', 'data-slug': 'scriptsource'}
|
|
script_source = forms.ChoiceField(label=_('Customization Script Source'),
|
|
choices=source_choices,
|
|
widget=forms.Select(attrs=attributes))
|
|
|
|
script_help = _("A script or set of commands to be executed after the "
|
|
"instance has been built (max 16kb).")
|
|
|
|
script_upload = forms.FileField(
|
|
label=_('Script File'),
|
|
help_text=script_help,
|
|
widget=forms.FileInput(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'scriptsource',
|
|
'data-scriptsource-file': _('Script File')}),
|
|
required=False)
|
|
|
|
script_data = forms.CharField(
|
|
label=_('Script Data'),
|
|
help_text=script_help,
|
|
widget=forms.widgets.Textarea(attrs={
|
|
'class': 'switched',
|
|
'data-switch-on': 'scriptsource',
|
|
'data-scriptsource-raw': _('Script Data')}),
|
|
required=False)
|
|
|
|
def __init__(self, *args):
|
|
super(CustomizeAction, self).__init__(*args)
|
|
|
|
def clean(self):
|
|
cleaned = super(CustomizeAction, self).clean()
|
|
|
|
files = self.request.FILES
|
|
script = self.clean_uploaded_files('script', files)
|
|
|
|
if script is not None:
|
|
cleaned['script_data'] = script
|
|
|
|
return cleaned
|
|
|
|
def clean_uploaded_files(self, prefix, files):
|
|
upload_str = prefix + "_upload"
|
|
|
|
has_upload = upload_str in files
|
|
if has_upload:
|
|
upload_file = files[upload_str]
|
|
log_script_name = upload_file.name
|
|
LOG.info('got upload %s' % log_script_name)
|
|
|
|
if upload_file._size > 16 * 1024: # 16kb
|
|
msg = _('File exceeds maximum size (16kb)')
|
|
raise forms.ValidationError(msg)
|
|
else:
|
|
script = upload_file.read()
|
|
if script != "":
|
|
try:
|
|
normalize_newlines(script)
|
|
except Exception as e:
|
|
msg = _('There was a problem parsing the'
|
|
' %(prefix)s: %(error)s')
|
|
msg = msg % {'prefix': prefix, 'error': e}
|
|
raise forms.ValidationError(msg)
|
|
return script
|
|
else:
|
|
return None
|
|
|
|
|
|
class PostCreationStep(workflows.Step):
|
|
action_class = CustomizeAction
|
|
contributes = ("script_data",)
|
|
|
|
|
|
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"))
|
|
if api.neutron.is_port_profiles_supported():
|
|
widget = None
|
|
else:
|
|
widget = forms.HiddenInput()
|
|
profile = forms.ChoiceField(label=_("Policy Profiles"),
|
|
required=False,
|
|
widget=widget,
|
|
help_text=_("Launch instance with "
|
|
"this policy profile"))
|
|
|
|
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]]
|
|
if api.neutron.is_port_profiles_supported():
|
|
self.fields['profile'].choices = (
|
|
self.get_policy_profile_choices(request))
|
|
|
|
class Meta:
|
|
name = _("Networking")
|
|
permissions = ('openstack.services.network',)
|
|
help_text = _("Select networks for your instance.")
|
|
|
|
def populate_network_choices(self, request, context):
|
|
network_list = []
|
|
try:
|
|
tenant_id = self.request.user.tenant_id
|
|
networks = api.neutron.network_list_for_tenant(request, tenant_id)
|
|
for n in networks:
|
|
n.set_id_as_name_if_empty()
|
|
network_list.append((n.id, n.name))
|
|
sorted(network_list, key=lambda obj: obj[1])
|
|
except Exception:
|
|
exceptions.handle(request,
|
|
_('Unable to retrieve networks.'))
|
|
return network_list
|
|
|
|
def get_policy_profile_choices(self, request):
|
|
profile_choices = [('', _("Select a profile"))]
|
|
for profile in self._get_profiles(request, 'policy'):
|
|
profile_choices.append((profile.id, profile.name))
|
|
return profile_choices
|
|
|
|
def _get_profiles(self, request, type_p):
|
|
profiles = []
|
|
try:
|
|
profiles = api.neutron.profile_list(request, type_p)
|
|
except Exception:
|
|
msg = _('Network Profiles could not be retrieved.')
|
|
exceptions.handle(request, msg)
|
|
return profiles
|
|
|
|
|
|
class SetNetwork(workflows.Step):
|
|
action_class = SetNetworkAction
|
|
# Disabling the template drag/drop only in the case port profiles
|
|
# are used till the issue with the drag/drop affecting the
|
|
# profile_id detection is fixed.
|
|
if api.neutron.is_port_profiles_supported():
|
|
contributes = ("network_id", "profile_id",)
|
|
else:
|
|
template_name = "project/instances/_update_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
|
|
|
|
if api.neutron.is_port_profiles_supported():
|
|
context['profile_id'] = data.get('profile', None)
|
|
return context
|
|
|
|
|
|
class SetAdvancedAction(workflows.Action):
|
|
disk_config = forms.ChoiceField(label=_("Disk Partition"), required=False,
|
|
help_text=_("Automatic: The entire disk is a single partition and "
|
|
"automatically resizes. Manual: Results in faster build "
|
|
"times but requires manual partitioning."))
|
|
config_drive = forms.BooleanField(label=_("Configuration Drive"),
|
|
required=False, help_text=_("Configure OpenStack to write metadata to "
|
|
"a special configuration drive that "
|
|
"attaches to the instance when it boots."))
|
|
|
|
def __init__(self, request, context, *args, **kwargs):
|
|
super(SetAdvancedAction, self).__init__(request, context,
|
|
*args, **kwargs)
|
|
try:
|
|
if not api.nova.extension_supported("DiskConfig", request):
|
|
del self.fields['disk_config']
|
|
else:
|
|
# Set our disk_config choices
|
|
config_choices = [("AUTO", _("Automatic")),
|
|
("MANUAL", _("Manual"))]
|
|
self.fields['disk_config'].choices = config_choices
|
|
# Only show the Config Drive option for the Launch Instance
|
|
# workflow (not Resize Instance) and only if the extension
|
|
# is supported.
|
|
if context.get('workflow_slug') != 'launch_instance' or (
|
|
not api.nova.extension_supported("ConfigDrive", request)):
|
|
del self.fields['config_drive']
|
|
except Exception:
|
|
exceptions.handle(request, _('Unable to retrieve extensions '
|
|
'information.'))
|
|
|
|
class Meta:
|
|
name = _("Advanced Options")
|
|
help_text_template = ("project/instances/"
|
|
"_launch_advanced_help.html")
|
|
|
|
|
|
class SetAdvanced(workflows.Step):
|
|
action_class = SetAdvancedAction
|
|
contributes = ("disk_config", "config_drive",)
|
|
|
|
def prepare_action_context(self, request, context):
|
|
context = super(SetAdvanced, self).prepare_action_context(request,
|
|
context)
|
|
# Add the workflow slug to the context so that we can tell which
|
|
# workflow is being used when creating the action. This step is
|
|
# used by both the Launch Instance and Resize Instance workflows.
|
|
context['workflow_slug'] = self.workflow.slug
|
|
return context
|
|
|
|
|
|
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:instances:index"
|
|
multipart = True
|
|
default_steps = (SelectProjectUser,
|
|
SetInstanceDetails,
|
|
SetAccessControls,
|
|
SetNetwork,
|
|
PostCreationStep,
|
|
SetAdvanced)
|
|
|
|
def format_status_message(self, message):
|
|
name = self.context.get('name', 'unknown instance')
|
|
count = self.context.get('count', 1)
|
|
if int(count) > 1:
|
|
return message % {"count": _("%s instances") % count,
|
|
"name": name}
|
|
else:
|
|
return message % {"count": _("instance"), "name": name}
|
|
|
|
@sensitive_variables('context')
|
|
def handle(self, request, context):
|
|
custom_script = context.get('script_data', '')
|
|
|
|
dev_mapping_1 = None
|
|
dev_mapping_2 = None
|
|
|
|
image_id = ''
|
|
|
|
# Determine volume mapping options
|
|
source_type = context.get('source_type', None)
|
|
if source_type in ['image_id', 'instance_snapshot_id']:
|
|
image_id = context['source_id']
|
|
elif source_type in ['volume_id', 'volume_snapshot_id']:
|
|
dev_mapping_1 = {context['device_name']: '%s::%s' %
|
|
(context['source_id'],
|
|
int(bool(context['delete_on_terminate'])))}
|
|
elif source_type == 'volume_image_id':
|
|
dev_mapping_2 = [
|
|
{'device_name': str(context['device_name']),
|
|
'source_type': 'image',
|
|
'destination_type': 'volume',
|
|
'delete_on_termination':
|
|
int(bool(context['delete_on_terminate'])),
|
|
'uuid': context['source_id'],
|
|
'boot_index': '0',
|
|
'volume_size': context['volume_size']
|
|
}
|
|
]
|
|
|
|
netids = context.get('network_id', None)
|
|
if netids:
|
|
nics = [{"net-id": netid, "v4-fixed-ip": ""}
|
|
for netid in netids]
|
|
else:
|
|
nics = None
|
|
|
|
avail_zone = context.get('availability_zone', None)
|
|
|
|
# Create port with Network Name and Port Profile
|
|
# for the use with the plugin supporting port profiles.
|
|
# neutron port-create <Network name> --n1kv:profile <Port Profile ID>
|
|
# for net_id in context['network_id']:
|
|
# HACK for now use first network.
|
|
if api.neutron.is_port_profiles_supported():
|
|
net_id = context['network_id'][0]
|
|
LOG.debug("Horizon->Create Port with %(netid)s %(profile_id)s",
|
|
{'netid': net_id, 'profile_id': context['profile_id']})
|
|
port = None
|
|
try:
|
|
port = api.neutron.port_create(
|
|
request, net_id, policy_profile_id=context['profile_id'])
|
|
except Exception:
|
|
msg = (_('Port not created for profile-id (%s).') %
|
|
context['profile_id'])
|
|
exceptions.handle(request, msg)
|
|
|
|
if port and port.id:
|
|
nics = [{"port-id": port.id}]
|
|
|
|
try:
|
|
api.nova.server_create(request,
|
|
context['name'],
|
|
image_id,
|
|
context['flavor'],
|
|
context['keypair_id'],
|
|
normalize_newlines(custom_script),
|
|
context['security_group_ids'],
|
|
block_device_mapping=dev_mapping_1,
|
|
block_device_mapping_v2=dev_mapping_2,
|
|
nics=nics,
|
|
availability_zone=avail_zone,
|
|
instance_count=int(context['count']),
|
|
admin_pass=context['admin_pass'],
|
|
disk_config=context.get('disk_config'),
|
|
config_drive=context.get('config_drive'))
|
|
return True
|
|
except Exception:
|
|
exceptions.handle(request)
|
|
return False
|