diff --git a/doc/source/configuration/settings.rst b/doc/source/configuration/settings.rst
index ca029f6fe3..f6fefd692f 100644
--- a/doc/source/configuration/settings.rst
+++ b/doc/source/configuration/settings.rst
@@ -2262,47 +2262,6 @@ specified in this setting is not found in the availability zone list,
the setting will be ignored and the behavior will be same as when ``Any``
is specified.
-LAUNCH_INSTANCE_LEGACY_ENABLED
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. versionadded:: 8.0.0(Liberty)
-
-.. versionchanged:: 9.0.0(Mitaka)
-
- The default value for this setting has been changed to ``False``
-
-.. deprecated:: 19.1.0(Wallaby)
-
- The Python Launch Instance workflow is deprecated.
- Consider switching to the AngujarJS workflow instead.
-
-Default: ``False``
-
-This setting enables the Python Launch Instance workflow.
-
-.. note::
-
- It is possible to run both the AngularJS and Python workflows simultaneously,
- so the other may be need to be toggled with `LAUNCH_INSTANCE_NG_ENABLED`_
-
-LAUNCH_INSTANCE_NG_ENABLED
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. versionadded:: 8.0.0(Liberty)
-
-.. versionchanged:: 9.0.0(Mitaka)
-
- The default value for this setting has been changed to ``True``
-
-Default: ``True``
-
-This setting enables the AngularJS Launch Instance workflow.
-
-.. note::
-
- It is possible to run both the AngularJS and Python workflows simultaneously,
- so the other may be need to be toggled with `LAUNCH_INSTANCE_LEGACY_ENABLED`_
-
OPENSTACK_ENABLE_PASSWORD_RETRIEVE
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/openstack_dashboard/dashboards/project/images/images/tables.py b/openstack_dashboard/dashboards/project/images/images/tables.py
index 21bea9a19c..c2c9022b09 100644
--- a/openstack_dashboard/dashboards/project/images/images/tables.py
+++ b/openstack_dashboard/dashboards/project/images/images/tables.py
@@ -346,11 +346,7 @@ class ImagesTable(tables.DataTable):
status_columns = ["status"]
verbose_name = _("Images")
table_actions = (OwnerFilter, CreateImage, DeleteImage,)
- launch_actions = ()
- if settings.LAUNCH_INSTANCE_LEGACY_ENABLED:
- launch_actions = (LaunchImage,) + launch_actions
- if settings.LAUNCH_INSTANCE_NG_ENABLED:
- launch_actions = (LaunchImageNG,) + launch_actions
+ launch_actions = (LaunchImageNG,)
row_actions = launch_actions + (CreateVolumeFromImage,
EditImage, UpdateMetadata,
DeleteImage,)
diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py
index 674ebdca28..4351e1c51d 100644
--- a/openstack_dashboard/dashboards/project/instances/tables.py
+++ b/openstack_dashboard/dashboards/project/instances/tables.py
@@ -413,14 +413,14 @@ class ToggleShelve(tables.BatchAction):
self.current_past_action = SHELVE
-class LaunchLink(tables.LinkAction):
- name = "launch"
+class LaunchLinkNG(tables.LinkAction):
+ name = "launch-ng"
verbose_name = _("Launch Instance")
- url = "horizon:project:instances:launch"
- classes = ("ajax-modal", "btn-launch")
+ url = "horizon:project:instances:index"
+ ajax = False
+ classes = ("btn-launch", )
icon = "cloud-upload"
policy_rules = (("compute", "os_compute_api:servers:create"),)
- ajax = True
def __init__(self, attrs=None, **kwargs):
kwargs['preempt'] = True
@@ -458,13 +458,6 @@ class LaunchLink(tables.LinkAction):
self.allowed(request, None)
return HttpResponse(self.render(is_table_action=True))
-
-class LaunchLinkNG(LaunchLink):
- name = "launch-ng"
- url = "horizon:project:instances:index"
- ajax = False
- classes = ("btn-launch", )
-
def get_default_attrs(self):
url = urls.reverse(self.url)
ngclick = "modal.openLaunchInstanceWizard(" \
@@ -1295,11 +1288,7 @@ class InstancesTable(tables.DataTable):
status_columns = ["status", "task"]
row_class = UpdateRow
table_actions_menu = (StartInstance, StopInstance, SoftRebootInstance)
- launch_actions = ()
- if settings.LAUNCH_INSTANCE_LEGACY_ENABLED:
- launch_actions = (LaunchLink,) + launch_actions
- if settings.LAUNCH_INSTANCE_NG_ENABLED:
- launch_actions = (LaunchLinkNG,) + launch_actions
+ launch_actions = (LaunchLinkNG,)
table_actions = launch_actions + (DeleteInstance,
InstancesFilterAction)
row_actions = (StartInstance, ConfirmResize, RevertResize,
diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html
deleted file mode 100644
index a1a83b8cd5..0000000000
--- a/openstack_dashboard/dashboards/project/instances/templates/instances/_launch_customize_help.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{% load i18n %}
-
{% blocktrans %}You can customize your instance after it has launched using the options available here.{% endblocktrans %}
-{% blocktrans %}"Customization Script" is analogous to "User Data" in other systems.{% endblocktrans %}
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 69cf67e407..075beea34a 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -19,11 +19,9 @@
import collections
import json
import logging
-import sys
from unittest import mock
from django.conf import settings
-from django.forms import widgets
from django import http
import django.test
from django.test.utils import override_settings
@@ -35,13 +33,11 @@ from horizon import exceptions
from horizon import forms
from horizon.workflows import views
from openstack_dashboard import api
-from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.instances import console
from openstack_dashboard.dashboards.project.instances import tables
from openstack_dashboard.dashboards.project.instances import tabs
from openstack_dashboard.dashboards.project.instances import workflows
from openstack_dashboard.test import helpers
-from openstack_dashboard.usage import quotas
from openstack_dashboard.views import get_url_with_pagination
@@ -249,7 +245,6 @@ class InstanceTableTests(InstanceTestBase, InstanceTableTestMixin):
mock.call(helpers.IsHttpRequest()))
def test_index(self):
-
res = self._get_index()
self.assertTemplateUsed(res, INDEX_TEMPLATE)
@@ -2055,1948 +2050,6 @@ class InstanceTests(InstanceTestBase):
)
-class InstanceLaunchInstanceTests(InstanceTestBase,
- InstanceTableTestMixin):
-
- @helpers.create_mocks({api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_snapshot_list',
- 'volume_list',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.glance: ('image_list_detailed',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_get(self,
- expect_password_fields=True,
- custom_flavor_sort=None,
- only_one_network=False,
- config_drive_default=False):
- image = self.versioned_images.first()
-
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self.mock_network_list.side_effect = [
- self.networks.list()[:1],
- [] if only_one_network else self.networks.list()[1:],
- self.networks.list()[:1],
- self.networks.list()[1:],
- ]
- self.mock_port_list_with_trunk_types.return_value = self.ports.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
- self.mock_tenant_quota_usages.return_value = self.quota_usages.first()
- self._mock_nova_lists()
-
- url = reverse('horizon:project:instances:launch')
- params = urlencode({"source_type": "image_id",
- "source_id": image.id})
- res = self.client.get("%s?%s" % (url, params))
-
- workflow = res.context['workflow']
- self.assertTemplateUsed(res, views.WorkflowView.template_name)
- self.assertEqual(res.context['workflow'].name,
- workflows.LaunchInstance.name)
- step = workflow.get_step("setinstancedetailsaction")
- self.assertEqual(step.action.initial['image_id'], image.id)
- self.assertQuerysetEqual(
- workflow.steps,
- ['',
- '',
- '',
- '',
- '',
- ''])
-
- if custom_flavor_sort == 'id':
- # Reverse sorted by id
- sorted_flavors = (
- ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
- ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
- ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
- ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
- )
- elif custom_flavor_sort == 'name':
- sorted_flavors = (
- ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
- ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
- ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
- ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
- )
- elif custom_flavor_sort == helpers.my_custom_sort:
- sorted_flavors = (
- ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
- ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
- ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
- ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
- )
- else:
- # Default - sorted by RAM
- sorted_flavors = (
- ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'm1.tiny'),
- ('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'm1.massive'),
- ('dddddddd-dddd-dddd-dddd-dddddddddddd', 'm1.secret'),
- ('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', 'm1.metadata'),
- )
-
- select_options = ''.join([
- '' % (f[0], f[1])
- for f in sorted_flavors
- ])
- self.assertContains(res, select_options)
-
- password_field_label = 'Admin Pass'
- if expect_password_fields:
- self.assertContains(res, password_field_label)
- else:
- self.assertNotContains(res, password_field_label)
-
- boot_from_image_field_label = 'Boot from image (creates a new volume)'
- self.assertContains(res, boot_from_image_field_label)
-
- # NOTE(adriant): Django 1.11 changes the checked syntax to use html5
- # "checked" rather than XHTML's "checked='checked'".
- checked_box = (
- ''
- )
- if only_one_network:
- self.assertContains(res, checked_box, html=True)
- else:
- self.assertNotContains(res, checked_box, html=True)
-
- self.assertContains(res, 'Disk Partition')
- self.assertContains(res, 'Configuration Drive')
-
- step = workflow.get_step("setadvancedaction")
- self.assertEqual(step.action.initial['config_drive'],
- config_drive_default)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self._check_glance_image_list_detailed(count=8)
-
- self.mock_network_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- ])
- self.assertEqual(4, self.mock_network_list.call_count)
- self.mock_port_list_with_trunk_types.assert_has_calls(
- [mock.call(helpers.IsHttpRequest(),
- network_id=net.id, tenant_id=self.tenant.id)
- for net in self.networks.list()])
- self.assertEqual(len(self.networks.list()),
- self.mock_port_list_with_trunk_types.call_count)
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- self._check_nova_lists(flavor_count=2)
-
- @helpers.update_settings(
- OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False})
- def test_launch_instance_get_without_password(self):
- self.test_launch_instance_get(expect_password_fields=False)
-
- @helpers.update_settings(
- OPENSTACK_HYPERVISOR_FEATURES={'requires_keypair': True})
- def test_launch_instance_required_key(self):
- flavor = self.flavors.first()
- image = self.images.first()
- image.min_ram = flavor.ram
- image.min_disk = flavor.disk
- res = self._launch_form_instance(image, flavor, keypair=None)
- msg = "This field is required"
- self.assertContains(res, msg)
-
- @django.test.utils.override_settings(
- LAUNCH_INSTANCE_DEFAULTS={'config_drive': True})
- def test_launch_instance_get_with_config_drive_default(self):
- self.test_launch_instance_get(config_drive_default=True)
-
- @django.test.utils.override_settings(
- CREATE_INSTANCE_FLAVOR_SORT={
- 'key': 'id',
- 'reverse': True,
- })
- def test_launch_instance_get_custom_flavor_sort_by_id(self):
- self.test_launch_instance_get(custom_flavor_sort='id')
-
- @django.test.utils.override_settings(
- CREATE_INSTANCE_FLAVOR_SORT={
- 'key': 'name',
- 'reverse': False,
- })
- def test_launch_instance_get_custom_flavor_sort_by_name(self):
- self.test_launch_instance_get(custom_flavor_sort='name')
-
- @django.test.utils.override_settings(
- CREATE_INSTANCE_FLAVOR_SORT={
- 'key': helpers.my_custom_sort,
- 'reverse': False,
- })
- def test_launch_instance_get_custom_flavor_sort_by_callable(self):
- self.test_launch_instance_get(
- custom_flavor_sort=helpers.my_custom_sort)
-
- @django.test.utils.override_settings(
- CREATE_INSTANCE_FLAVOR_SORT={
- 'key': 'no_such_column',
- 'reverse': False,
- })
- def test_launch_instance_get_custom_flavor_sort_by_missing_column(self):
- self.test_launch_instance_get(custom_flavor_sort='no_such_column')
-
- def test_launch_instance_get_with_only_one_network(self):
- self.test_launch_instance_get(only_one_network=True)
-
- @helpers.create_mocks({api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_snapshot_list',
- 'volume_list',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.glance: ('image_list_detailed',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_get_images_snapshots(self,
- block_device_mapping_v2=True,
- only_one_network=False,
- disk_config=True,
- config_drive=True):
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self._mock_glance_image_list_detailed(self.versioned_images.list() +
- self.versioned_snapshots.list())
- self.mock_network_list.side_effect = [
- self.networks.list()[:1],
- [] if only_one_network else self.networks.list()[1:],
- self.networks.list()[:1],
- self.networks.list()[1:],
- ]
- self.mock_port_list_with_trunk_types.return_value = self.ports.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
- self.mock_tenant_quota_usages.return_value = self.limits['absolute']
- self._mock_nova_lists()
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.get(url)
-
- image_sources = (res.context_data['workflow'].steps[0].
- action.fields['image_id'].choices)
-
- snapshot_sources = (res.context_data['workflow'].steps[0].
- action.fields['instance_snapshot_id'].choices)
-
- images = [image.id for image in self.versioned_images.list()]
- snapshots = [s.id for s in self.versioned_snapshots.list()]
-
- image_sources_ids = []
- snapshot_sources_ids = []
- for image in image_sources:
- self.assertTrue(image[0] in images or image[0] == '')
- if image[0] != '':
- image_sources_ids.append(image[0])
-
- for image in images:
- self.assertIn(image, image_sources_ids)
-
- for snapshot in snapshot_sources:
- self.assertTrue(snapshot[0] in snapshots or snapshot[0] == '')
- if snapshot[0] != '':
- snapshot_sources_ids.append(snapshot[0])
-
- for snapshot in snapshots:
- self.assertIn(snapshot, snapshot_sources_ids)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self._check_glance_image_list_detailed(count=8)
- self.mock_network_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- ])
- self.assertEqual(4, self.mock_network_list.call_count)
- self.mock_port_list_with_trunk_types.assert_has_calls(
- [mock.call(helpers.IsHttpRequest(),
- network_id=net.id, tenant_id=self.tenant.id)
- for net in self.networks.list()])
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- self._check_nova_lists(flavor_count=2)
-
- @helpers.create_mocks({api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_snapshot_list',
- 'volume_list',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.glance: ('image_list_detailed',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_get_bootable_volumes(self,
- block_device_mapping_v2=True,
- only_one_network=False,
- disk_config=True,
- config_drive=True):
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self.mock_network_list.side_effect = [
- self.networks.list()[:1],
- [] if only_one_network else self.networks.list()[1:],
- self.networks.list()[:1],
- self.networks.list()[1:],
- ]
- self.mock_port_list_with_trunk_types.return_value = self.ports.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
- self.mock_tenant_quota_usages.return_value = self.quota_usages.first()
- self._mock_nova_lists()
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.get(url)
-
- bootable_volumes = [v.id for v in self.cinder_volumes.list()
- if (v.bootable == 'true' and
- v.status == 'available')]
-
- volume_sources = (res.context_data['workflow'].steps[0].
- action.fields['volume_id'].choices)
-
- volume_sources_ids = []
- for volume in volume_sources:
- self.assertTrue(volume[0].split(":vol")[0] in bootable_volumes or
- volume[0] == '')
- if volume[0] != '':
- volume_sources_ids.append(volume[0].split(":vol")[0])
-
- for volume in bootable_volumes:
- self.assertIn(volume, volume_sources_ids)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self._check_glance_image_list_detailed(count=8)
- self.mock_network_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- mock.call(helpers.IsHttpRequest(),
- tenant_id=self.tenant.id, shared=False),
- mock.call(helpers.IsHttpRequest(), shared=True),
- ])
- self.assertEqual(4, self.mock_network_list.call_count)
- self.mock_port_list_with_trunk_types.assert_has_calls(
- [mock.call(helpers.IsHttpRequest(),
- network_id=net.id, tenant_id=self.tenant.id)
- for net in self.networks.list()])
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- self._check_nova_lists(flavor_count=2)
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'server_create',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post(self):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
- quota_usages = self.quota_usages.first()
- scheduler_hints = {"group": self.server_groups.first().id}
-
- self._mock_nova_glance_neutron_lists()
-
- self.mock_server_group_list.return_value = self.server_groups.list()
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self.mock_server_create.return_value = None
- self.mock_tenant_quota_usages.return_value = quota_usages
- self.mock_flavor_list.return_value = self.flavors.list()
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'image_id': image.id,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'availability_zone': avail_zone.zoneName,
- 'volume_type': '',
- 'network': self.networks.first().id,
- 'count': 1,
- 'server_group': self.server_groups.first().id,
- 'disk_config': 'AUTO',
- 'config_drive': True,
- }
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertNoFormErrors(res)
- self.assertRedirectsNoFollow(res, INDEX_URL)
-
- self._check_nova_glance_neutron_lists(flavor_count=2, image_count=8)
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- image.id,
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=None,
- block_device_mapping_v2=None,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='',
- disk_config='AUTO',
- config_drive=True,
- scheduler_hints=scheduler_hints)
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'server_create',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post_boot_from_volume(self):
- flavor = self.flavors.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- volume_choice = "%s:vol" % volume.id
-
- volume_source_id = volume.id.split(':')[0]
- block_device_mapping = None
- block_device_mapping_2 = [
- {'device_name': 'vda',
- 'source_type': 'volume',
- 'destination_type': 'volume',
- 'delete_on_termination': False,
- 'uuid': volume_source_id,
- 'boot_index': '0',
- 'volume_size': 1
- }
- ]
-
- nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
- quota_usages = self.quota_usages.first()
-
- self._mock_nova_glance_neutron_lists()
-
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_server_create.return_value = None
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'volume_id',
- 'source_id': volume_choice,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'availability_zone': avail_zone.zoneName,
- 'volume_size': '1',
- 'volume_id': volume_choice,
- 'device_name': device_name,
- 'network': self.networks.first().id,
- 'count': 1,
- 'disk_config': 'AUTO',
- 'config_drive': True}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertNoFormErrors(res)
- self.assertRedirectsNoFollow(res, INDEX_URL)
-
- self._check_nova_glance_neutron_lists(flavor_count=2, image_count=6)
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
-
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- '',
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=block_device_mapping,
- block_device_mapping_v2=block_device_mapping_2,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='',
- disk_config='AUTO',
- config_drive=True,
- scheduler_hints={})
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('server_create',
- 'is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post_no_images_available_boot_from_volume(self):
- flavor = self.flavors.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- volume_choice = "%s:vol" % volume.id
- block_device_mapping = [
- {'device_name': device_name,
- 'source_type': 'volume',
- 'destination_type': 'volume',
- 'delete_on_termination': False,
- 'uuid': volume.id,
- 'boot_index': '0',
- 'volume_size': None,
- }
- ]
- nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
- quota_usages = self.quota_usages.first()
-
- self._mock_nova_glance_neutron_lists()
-
- self.mock_flavor_list.return_value = self.flavors.list()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_tenant_quota_usages.return_value = quota_usages
- self.mock_server_create.return_value = None
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'volume_id',
- # 'image_id': '',
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'availability_zone': avail_zone.zoneName,
- 'network': self.networks.first().id,
- 'volume_type': 'volume_id',
- 'volume_id': volume_choice,
- 'device_name': device_name,
- 'count': 1,
- 'disk_config': 'MANUAL',
- 'config_drive': True}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertNoFormErrors(res)
- self.assertRedirectsNoFollow(res, INDEX_URL)
-
- self._check_nova_glance_neutron_lists(flavor_count=2,
- image_count=6)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
-
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- '',
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=None,
- block_device_mapping_v2=block_device_mapping,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='',
- disk_config='MANUAL',
- config_drive=True,
- scheduler_hints={})
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list'),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post_no_images_available(self):
- flavor = self.flavors.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- quota_usages = self.quota_usages.first()
-
- self.mock_tenant_quota_usages.return_value = self.quota_usages.first()
- self._mock_glance_image_list_detailed([])
- self._mock_neutron_network_and_port_list()
- self._mock_nova_lists()
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'image_id': '',
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'availability_zone': avail_zone.zoneName,
- 'volume_type': '',
- 'count': 1}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertFormErrors(res, 1, "You must select an image.")
- self.assertTemplateUsed(res, views.WorkflowView.template_name)
-
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
- self._check_glance_image_list_detailed(count=8)
- self._check_neutron_network_and_port_list()
- self._check_nova_lists(flavor_count=3)
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- @helpers.create_mocks({
- api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'server_create',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post_boot_from_snapshot(self):
- flavor = self.flavors.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- snapshot = self.cinder_volume_snapshots.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- snapshot_choice = "%s:snap" % snapshot.id
-
- snapshot_source_id = snapshot.id.split(':')[0]
- block_device_mapping = None
- block_device_mapping_2 = [
- {'device_name': 'vda',
- 'source_type': 'snapshot',
- 'destination_type': 'volume',
- 'delete_on_termination': 0,
- 'uuid': snapshot_source_id,
- 'boot_index': '0',
- 'volume_size': 1
- }
- ]
-
- nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
- quota_usages = self.quota_usages.first()
-
- self._mock_nova_glance_neutron_lists()
-
- volumes = [v for v in self.cinder_volumes.list()
- if (getattr(v, 'bootable', 'false') == 'true')]
- snapshots = [v for v in self.cinder_volume_snapshots.list()
- if (v.status == AVAILABLE)]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = snapshots
- self.mock_server_create.return_value = None
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'volume_snapshot_id',
- 'source_id': snapshot_choice,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'availability_zone': avail_zone.zoneName,
- 'volume_size': '1',
- 'volume_snapshot_id': snapshot_choice,
- 'device_name': device_name,
- 'network': self.networks.first().id,
- 'count': 1,
- 'disk_config': 'AUTO',
- 'config_drive': True}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertNoFormErrors(res)
- self.assertRedirectsNoFollow(res, INDEX_URL)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- '',
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=block_device_mapping,
- block_device_mapping_v2=block_device_mapping_2,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='',
- disk_config='AUTO',
- config_drive=True,
- scheduler_hints={})
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
-
- @helpers.create_mocks({
- api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'server_create'),
- cinder: ('volume_list',
- 'volume_snapshot_list'),
- quotas: ('tenant_quota_usages',)})
- def test_launch_instance_post_boot_from_snapshot_error(self):
- flavor = self.flavors.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- avail_zone = self.availability_zones.first()
- quota_usages = self.quota_usages.first()
-
- self.mock_image_list_detailed.return_value = [[], False, False]
- self.mock_tenant_quota_usages.return_value = quota_usages
- self._mock_neutron_network_and_port_list()
-
- bad_snapshot_id = 'a-bogus-id'
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'instance_snapshot_id',
- 'instance_snapshot_id': bad_snapshot_id,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'availability_zone': avail_zone.zoneName,
- 'network': self.networks.first().id,
- 'volume_id': '',
- 'volume_snapshot_id': '',
- 'image_id': '',
- 'device_name': 'vda',
- 'count': 1,
- 'customization_script': ''}
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertFormErrors(res, 3, "You must select a snapshot.")
-
- self.assertEqual(4, self.mock_image_list_detailed.call_count)
- self.mock_image_list_detailed.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- filters={'is_public': True,
- 'status': 'active'}),
- mock.call(helpers.IsHttpRequest(),
- filters={'property-owner_id': self.tenant.id,
- 'status': 'active'}),
- mock.call(helpers.IsHttpRequest(),
- filters={'status': 'active', 'visibility': 'community'}),
- mock.call(helpers.IsHttpRequest(),
- filters={'status': 'active', 'visibility': 'shared'}),
- ])
-
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- self._check_neutron_network_and_port_list()
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_flavorlist_error(self):
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self._mock_neutron_network_and_port_list()
- self.mock_tenant_quota_usages.return_value = self.quota_usages.first()
- self.mock_flavor_list.side_effect = self.exceptions.nova
- self.mock_keypair_list.return_value = self.keypairs.list()
- self.mock_security_group_list.return_value = \
- self.security_groups.list()
- self.mock_availability_zone_list.return_value = \
- self.availability_zones.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.get(url)
-
- self.assertTemplateUsed(res, views.WorkflowView.template_name)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self._check_glance_image_list_detailed(count=8)
- self._check_neutron_network_and_port_list()
-
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
- self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_security_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_availability_zone_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_create',
- 'port_delete',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'server_create',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_form_keystone_exception(self):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
- quota_usages = self.quota_usages.first()
-
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE)]
- self.mock_volume_snapshot_list.return_value = volumes
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_keypair_list.return_value = self.keypairs.list()
- self.mock_security_group_list.return_value = \
- self.security_groups.list()
- self.mock_availability_zone_list.return_value = \
- self.availability_zones.list()
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self._mock_neutron_network_and_port_list()
- self.mock_server_create.side_effect = self.exceptions.keystone
- self.mock_tenant_quota_usages.return_value = quota_usages
- self.mock_flavor_list.return_value = self.flavors.list()
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'source_id': image.id,
- 'volume_size': '1',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_type': '',
- 'network': self.networks.first().id,
- 'count': 1,
- 'admin_pass': 'password',
- 'confirm_admin_pass': 'password',
- 'disk_config': 'AUTO',
- 'config_drive': False}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertRedirectsNoFollow(res, INDEX_URL)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
- self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_security_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_availability_zone_list.assert_called_once_with(
- helpers.IsHttpRequest())
-
- self._check_glance_image_list_detailed(count=8)
- self._check_neutron_network_and_port_list()
-
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- image.id,
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=None,
- block_device_mapping_v2=None,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='password',
- disk_config='AUTO',
- config_drive=False,
- scheduler_hints={})
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list'),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_form_instance_count_error(self):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- volume_choice = "%s:vol" % volume.id
- quota_usages = self.quota_usages.first()
-
- self._mock_nova_glance_neutron_lists()
-
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_type': 'volume_id',
- 'volume_id': volume_choice,
- 'device_name': device_name,
- 'count': 0}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self.assertContains(res, "greater than or equal to 1")
-
- self._check_nova_glance_neutron_lists(flavor_count=3,
- image_count=10)
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 3,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def _test_launch_form_count_error(self, resource, avail):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- volume_choice = "%s:vol" % volume.id
- quota_usages = self.quota_usages.first()
- if resource == 'both':
- quota_usages['cores']['available'] = avail
- quota_usages['ram']['available'] = 512
- else:
- quota_usages[resource]['available'] = avail
-
- self._mock_nova_glance_neutron_lists()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_type': 'volume_id',
- 'volume_id': volume_choice,
- 'device_name': device_name,
- 'count': 2}
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- if resource == 'ram':
- msg = ("The following requested resource(s) exceed quota(s): "
- "RAM(Available: %s" % avail)
- if resource == 'cores':
- msg = ("The following requested resource(s) exceed quota(s): "
- "Cores(Available: %s" % avail)
- if resource == 'both':
- msg = ("The following requested resource(s) exceed quota(s): "
- "Cores(Available: %(avail)s, Requested: 2), RAM(Available: "
- "512, Requested: 1024)" % {'avail': avail})
- self.assertContains(res, msg)
-
- self._check_nova_glance_neutron_lists(flavor_count=3,
- image_count=10)
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 3,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- def test_launch_form_cores_count_error_glance_v2(self):
- self._test_launch_form_count_error('cores', 1)
-
- def test_launch_form_ram_count_error(self):
- self._test_launch_form_count_error('ram', 512)
-
- def test_launch_form_ram_cores_count_error(self):
- self._test_launch_form_count_error('both', 1)
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def _launch_form_instance(self, image, flavor, keypair=None):
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- volume_choice = "%s:vol" % volume.id
- quota_usages = self.quota_usages.first()
-
- self._mock_nova_glance_neutron_lists()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_type': 'volume_id',
- 'volume_id': volume_choice,
- 'device_name': device_name,
- 'count': 1}
- if keypair:
- form_data['keypair'] = keypair.name
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
-
- self._check_nova_glance_neutron_lists(flavor_count=3,
- image_count=10)
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 3,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- return res
-
- def test_launch_form_instance_requirement_error_disk(self):
- flavor = self.flavors.get(id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
- image = self.versioned_images.first()
- image.min_ram = flavor.ram
- image.min_disk = flavor.disk + 1
- keypair = self.keypairs.first()
- res = self._launch_form_instance(image, flavor, keypair)
- msg = (f"The flavor '{flavor.name}' is too small for requested "
- f"image. Minimum requirements: {image.min_ram} MB of RAM and "
- f"{image.min_disk} GB of Root Disk.")
- self.assertContains(res, msg, html=True)
-
- def test_launch_form_instance_requirement_error_ram(self):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- image.min_ram = flavor.ram + 1
- image.min_disk = flavor.disk
- keypair = self.keypairs.first()
- res = self._launch_form_instance(image, flavor, keypair)
- msg = (f"The flavor '{flavor.name}' is too small for requested "
- f"image. Minimum requirements: {image.min_ram} MB of RAM and "
- f"{image.min_disk} GB of Root Disk.")
- self.assertContains(res, msg, html=True)
-
- def test_launch_form_instance_zero_value_flavor_with_min_req(self):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- image.min_ram = flavor.ram
- image.min_disk = flavor.disk + 1
- keypair = self.keypairs.first()
- res = self._launch_form_instance(image, flavor, keypair)
- msg = (f"The flavor &39;{flavor.name}&39; is too small for requested "
- f"image. Minimum requirements: {image.min_ram} MB of RAM and "
- f"{image.min_disk} GB of Root Disk.")
- self.assertNotContains(res, msg, html=True)
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def _test_launch_form_instance_show_device_name(self, device_name,
- widget_class,
- widget_attrs):
- flavor = self.flavors.first()
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- volume = self.cinder_volumes.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- volume_choice = "%s:vol" % volume.id
- quota_usages = self.quota_usages.first()
-
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_keypair_list.return_value = self.keypairs.list()
- self.mock_security_group_list.return_value = \
- self.security_groups.list()
- self.mock_availability_zone_list.return_value = \
- self.availability_zones.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
- self.mock_image_list_detailed.side_effect = [
- [self.versioned_images.list(), False, False],
- [[], False, False],
- ]
- self.mock_network_list.side_effect = [
- self.networks.list()[:1],
- self.networks.list()[1:],
- self.networks.list()[:1],
- self.networks.list()[1:],
- ]
- self.mock_port_list_with_trunk_types.return_value = self.ports.list()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {'flavor': flavor.id,
- 'source_type': 'volume_image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'customization_script': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_type': 'volume_id',
- 'volume_id': volume_choice,
- 'volume_size': max(
- image.min_disk, image.size // 1024 ** 3),
- 'device_name': device_name,
- 'count': 1}
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.post(url, form_data)
- self.assertNoFormErrors(res)
- widget_content = widget_class().render(**widget_attrs)
- # In django 1.4, the widget's html attributes are not always rendered
- # in the same order and checking the fully rendered widget fails.
- for widget_part in widget_content.split():
- self.assertContains(res, widget_part)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 3,
- mock.call(helpers.IsHttpRequest()))
- self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_security_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_availability_zone_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.assertEqual(10, self.mock_image_list_detailed.call_count)
- self.mock_image_list_detailed.assert_has_calls(
- [
- mock.call(helpers.IsHttpRequest(),
- filters={'is_public': True,
- 'status': 'active'}),
- mock.call(helpers.IsHttpRequest(),
- filters={'property-owner_id': self.tenant.id,
- 'status': 'active'})
- ] +
- [
- mock.call(helpers.IsHttpRequest(),
- filters={'status': 'active',
- 'visibility': 'community'}),
- mock.call(helpers.IsHttpRequest(),
- filters={'status': 'active',
- 'visibility': 'shared'})
- ] * 3
- )
- self.assertEqual(4, self.mock_network_list.call_count)
- self.mock_network_list.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- tenant_id=self.tenant.id,
- shared=False),
- mock.call(
- helpers.IsHttpRequest(),
- shared=True),
- mock.call(
- helpers.IsHttpRequest(),
- tenant_id=self.tenant.id,
- shared=False),
- mock.call(
- helpers.IsHttpRequest(),
- shared=True),
- ])
- self.assertEqual(len(self.networks.list()),
- self.mock_port_list_with_trunk_types.call_count)
- self.mock_port_list_with_trunk_types.assert_has_calls(
- [mock.call(helpers.IsHttpRequest(),
- network_id=net.id,
- tenant_id=self.tenant.id)
- for net in self.networks.list()])
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 3,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- @helpers.update_settings(
- OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},)
- def test_launch_form_instance_device_name_showed(self):
- self._test_launch_form_instance_show_device_name(
- 'vda', widgets.TextInput, {
- 'name': 'device_name', 'value': 'vda',
- 'attrs': {'id': 'id_device_name'}}
- )
-
- @helpers.update_settings(
- OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': False})
- def test_launch_form_instance_device_name_hidden(self):
- self._test_launch_form_instance_show_device_name(
- '', widgets.HiddenInput, {
- 'name': 'device_name', 'value': '',
- 'attrs': {'id': 'id_device_name'}}
- )
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def _test_launch_form_instance_volume_size(self, image, volume_size, msg,
- avail_volumes=None):
- flavor = self.flavors.get(name='m1.massive')
- keypair = self.keypairs.first()
- server = self.servers.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- device_name = 'vda'
- quota_usages = self.quota_usages.first()
- quota_usages['cores']['available'] = 2000
- if avail_volumes is not None:
- quota_usages['volumes']['available'] = avail_volumes
-
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_keypair_list.return_value = self.keypairs.list()
- self.mock_security_group_list.return_value = \
- self.security_groups.list()
- self.mock_availability_zone_list.return_value = \
- self.availability_zones.list()
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self._mock_neutron_network_and_port_list()
- self.mock_server_group_list.return_value = self.server_groups.list()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_tenant_quota_usages.return_value = quota_usages
-
- form_data = {
- 'flavor': flavor.id,
- 'source_type': 'volume_image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_size': volume_size,
- 'device_name': device_name,
- 'count': 1
- }
- url = reverse('horizon:project:instances:launch')
-
- res = self.client.post(url, form_data)
- self.assertContains(res, msg, html=True)
-
- self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_security_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_availability_zone_list.assert_called_once_with(
- helpers.IsHttpRequest())
- if avail_volumes is None:
- image_list_count = 10
- else:
- image_list_count = 8
- self._check_glance_image_list_detailed(count=image_list_count)
- self._check_neutron_network_and_port_list()
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
- if avail_volumes is None:
- flavor_list_count = 3
- else:
- flavor_list_count = 2
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, flavor_list_count,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_has_calls([
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', )),
- mock.call(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes')),
- ])
- self.assertEqual(2, self.mock_tenant_quota_usages.call_count)
-
- def test_launch_form_instance_volume_size_error(self):
- image = self.versioned_images.get(name='protected_images')
- volume_size = image.min_disk // 2
- msg = ("The Volume size is too small for the '%s' image "
- "and has to be greater than or equal to '%s' GB." %
- (image.name, image.min_disk))
- self._test_launch_form_instance_volume_size(image, volume_size, msg)
-
- def test_launch_form_instance_non_int_volume_size(self):
- image = self.versioned_images.get(name='protected_images')
- msg = "Enter a whole number."
- self._test_launch_form_instance_volume_size(image, 1.5, msg)
-
- def test_launch_form_instance_volume_exceed_quota(self):
- image = self.versioned_images.get(name='protected_images')
- msg = ("The requested instance cannot be launched. "
- "Requested volume exceeds quota: Available: 0, Requested: 1.")
- self._test_launch_form_instance_volume_size(image, image.min_disk,
- msg, 0)
-
- @helpers.create_mocks({
- api.nova: ('flavor_list',
- 'server_list_paged',
- 'tenant_absolute_limits',
- 'is_feature_available',),
- api.glance: ('image_list_detailed',),
- api.neutron: ('floating_ip_simple_associate_supported',
- 'floating_ip_supported',),
- api.network: ('servers_update_addresses',),
- api.cinder: ('volume_list',),
- })
- def test_launch_button_attributes(self):
- servers = self.servers.list()
- limits = self.limits['absolute']
-
- self.mock_is_feature_available.return_value = True
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_image_list_detailed.return_value = (self.images.list(),
- False, False)
- self.mock_server_list_paged.return_value = [servers, False, False]
- self.mock_servers_update_addresses.return_value = None
- self.mock_tenant_absolute_limits.return_value = limits
- self.mock_floating_ip_supported.return_value = True
- self.mock_floating_ip_simple_associate_supported.return_value = True
-
- tables.LaunchLink()
- res = self.client.get(INDEX_URL)
-
- launch_action = self.getAndAssertTableAction(res, 'instances',
- 'launch-ng')
-
- self.assertEqual(set(['btn-launch']),
- set(launch_action.classes))
- self.assertEqual('Launch Instance', launch_action.verbose_name)
- self.assertEqual((('compute', 'os_compute_api:servers:create'),),
- launch_action.policy_rules)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_is_feature_available, 10,
- mock.call(helpers.IsHttpRequest(), 'locked_attribute'))
- self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
- self._assert_mock_image_list_detailed_calls()
-
- search_opts = {'marker': None, 'paginate': True}
- self.mock_server_list_paged.assert_called_once_with(
- helpers.IsHttpRequest(),
- sort_dir='desc',
- search_opts=search_opts)
- self.mock_servers_update_addresses.assert_called_once_with(
- helpers.IsHttpRequest(), servers)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_tenant_absolute_limits, 3,
- mock.call(helpers.IsHttpRequest(), reserved=True))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_floating_ip_supported, 10,
- mock.call(helpers.IsHttpRequest()))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_floating_ip_simple_associate_supported, 5,
- mock.call(helpers.IsHttpRequest()))
-
- @helpers.create_mocks({
- api.nova: ('flavor_list',
- 'server_list_paged',
- 'tenant_absolute_limits',
- 'is_feature_available',),
- api.glance: ('image_list_detailed',),
- api.neutron: ('floating_ip_simple_associate_supported',
- 'floating_ip_supported',),
- api.network: ('servers_update_addresses',),
- api.cinder: ('volume_list',),
- })
- def test_launch_button_disabled_when_quota_exceeded(self):
- servers = self.servers.list()
- limits = self.limits['absolute']
- limits['totalInstancesUsed'] = limits['maxTotalInstances']
-
- self.mock_is_feature_available.return_value = True
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_image_list_detailed.return_value = (self.images.list(),
- False, False)
- self.mock_server_list_paged.return_value = [servers, False, False]
- self.mock_servers_update_addresses.return_value = None
- self.mock_tenant_absolute_limits.return_value = limits
- self.mock_floating_ip_supported.return_value = True
- self.mock_floating_ip_simple_associate_supported.return_value = True
-
- tables.LaunchLink()
- res = self.client.get(INDEX_URL)
-
- launch_action = self.getAndAssertTableAction(
- res, 'instances', 'launch-ng')
-
- self.assertIn('disabled', launch_action.classes,
- 'The launch button should be disabled')
- self.assertEqual('Launch Instance (Quota exceeded)',
- launch_action.verbose_name)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_is_feature_available, 10,
- mock.call(helpers.IsHttpRequest(), 'locked_attribute'))
- self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
- self._assert_mock_image_list_detailed_calls()
-
- search_opts = {'marker': None, 'paginate': True}
- self.mock_server_list_paged.assert_called_once_with(
- helpers.IsHttpRequest(),
- sort_dir='desc',
- search_opts=search_opts)
- self.mock_servers_update_addresses.assert_called_once_with(
- helpers.IsHttpRequest(), servers)
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_tenant_absolute_limits, 3,
- mock.call(helpers.IsHttpRequest(), reserved=True))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_floating_ip_supported, 10,
- mock.call(helpers.IsHttpRequest()))
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_floating_ip_simple_associate_supported, 5,
- mock.call(helpers.IsHttpRequest()))
-
- @helpers.create_mocks({api.glance: ('image_list_detailed',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'availability_zone_list',
- 'server_group_list',
- 'tenant_absolute_limits',
- 'server_create',),
- cinder: ('volume_list',
- 'volume_snapshot_list',),
- quotas: ('tenant_quota_usages',)})
- def test_launch_with_empty_device_name_allowed(self):
- flavor = self.flavors.get(name='m1.massive')
- image = self.versioned_images.first()
- keypair = self.keypairs.first()
- server = self.servers.first()
- sec_group = self.security_groups.first()
- avail_zone = self.availability_zones.first()
- customization_script = 'user data'
- nics = [{'net-id': self.networks.first().id, 'v4-fixed-ip': ''}]
- device_name = ''
- quota_usages = self.quota_usages.first()
- quota_usages['cores']['available'] = 2000
- device_mapping_v2 = [{'device_name': None, # device_name must be None
- 'source_type': 'image',
- 'destination_type': 'volume',
- 'delete_on_termination': False,
- 'uuid': image.id,
- 'boot_index': '0',
- 'volume_size': image.size}]
-
- self._mock_nova_glance_neutron_lists()
- volumes = [v for v in self.cinder_volumes.list()
- if (v.status == AVAILABLE and v.bootable == 'true')]
- self.mock_volume_list.return_value = volumes
- self.mock_volume_snapshot_list.return_value = []
- self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_tenant_quota_usages.return_value = quota_usages
- self.mock_server_create.return_value = None
-
- form_data = {
- 'flavor': flavor.id,
- 'source_type': 'volume_image_id',
- 'image_id': image.id,
- 'availability_zone': avail_zone.zoneName,
- 'keypair': keypair.name,
- 'name': server.name,
- 'script_source': 'raw',
- 'script_data': customization_script,
- 'project_id': self.tenants.first().id,
- 'user_id': self.user.id,
- 'groups': str(sec_group.id),
- 'volume_size': image.size,
- 'device_name': device_name,
- 'network': self.networks.first().id,
- 'count': 1
- }
- url = reverse('horizon:project:instances:launch')
-
- res = self.client.post(url, form_data)
- self.assertNoFormErrors(res)
-
- self._check_nova_glance_neutron_lists(flavor_count=2,
- image_count=8)
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(),
- search_opts=SNAPSHOT_SEARCH_OPTS)
-
- self.assert_mock_multiple_calls_with_same_arguments(
- self.mock_flavor_list, 2,
- mock.call(helpers.IsHttpRequest()))
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', ))
- self.mock_server_create.assert_called_once_with(
- helpers.IsHttpRequest(),
- server.name,
- '',
- flavor.id,
- keypair.name,
- customization_script,
- [str(sec_group.id)],
- block_device_mapping=None,
- block_device_mapping_v2=device_mapping_v2,
- nics=nics,
- availability_zone=avail_zone.zoneName,
- instance_count=helpers.IsA(int),
- admin_pass='',
- config_drive=False,
- disk_config='',
- scheduler_hints={})
-
-
class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
@helpers.create_mocks({
@@ -4052,51 +2105,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_floating_ip_simple_associate_supported, 5,
mock.call(helpers.IsHttpRequest()))
- @helpers.create_mocks({api.nova: ('is_feature_available',
- 'flavor_list',
- 'keypair_list',
- 'server_group_list',
- 'availability_zone_list'),
- cinder: ('volume_snapshot_list',
- 'volume_list',),
- api.neutron: ('network_list',
- 'port_list_with_trunk_types',
- 'security_group_list',),
- api.glance: ('image_list_detailed',),
- quotas: ('tenant_quota_usages',)})
- def test_select_default_keypair_if_only_one(self):
- keypair = self.keypairs.first()
-
- self.mock_volume_list.return_value = []
- self.mock_volume_snapshot_list.return_value = []
- self._mock_glance_image_list_detailed(self.versioned_images.list())
- self._mock_neutron_network_and_port_list()
- self.mock_tenant_quota_usages.return_value = self.quota_usages.first()
- self._mock_nova_lists()
-
- url = reverse('horizon:project:instances:launch')
- res = self.client.get(url)
- self.assertContains(
- res, "" % {'key': keypair.name},
- html=True,
- msg_prefix="The default key pair was not selected.")
-
- self.mock_volume_list.assert_has_calls([
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_SEARCH_OPTS),
- mock.call(helpers.IsHttpRequest(),
- search_opts=VOLUME_BOOTABLE_SEARCH_OPTS),
- ])
- self.mock_volume_snapshot_list.assert_called_once_with(
- helpers.IsHttpRequest(), search_opts=SNAPSHOT_SEARCH_OPTS)
- self._check_glance_image_list_detailed(count=8)
- self._check_neutron_network_and_port_list()
- self.mock_tenant_quota_usages.assert_called_once_with(
- helpers.IsHttpRequest(),
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- self._check_nova_lists(flavor_count=2)
-
@helpers.create_mocks({
api.neutron: ('floating_ip_target_list_by_instance',
'tenant_floating_ip_list',
@@ -4149,24 +2157,36 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
@helpers.create_mocks({api.nova: ('server_get',
'flavor_list',
- 'server_group_list',
'tenant_absolute_limits',
'is_feature_available')})
def test_instance_resize_get(self):
server = self.servers.first()
+ flavor = self.flavors.first()
self.mock_server_get.return_value = server
self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
self.mock_tenant_absolute_limits.return_value = self.limits['absolute']
url = reverse('horizon:project:instances:resize', args=[server.id])
res = self.client.get(url)
+ workflow = res.context['workflow']
self.assertTemplateUsed(res, views.WorkflowView.template_name)
+ self.assertEqual(res.context['workflow'].name,
+ workflows.ResizeInstance.name)
+ self.assertContains(res, 'Disk Partition')
config_drive_field_label = 'Configuration Drive'
self.assertNotContains(res, config_drive_field_label)
+ step = workflow.get_step("flavor_choice")
+ self.assertEqual(step.action.initial['old_flavor_id'], flavor.id)
+
+ step = workflow.get_step("setadvancedaction")
+ self.assertEqual(step.action.fields['disk_config'].label,
+ 'Disk Partition')
+ self.assertQuerysetEqual(workflow.steps,
+ ['',
+ ''])
option = ''
for flavor in self.flavors.list():
if flavor.id == server.flavor['id']:
@@ -4179,8 +2199,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_flavor_list, 2,
mock.call(helpers.IsHttpRequest()))
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
self.mock_tenant_absolute_limits.assert_called_once_with(
helpers.IsHttpRequest(), reserved=True)
@@ -4219,7 +2237,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
@helpers.create_mocks({api.nova: ('server_get',
'flavor_list',
'flavor_get',
- 'server_group_list',
'tenant_absolute_limits',
'is_feature_available')})
def test_instance_resize_get_current_flavor_not_found(self):
@@ -4227,7 +2244,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.return_value = server
self.mock_flavor_list.return_value = []
self.mock_flavor_get.side_effect = self.exceptions.nova
- self.mock_server_group_list.return_value = self.server_groups.list()
self.mock_tenant_absolute_limits.return_value = self.limits['absolute']
url = reverse('horizon:project:instances:resize', args=[server.id])
@@ -4242,8 +2258,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
mock.call(helpers.IsHttpRequest()))
self.mock_flavor_get.assert_called_once_with(
helpers.IsHttpRequest(), server.flavor['id'])
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
self.mock_tenant_absolute_limits.assert_called_once_with(
helpers.IsHttpRequest(), reserved=True)
@@ -4257,7 +2271,7 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
instance_resize_post_stubs = {
api.nova: ('server_get', 'server_resize',
- 'flavor_list', 'flavor_get', 'server_group_list',
+ 'flavor_list', 'flavor_get',
'is_feature_available')}
@helpers.create_mocks(instance_resize_post_stubs)
@@ -4269,7 +2283,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.return_value = server
self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
self.mock_server_resize.return_value = []
res = self._instance_resize_post(server.id, flavor.id, 'AUTO')
@@ -4279,8 +2292,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(helpers.IsHttpRequest(),
server.id)
self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
self.mock_server_resize.assert_called_once_with(
helpers.IsHttpRequest(), server.id, flavor.id, 'AUTO')
@@ -4293,7 +2304,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.return_value = server
self.mock_flavor_list.return_value = self.flavors.list()
- self.mock_server_group_list.return_value = self.server_groups.list()
self.mock_server_resize.side_effect = self.exceptions.nova
res = self._instance_resize_post(server.id, flavor.id, 'AUTO')
@@ -4302,8 +2312,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
self.mock_server_get.assert_called_once_with(helpers.IsHttpRequest(),
server.id)
self.mock_flavor_list.assert_called_once_with(helpers.IsHttpRequest())
- self.mock_server_group_list.assert_called_once_with(
- helpers.IsHttpRequest())
self.mock_server_resize.assert_called_once_with(
helpers.IsHttpRequest(), server.id, flavor.id, 'AUTO')
@@ -4538,7 +2546,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
def test_index_form_action_with_pagination(self):
# The form action on the next page should have marker
# object from the previous page last element.
-
page_size = settings.API_RESULT_PAGE_SIZE
servers = self.servers.list()[:3]
@@ -4664,49 +2671,6 @@ class InstanceTests2(InstanceTestBase, InstanceTableTestMixin):
def read(self):
return self.data
- def test_clean_file_upload_form_oversize_data(self):
- t = workflows.create_instance.CustomizeAction(self.request, {})
- upload_str = 'user data'
- files = {'script_upload':
- self.SimpleFile('script_name',
- upload_str,
- (16 * 1024) + 1)}
-
- self.assertRaises(
- forms.ValidationError,
- t.clean_uploaded_files,
- 'script',
- files)
-
- def test_clean_file_upload_form_invalid_data(self):
- t = workflows.create_instance.CustomizeAction(self.request, {})
- upload_str = b'\x81'
- files = {'script_upload':
- self.SimpleFile('script_name',
- upload_str,
- sys.getsizeof(upload_str))}
-
- self.assertRaises(
- forms.ValidationError,
- t.clean_uploaded_files,
- 'script',
- files)
-
- def test_clean_file_upload_form_valid_data(self):
- t = workflows.create_instance.CustomizeAction(self.request, {})
- precleaned = 'user data'
- upload_str = 'user data'
- files = {'script_upload':
- self.SimpleFile('script_name',
- upload_str,
- sys.getsizeof(upload_str))}
-
- cleaned = t.clean_uploaded_files('script', files)
-
- self.assertEqual(
- cleaned,
- precleaned)
-
def _server_rescue_post(self, server_id, image_id,
password=None):
form_data = {'instance_id': server_id,
diff --git a/openstack_dashboard/dashboards/project/instances/urls.py b/openstack_dashboard/dashboards/project/instances/urls.py
index b531ce996b..7d76c3ef82 100644
--- a/openstack_dashboard/dashboards/project/instances/urls.py
+++ b/openstack_dashboard/dashboards/project/instances/urls.py
@@ -26,7 +26,6 @@ INSTANCES_KEYPAIR = r'^(?P[^/]+)/(?P[^/]+)/%s$'
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
- url(r'^launch$', views.LaunchInstanceView.as_view(), name='launch'),
url(r'^(?P[^/]+)/$',
views.DetailView.as_view(), name='detail'),
url(INSTANCES % 'update', views.UpdateView.as_view(), name='update'),
diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py
index f767c538fa..ee8b1636c3 100644
--- a/openstack_dashboard/dashboards/project/instances/views.py
+++ b/openstack_dashboard/dashboards/project/instances/views.py
@@ -40,7 +40,6 @@ from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.utils import filters
-from openstack_dashboard.utils import settings as setting_utils
from openstack_dashboard.dashboards.project.instances \
import console as project_console
@@ -260,26 +259,6 @@ def _swap_filter(resources, search_opts, fake_field, real_field):
return True
-class LaunchInstanceView(workflows.WorkflowView):
- workflow_class = project_workflows.LaunchInstance
-
- def __init__(self):
- super().__init__()
- LOG.warning('Django version of the launch instance form is '
- 'deprecated since Wallaby release. Switch to '
- 'the AngularJS version of the form by setting '
- 'LAUNCH_INSTANCE_NG_ENABLED to True and '
- 'LAUNCH_INSTANCE_LEGACY_ENABLED to False.')
-
- def get_initial(self):
- initial = super().get_initial()
- initial['project_id'] = self.request.user.tenant_id
- initial['user_id'] = self.request.user.id
- initial['config_drive'] = setting_utils.get_dict_config(
- 'LAUNCH_INSTANCE_DEFAULTS', 'config_drive')
- return initial
-
-
# TODO(stephenfin): Migrate to CBV
def console(request, instance_id):
data = _('Unable to get log for instance "%s".') % instance_id
diff --git a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py
index 54ac87e84f..dde37d252a 100644
--- a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py
+++ b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py
@@ -10,8 +10,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from openstack_dashboard.dashboards.project.instances.workflows.\
- create_instance import LaunchInstance
from openstack_dashboard.dashboards.project.instances.workflows.\
resize_instance import ResizeInstance
from openstack_dashboard.dashboards.project.instances.workflows.\
@@ -20,7 +18,6 @@ from openstack_dashboard.dashboards.project.instances.workflows.\
update_port import UpdatePort
__all__ = [
- 'LaunchInstance',
'ResizeInstance',
'UpdateInstance',
'UpdatePort',
diff --git a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py
deleted file mode 100644
index fd575e353d..0000000000
--- a/openstack_dashboard/dashboards/project/instances/workflows/create_instance.py
+++ /dev/null
@@ -1,959 +0,0 @@
-# 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 oslo_utils import units
-
-from django.template.defaultfilters import filesizeformat
-from django.utils.text import normalize_newlines
-from django.utils.translation import ugettext_lazy as _
-from django.views.decorators.debug import sensitive_variables
-
-from horizon import exceptions
-from horizon import forms
-from horizon.utils import functions
-from horizon.utils import memoized
-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.api import nova
-from openstack_dashboard.usage import quotas
-
-from openstack_dashboard.dashboards.project.images.images \
- import tables as image_tables
-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.ThemableChoiceField(label=_("Project"))
- user_id = forms.ThemableChoiceField(label=_("User"))
-
- def __init__(self, request, *args, **kwargs):
- super().__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(object):
- 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.ThemableChoiceField(label=_("Availability Zone"),
- required=False)
-
- name = forms.CharField(label=_("Instance Name"),
- max_length=255)
-
- flavor = forms.ThemableChoiceField(label=_("Flavor"),
- help_text=_("Size of image to launch."))
-
- count = forms.IntegerField(label=_("Number of Instances"),
- min_value=1,
- initial=1)
-
- source_type = forms.ThemableChoiceField(
- label=_("Instance Boot Source"),
- help_text=_("Choose Your Boot Source "
- "Type."))
-
- instance_snapshot_id = forms.ThemableChoiceField(
- label=_("Instance Snapshot"),
- required=False)
-
- volume_id = forms.ThemableChoiceField(label=_("Volume"), required=False)
-
- volume_snapshot_id = forms.ThemableChoiceField(label=_("Volume Snapshot"),
- required=False)
-
- image_id = forms.ChoiceField(
- label=_("Image Name"),
- required=False,
- widget=forms.ThemableSelectWidget(
- 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=0,
- 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'). Leave "
- "this field blank to let the "
- "system choose a device name "
- "for you."))
-
- vol_delete_on_instance_delete = forms.BooleanField(
- label=_("Delete Volume on Instance Delete"),
- initial=False,
- required=False,
- help_text=_("Delete volume when the instance is deleted"))
-
- class Meta(object):
- 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().__init__(request, context, *args, **kwargs)
-
- # Hide the device field if the hypervisor doesn't support it.
- if not nova.can_set_mount_point():
- self.fields['device_name'].widget = forms.widgets.HiddenInput()
-
- source_type_choices = [
- ('', _("Select source")),
- ("image_id", _("Boot from image")),
- ("instance_snapshot_id", _("Boot from snapshot")),
- ]
- if cinder.is_volume_service_enabled(request):
- source_type_choices += [
- ("volume_id", _("Boot from volume")),
- ("volume_image_id",
- _("Boot from image (creates a new volume)")),
- ("volume_snapshot_id",
- _("Boot from volume snapshot (creates a new volume)")),
- ]
- self.fields['source_type'].choices = source_type_choices
-
- @memoized.memoized_method
- def _get_flavor(self, flavor_id):
- try:
- # 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
- return flavor
-
- @memoized.memoized_method
- def _get_image(self, image_id):
- try:
- # 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
- return image
-
- def _check_quotas(self, cleaned_data):
- count = cleaned_data.get('count', 1)
-
- # Prevent launching more instances than the quota allows
- usages = quotas.tenant_quota_usages(
- self.request,
- targets=('instances', 'cores', 'ram', 'volumes', ))
- available_count = usages['instances']['available']
- if available_count < count:
- msg = (_('The requested instance(s) cannot be launched '
- 'as your quota will be exceeded: Available: '
- '%(avail)s, Requested: %(req)s.')
- % {'avail': available_count, 'req': count})
- raise forms.ValidationError(msg)
-
- source_type = cleaned_data.get('source_type')
- if source_type in ('volume_image_id', 'volume_snapshot_id'):
- available_volume = usages['volumes']['available']
- if available_volume < count:
- msg = (_('The requested instance cannot be launched. '
- 'Requested volume exceeds quota: Available: '
- '%(avail)s, Requested: %(req)s.')
- % {'avail': available_volume, 'req': count})
- raise forms.ValidationError(msg)
-
- flavor_id = cleaned_data.get('flavor')
- flavor = self._get_flavor(flavor_id)
-
- 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])
-
- def _check_flavor_for_image(self, cleaned_data):
- # Prevents trying to launch an image needing more resources.
- image_id = cleaned_data.get('image_id')
- image = self._get_image(image_id)
- flavor_id = cleaned_data.get('flavor')
- flavor = self._get_flavor(flavor_id)
- if not image or not flavor:
- return
- props_mapping = (("min_ram", "ram"), ("min_disk", "disk"))
- for iprop, fprop in props_mapping:
- if (getattr(image, iprop) > 0 and
- getattr(flavor, fprop) > 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.
-
- def _check_volume_for_image(self, cleaned_data):
- image_id = cleaned_data.get('image_id')
- image = self._get_image(image_id)
- volume_size = cleaned_data.get('volume_size')
- if not image or not volume_size:
- return
- 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])
-
- def _check_source_image(self, cleaned_data):
- if not cleaned_data.get('image_id'):
- msg = _("You must select an image.")
- self._errors['image_id'] = self.error_class([msg])
- else:
- self._check_flavor_for_image(cleaned_data)
-
- def _check_source_volume_image(self, cleaned_data):
- volume_size = self.data.get('volume_size', None)
- if not volume_size:
- msg = _("You must set volume size")
- self._errors['volume_size'] = self.error_class([msg])
- if float(volume_size) <= 0:
- msg = _("Volume size must be greater than 0")
- self._errors['volume_size'] = 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])
- return
- self._check_flavor_for_image(cleaned_data)
- self._check_volume_for_image(cleaned_data)
-
- def _check_source_instance_snapshot(self, cleaned_data):
- # using the array form of get blows up with KeyError
- # if instance_snapshot_id is nil
- if not cleaned_data.get('instance_snapshot_id'):
- msg = _("You must select a snapshot.")
- self._errors['instance_snapshot_id'] = self.error_class([msg])
-
- def _check_source_volume(self, cleaned_data):
- 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?
- count = cleaned_data.get('count', 1)
- if count > 1:
- msg = _('Launching multiple instances is only supported for '
- 'images and instance snapshots.')
- raise forms.ValidationError(msg)
-
- def _check_source_volume_snapshot(self, cleaned_data):
- if not cleaned_data.get('volume_snapshot_id'):
- msg = _("You must select a snapshot.")
- self._errors['volume_snapshot_id'] = self.error_class([msg])
-
- def _check_source(self, cleaned_data):
- # Validate our instance source.
- source_type = self.data.get('source_type', None)
- source_check_methods = {
- 'image_id': self._check_source_image,
- 'volume_image_id': self._check_source_volume_image,
- 'instance_snapshot_id': self._check_source_instance_snapshot,
- 'volume_id': self._check_source_volume,
- 'volume_snapshot_id': self._check_source_volume_snapshot
- }
- check_method = source_check_methods.get(source_type)
- if check_method:
- check_method(cleaned_data)
-
- def clean(self):
- cleaned_data = super().clean()
-
- self._check_quotas(cleaned_data)
- self._check_source(cleaned_data)
-
- return cleaned_data
-
- def populate_flavor_choices(self, request, context):
- return instance_utils.flavor_field_data(request, False)
-
- 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 = {} if extra_context is None else dict(extra_context)
- try:
- extra['usages'] = quotas.tenant_quota_usages(
- self.request,
- targets=('instances', 'cores', 'ram', 'volumes', 'gigabytes'))
- extra['usages_json'] = json.dumps(extra['usages'])
- extra['cinder_enabled'] = \
- base.is_service_enabled(self.request, 'volume')
- 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),
- 'size': functions.bytes_to_gigabytes(i.size)}
- for i in images]
- extra['images'] = json.dumps(attrs)
-
- except Exception:
- exceptions.handle(self.request,
- _("Unable to retrieve quota information."))
- return super().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:
- if image_tables.get_image_type(image) != "snapshot":
- image.bytes = getattr(
- image, 'virtual_size', None) or 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 or '')
- 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_tables.get_image_type(image) == "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):
- volumes = []
- try:
- if cinder.is_volume_service_enabled(request):
- available = api.cinder.VOLUME_STATE_AVAILABLE
- volumes = [self._get_volume_display_name(v)
- for v in cinder.volume_list(self.request,
- search_opts=dict(status=available, bootable=True))]
- except Exception:
- 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):
- snapshots = []
- try:
- if cinder.is_volume_service_enabled(request):
- available = api.cinder.VOLUME_STATE_AVAILABLE
- volumes = [v.id for v in cinder.volume_list(
- self.request, search_opts=dict(bootable=True))]
- snapshots = [self._get_volume_display_name(s)
- for s in cinder.volume_snapshot_list(
- self.request, search_opts=dict(status=available))
- if s.volume_id in volumes]
- except Exception:
- 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.
- "vol_delete_on_instance_delete")
-
- 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().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:key_pairs:import"
-
-
-class SetAccessControlsAction(workflows.Action):
- keypair = forms.ThemableDynamicChoiceField(
- label=_("Key Pair"),
- help_text=_("Key pair to use for "
- "authentication."),
- add_item_link=KEYPAIR_IMPORT_URL)
- admin_pass = forms.RegexField(
- label=_("Admin Password"),
- 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 Password"),
- strip=False,
- required=False,
- widget=forms.PasswordInput(render_value=False))
- groups = forms.MultipleChoiceField(
- label=_("Security Groups"),
- required=False,
- initial=["default"],
- widget=forms.ThemableCheckboxSelectMultiple(),
- help_text=_("Launch instance in these "
- "security groups."))
-
- class Meta(object):
- 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().__init__(request, *args, **kwargs)
- if not api.nova.can_set_server_password():
- del self.fields['admin_pass']
- del self.fields['confirm_admin_pass']
- self.fields['keypair'].required = api.nova.requires_keypair()
-
- def populate_keypair_choices(self, request, context):
- keypairs = instance_utils.keypair_field_data(request, True)
- if len(keypairs) == 2:
- self.fields['keypair'].initial = keypairs[1][0]
- return keypairs
-
- def populate_groups_choices(self, request, context):
- try:
- groups = api.neutron.security_group_list(request)
- security_group_list = [(sg.id, 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().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(object):
- name = _("Post-Creation")
- help_text_template = ("project/instances/"
- "_launch_customize_help.html")
-
- source_choices = [('', _('Select Script Source')),
- ('raw', _('Direct Input')),
- ('file', _('File'))]
-
- attributes = {'class': 'switchable', 'data-slug': 'scriptsource'}
- script_source = forms.ChoiceField(
- label=_('Customization Script Source'),
- choices=source_choices,
- widget=forms.ThemableSelectWidget(attrs=attributes),
- required=False)
-
- 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 clean(self):
- cleaned = super().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"
-
- if upload_str not in files:
- return None
-
- upload_file = files[upload_str]
- log_script_name = upload_file.name
- LOG.info('got upload %s', log_script_name)
-
- if upload_file._size > 16 * units.Ki: # 16kb
- msg = _('File exceeds maximum size (16kb)')
- raise forms.ValidationError(msg)
-
- script = upload_file.read()
- if script != "":
- try:
- if not isinstance(script, str):
- script = script.decode()
- 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
-
-
-class PostCreationStep(workflows.Step):
- action_class = CustomizeAction
- contributes = ("script_data",)
-
-
-class SetNetworkAction(workflows.Action):
- network = forms.MultipleChoiceField(
- label=_("Networks"),
- widget=forms.ThemableCheckboxSelectMultiple(),
- error_messages={
- 'required': _(
- "At least one network must"
- " be specified.")},
- help_text=_("Launch instance with"
- " these networks"))
-
- def __init__(self, request, *args, **kwargs):
- super().__init__(request, *args, **kwargs)
-
- # NOTE(e0ne): we don't need 'required attribute for networks
- # checkboxes to be able to select only one network
- # NOTE(e0ne): we need it for compatibility with different
- # Django versions (prior to 1.11)
- self.use_required_attribute = False
-
- 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):
- return instance_utils.network_field_data(request, for_launch=True)
-
-
-class SetNetwork(workflows.Step):
- action_class = SetNetworkAction
- 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
- return context
-
-
-class SetNetworkPortsAction(workflows.Action):
- ports = forms.MultipleChoiceField(label=_("Ports"),
- widget=forms.CheckboxSelectMultiple(),
- required=False,
- help_text=_("Launch instance with"
- " these ports"))
-
- class Meta(object):
- name = _("Network Ports")
- permissions = ('openstack.services.network',)
- help_text_template = ("project/instances/"
- "_launch_network_ports_help.html")
-
- def populate_ports_choices(self, request, context):
- ports = instance_utils.port_field_data(request)
- if not ports:
- self.fields['ports'].label = _("No ports available")
- self.fields['ports'].help_text = _("No ports available")
- return ports
-
-
-class SetNetworkPorts(workflows.Step):
- action_class = SetNetworkPortsAction
- contributes = ("ports",)
-
- def contribute(self, data, context):
- if data:
- ports = self.workflow.request.POST.getlist("ports")
- if ports:
- context['ports'] = ports
- return context
-
-
-class SetAdvancedAction(workflows.Action):
- disk_config = forms.ThemableChoiceField(
- 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."))
- server_group = forms.ThemableChoiceField(
- label=_("Server Group"), required=False,
- help_text=_("Server group to associate with this instance."))
-
- def __init__(self, request, context, *args, **kwargs):
- super().__init__(request, context, *args, **kwargs)
- try:
- config_choices = [("AUTO", _("Automatic")),
- ("MANUAL", _("Manual"))]
- self.fields['disk_config'].choices = config_choices
-
- # Only show the Config Drive option for the Launch Instance
- # is supported.
- if context.get('workflow_slug') != 'launch_instance':
- del self.fields['config_drive']
-
- server_group_choices = instance_utils.server_group_field_data(
- request)
- self.fields['server_group'].choices = server_group_choices
- except Exception:
- exceptions.handle(request, _('Unable to retrieve extensions '
- 'information.'))
-
- class Meta(object):
- name = _("Advanced Options")
- help_text_template = ("project/instances/"
- "_launch_advanced_help.html")
-
-
-class SetAdvanced(workflows.Step):
- action_class = SetAdvancedAction
- contributes = ("disk_config", "config_drive", "server_group",)
-
- def prepare_action_context(self, request, context):
- context = super().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 = _('Request for launching %(count)s named "%(name)s" '
- 'has been submitted.')
- failure_message = _('Unable to launch %(count)s named "%(name)s".')
- success_url = "horizon:project:instances:index"
- multipart = True
- default_steps = (SelectProjectUser,
- SetInstanceDetails,
- SetAccessControls,
- SetNetwork,
- SetNetworkPorts,
- 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}
- 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']:
- # Volume source id is extracted from the source
- volume_source_id = context['source_id'].split(':')[0]
- device_name = context.get('device_name', '').strip() or None
- dev_source_type_mapping = {
- 'volume_id': 'volume',
- 'volume_snapshot_id': 'snapshot'
- }
- dev_mapping_2 = [
- {'device_name': device_name,
- 'source_type': dev_source_type_mapping[source_type],
- 'destination_type': 'volume',
- 'delete_on_termination':
- bool(context['vol_delete_on_instance_delete']),
- 'uuid': volume_source_id,
- 'boot_index': '0',
- 'volume_size': context['volume_size']
- }
- ]
- elif source_type == 'volume_image_id':
- device_name = context.get('device_name', '').strip() or None
- dev_mapping_2 = [
- {'device_name': device_name, # None auto-selects device
- 'source_type': 'image',
- 'destination_type': 'volume',
- 'delete_on_termination':
- bool(context['vol_delete_on_instance_delete']),
- '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)
-
- scheduler_hints = {}
- server_group = context.get('server_group', None)
- if server_group:
- scheduler_hints['group'] = server_group
-
- ports = context.get('ports')
- if ports:
- if nics is None:
- nics = []
- nics.extend([{'port-id': port} for port in ports])
-
- 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'),
- scheduler_hints=scheduler_hints)
- return True
- except Exception:
- exceptions.handle(request)
- return False
-
-
-def _cleanup_ports_on_failed_vm_launch(request, nics):
- ports_failing_deletes = []
- LOG.debug('Cleaning up stale VM ports.')
- for nic in nics:
- try:
- LOG.debug('Deleting port with id: %s', nic['port-id'])
- api.neutron.port_delete(request, nic['port-id'])
- except Exception:
- ports_failing_deletes.append(nic['port-id'])
- return ports_failing_deletes
diff --git a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py
index bbc6906c5b..00d25938fa 100644
--- a/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py
+++ b/openstack_dashboard/dashboards/project/instances/workflows/resize_instance.py
@@ -25,8 +25,53 @@ from horizon import workflows
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.instances \
import utils as instance_utils
-from openstack_dashboard.dashboards.project.instances.workflows \
- import create_instance
+
+
+class SetAdvancedAction(workflows.Action):
+ disk_config = forms.ThemableChoiceField(
+ 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().__init__(request, context, *args, **kwargs)
+ try:
+ config_choices = [("AUTO", _("Automatic")),
+ ("MANUAL", _("Manual"))]
+ self.fields['disk_config'].choices = config_choices
+
+ # Only show the Config Drive option for the Launch Instance
+ # is supported.
+ if context.get('workflow_slug') != 'launch_instance':
+ del self.fields['config_drive']
+
+ except Exception:
+ exceptions.handle(request, _('Unable to retrieve extensions '
+ 'information.'))
+
+ class Meta(object):
+ 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().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 SetFlavorChoiceAction(workflows.Action):
@@ -94,7 +139,7 @@ class ResizeInstance(workflows.Workflow):
'has been submitted.')
failure_message = _('Unable to resize instance "%s".')
success_url = "horizon:project:instances:index"
- default_steps = (SetFlavorChoice, create_instance.SetAdvanced)
+ default_steps = (SetFlavorChoice, SetAdvanced,)
def format_status_message(self, message):
if "%s" in message:
diff --git a/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html
index a609611902..fcefd766b3 100644
--- a/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html
+++ b/openstack_dashboard/dashboards/project/network_topology/templates/network_topology/_actions_list.html
@@ -2,31 +2,17 @@