67
72
contributes = ("project_id", "user_id")
70
class VolumeOptionsAction(workflows.Action):
72
('', _("Don't boot from a volume.")),
75
class SetInstanceDetailsAction(workflows.Action):
76
SOURCE_TYPE_CHOICES = (
77
('', _("--- Select source ---")),
78
("image_id", _("Boot from image.")),
79
("instance_snapshot_id", _("Boot from snapshot.")),
73
80
("volume_id", _("Boot from volume.")),
81
("volume_image_id", _("Boot from image "
82
"(creates a new volume).")),
74
83
("volume_snapshot_id", _("Boot from volume snapshot "
75
84
"(creates a new volume).")),
77
# Boot from volume options
78
volume_type = forms.ChoiceField(label=_("Volume Options"),
79
choices=VOLUME_CHOICES,
87
availability_zone = forms.ChoiceField(label=_("Availability Zone"),
90
name = forms.CharField(max_length=80, label=_("Instance Name"))
92
flavor = forms.ChoiceField(label=_("Flavor"),
93
help_text=_("Size of image to launch."))
95
count = forms.IntegerField(label=_("Instance Count"),
98
help_text=_("Number of instances to launch."))
100
source_type = forms.ChoiceField(label=_("Instance Boot Source"),
102
choices=SOURCE_TYPE_CHOICES,
103
help_text=_("Choose Your Boot Source "
106
instance_snapshot_id = forms.ChoiceField(label=_("Instance Snapshot"),
81
109
volume_id = forms.ChoiceField(label=_("Volume"), required=False)
82
111
volume_snapshot_id = forms.ChoiceField(label=_("Volume Snapshot"),
114
image_id = forms.ChoiceField(
115
label=_("Image Name"),
117
widget=fields.SelectWidget(
118
data_attrs=('volume_size',),
119
transform=lambda x: ("%s (%s)" % (x.name,
120
filesizeformat(x.bytes)))))
122
volume_size = forms.CharField(label=_("Device size (GB)"),
124
help_text=_("Volume size in gigabytes "
84
127
device_name = forms.CharField(label=_("Device Name"),
87
130
help_text=_("Volume mount point (e.g. 'vda' "
88
131
"mounts at '/dev/vda')."))
89
133
delete_on_terminate = forms.BooleanField(label=_("Delete on Terminate"),
93
137
"instance terminate"))
96
name = _("Volume Options")
97
permissions = ('openstack.services.volume',)
98
help_text_template = ("project/instances/"
99
"_launch_volumes_help.html")
102
cleaned_data = super(VolumeOptionsAction, self).clean()
103
volume_opt = cleaned_data.get('volume_type', None)
105
if volume_opt and not cleaned_data[volume_opt]:
106
raise forms.ValidationError(_('Please choose a volume, or select '
107
'%s.') % self.VOLUME_CHOICES[0][1])
110
def _get_volume_display_name(self, volume):
111
if hasattr(volume, "volume_id"):
113
visible_label = _("Snapshot")
116
visible_label = _("Volume")
117
return (("%s:%s" % (volume.id, vol_type)),
118
(_("%(name)s - %(size)s GB (%(label)s)") %
119
{'name': volume.display_name or volume.id,
121
'label': visible_label}))
123
def populate_volume_id_choices(self, request, context):
124
volume_options = [("", _("Select Volume"))]
126
volumes = [v for v in cinder.volume_list(self.request)
127
if v.status == api.cinder.VOLUME_STATE_AVAILABLE]
128
volume_options.extend([self._get_volume_display_name(vol)
131
exceptions.handle(self.request,
132
_('Unable to retrieve list of volumes.'))
133
return volume_options
135
def populate_volume_snapshot_id_choices(self, request, context):
136
volume_options = [("", _("Select Volume Snapshot"))]
138
snapshots = cinder.volume_snapshot_list(self.request)
139
snapshots = [s for s in snapshots
140
if s.status == api.cinder.VOLUME_STATE_AVAILABLE]
141
volume_options.extend([self._get_volume_display_name(snap)
142
for snap in snapshots])
144
exceptions.handle(self.request,
145
_('Unable to retrieve list of volume '
148
return volume_options
151
class VolumeOptions(workflows.Step):
152
action_class = VolumeOptionsAction
153
depends_on = ("project_id", "user_id")
154
contributes = ("volume_type",
156
"device_name", # Can be None for an image.
157
"delete_on_terminate")
159
def contribute(self, data, context):
160
context = super(VolumeOptions, self).contribute(data, context)
161
# Translate form input to context for volume values.
162
if "volume_type" in data and data["volume_type"]:
163
context['volume_id'] = data.get(data['volume_type'], None)
165
if not context.get("volume_type", ""):
166
context['volume_type'] = self.action.VOLUME_CHOICES[0][0]
167
context['volume_id'] = None
168
context['device_name'] = None
169
context['delete_on_terminate'] = None
173
class SetInstanceDetailsAction(workflows.Action):
174
SOURCE_TYPE_CHOICES = (
175
("image_id", _("Image")),
176
("instance_snapshot_id", _("Snapshot")),
178
source_type = forms.ChoiceField(label=_("Instance Source"),
179
choices=SOURCE_TYPE_CHOICES)
180
image_id = forms.ChoiceField(label=_("Image"), required=False)
181
instance_snapshot_id = forms.ChoiceField(label=_("Instance Snapshot"),
183
availability_zone = forms.ChoiceField(label=_("Availability Zone"),
185
name = forms.CharField(max_length=80, label=_("Instance Name"))
186
flavor = forms.ChoiceField(label=_("Flavor"),
187
help_text=_("Size of image to launch."))
188
count = forms.IntegerField(label=_("Instance Count"),
191
help_text=_("Number of instances to launch."))
194
140
name = _("Details")
195
141
help_text_template = ("project/instances/"
196
142
"_launch_details_help.html")
144
def __init__(self, request, context, *args, **kwargs):
145
super(SetInstanceDetailsAction, self).__init__(request,
149
choices = [("", _("Select Image"))]
151
images = utils.get_available_images(request,
152
context.get('project_id'))
154
image.bytes = image.size
155
image.volume_size = functions.bytes_to_gigabytes(image.bytes)
156
choices.append((image.id, image))
157
self.fields['image_id'].choices = choices
159
exceptions.handle(self.request,
160
_('Unable to retrieve list of images .'))
199
163
cleaned_data = super(SetInstanceDetailsAction, self).clean()
165
count = cleaned_data.get('count', 1)
166
# Prevent launching more instances than the quota allows
167
usages = quotas.tenant_quota_usages(self.request)
168
available_count = usages['instances']['available']
169
if available_count < count:
170
error_message = ungettext_lazy('The requested instance '
171
'cannot be launched as you only '
172
'have %(avail)i of your quota '
174
'The requested %(req)i instances '
175
'cannot be launched as you only '
176
'have %(avail)i of your quota '
179
params = {'req': count,
180
'avail': available_count}
181
raise forms.ValidationError(error_message % params)
201
183
# Validate our instance source.
202
source = cleaned_data['source_type']
203
# There should always be at least one image_id choice, telling the user
204
# that there are "No Images Available" so we check for 2 here...
205
volume_type = self.data.get('volume_type', None)
206
if volume_type: # Boot from volume
207
if cleaned_data[source]:
184
source_type = self.data.get('source_type', None)
186
if source_type == 'image_id':
187
if not cleaned_data.get('image_id'):
188
raise forms.ValidationError(_("There are no image sources "
189
"available; you must first "
190
"create an image before "
191
"attemtping to launch an "
194
elif source_type == 'instance_snapshot_id':
195
if not cleaned_data['instance_snapshot_id']:
196
raise forms.ValidationError(_("There are no snapshot sources "
197
"available; you must first "
198
"create an snapshot before "
199
"attemtping to launch an "
202
elif source_type == 'volume_id':
203
if not cleaned_data.get('volume_id'):
208
204
raise forms.ValidationError(_("You can't select an instance "
209
205
"source when booting from a "
210
206
"Volume. The Volume is your "
211
207
"source and should contain "
212
208
"the operating system."))
213
else: # Boot from image / image_snapshot
214
if source == 'image_id' and not \
215
filter(lambda x: x[0] != '', self.fields['image_id'].choices):
216
raise forms.ValidationError(_("There are no image sources "
217
"available; you must first "
218
"create an image before "
219
"attemtping to launch an "
221
elif not cleaned_data[source]:
222
raise forms.ValidationError(_("Please select an option for the"
223
" instance source."))
225
# Prevent launching multiple instances with the same volume.
226
# TODO(gabriel): is it safe to launch multiple instances with
227
# a snapshot since it should be cloned to new volumes?
228
count = cleaned_data.get('count', 1)
229
if volume_type and count > 1:
230
msg = _('Launching multiple instances is only supported for '
231
'images and instance snapshots.')
232
raise forms.ValidationError(msg)
209
# Prevent launching multiple instances with the same volume.
210
# TODO(gabriel): is it safe to launch multiple instances with
211
# a snapshot since it should be cloned to new volumes?
213
msg = _('Launching multiple instances is only supported for '
214
'images and instance snapshots.')
215
raise forms.ValidationError(msg)
217
elif source_type == 'volume_image_id':
218
if not cleaned_data['image_id']:
219
self._errors[_('volume_image_id')] = [
220
u"You must select an image."]
221
if not self.data.get('volume_size', None):
222
self._errors['volume_size'] = [_(u"You must set volume size")]
223
if not cleaned_data.get('device_name'):
224
self._errors['device_name'] = [_(u"You must set device name")]
226
elif source_type == 'volume_snapshot_id':
227
if not cleaned_data.get('volume_snapshot_id'):
228
self._errors['volume_snapshot_id'] = [
229
_(u"You must select a snapshot.")]
230
if not cleaned_data.get('device_name'):
231
self._errors['device_name'] = [_(u"You must set device name")]
234
233
return cleaned_data
236
def _init_images_cache(self):
237
if not hasattr(self, '_images_cache'):
238
self._images_cache = {}
240
def populate_image_id_choices(self, request, context):
241
self._init_images_cache()
242
images = get_available_images(request, context.get('project_id'),
244
choices = [(image.id, image.name)
246
if image.properties.get("image_type", '') != "snapshot"]
248
choices.insert(0, ("", _("Select Image")))
250
choices.insert(0, ("", _("No images available.")))
253
def populate_instance_snapshot_id_choices(self, request, context):
254
self._init_images_cache()
255
images = get_available_images(request, context.get('project_id'),
257
choices = [(image.id, image.name)
259
if image.properties.get("image_type", '') == "snapshot"]
261
choices.insert(0, ("", _("Select Instance Snapshot")))
263
choices.insert(0, ("", _("No snapshots available.")))
266
235
def populate_flavor_choices(self, request, context):
236
"""By default, returns the available flavors, sorted by RAM
238
Override these behaviours with a CREATE_INSTANCE_FLAVOR_SORT dict
239
in local_settings.py."""
268
241
flavors = api.nova.flavor_list(request)
242
flavor_sort = getattr(settings, 'CREATE_INSTANCE_FLAVOR_SORT', {})
243
rev = flavor_sort.get('reverse', False)
244
key = flavor_sort.get('key', lambda flavor: flavor.ram)
269
246
flavor_list = [(flavor.id, "%s" % flavor.name)
270
for flavor in flavors]
247
for flavor in sorted(flavors, key=key, reverse=rev)]
273
250
exceptions.handle(request,
274
251
_('Unable to retrieve instance flavors.'))
275
return sorted(flavor_list)
277
254
def populate_availability_zone_choices(self, request, context):
279
256
zones = api.nova.availability_zone_list(request)
282
259
exceptions.handle(request,
283
260
_('Unable to retrieve availability zones.'))
297
274
extra['usages'] = api.nova.tenant_absolute_limits(self.request)
298
275
extra['usages_json'] = json.dumps(extra['usages'])
299
276
flavors = json.dumps([f._info for f in
300
api.nova.flavor_list(self.request)])
277
api.nova.flavor_list(self.request)])
301
278
extra['flavors'] = flavors
303
280
exceptions.handle(self.request,
304
281
_("Unable to retrieve quota information."))
305
282
return super(SetInstanceDetailsAction, self).get_help_text(extra)
284
def _init_images_cache(self):
285
if not hasattr(self, '_images_cache'):
286
self._images_cache = {}
288
def _get_volume_display_name(self, volume):
289
if hasattr(volume, "volume_id"):
291
visible_label = _("Snapshot")
294
visible_label = _("Volume")
295
return (("%s:%s" % (volume.id, vol_type)),
296
(_("%(name)s - %(size)s GB (%(label)s)") %
297
{'name': volume.display_name or volume.id,
299
'label': visible_label}))
301
def populate_instance_snapshot_id_choices(self, request, context):
302
self._init_images_cache()
303
images = utils.get_available_images(request,
304
context.get('project_id'),
306
choices = [(image.id, image.name)
308
if image.properties.get("image_type", '') == "snapshot"]
310
choices.insert(0, ("", _("Select Instance Snapshot")))
312
choices.insert(0, ("", _("No snapshots available.")))
315
def populate_volume_id_choices(self, request, context):
316
volume_options = [("", _("Select Volume"))]
318
volumes = [v for v in cinder.volume_list(self.request)
319
if v.status == api.cinder.VOLUME_STATE_AVAILABLE]
320
volume_options.extend([self._get_volume_display_name(vol)
323
exceptions.handle(self.request,
324
_('Unable to retrieve list of volumes.'))
325
return volume_options
327
def populate_volume_snapshot_id_choices(self, request, context):
328
volume_options = [("", _("Select Volume Snapshot"))]
330
snapshots = cinder.volume_snapshot_list(self.request)
331
snapshots = [s for s in snapshots
332
if s.status == api.cinder.VOLUME_STATE_AVAILABLE]
333
volume_options.extend([self._get_volume_display_name(snap)
334
for snap in snapshots])
336
exceptions.handle(self.request,
337
_('Unable to retrieve list of volume '
339
return volume_options
308
342
class SetInstanceDetails(workflows.Step):
309
343
action_class = SetInstanceDetailsAction
310
contributes = ("source_type", "source_id", "availability_zone",
311
"name", "count", "flavor")
344
depends_on = ("project_id", "user_id")
345
contributes = ("source_type", "source_id",
346
"availability_zone", "name", "count", "flavor",
347
"device_name", # Can be None for an image.
348
"delete_on_terminate")
313
350
def prepare_action_context(self, request, context):
314
351
if 'source_type' in context and 'source_id' in context: